diff --git a/adapters/outboundgroup/common.go b/adapters/outboundgroup/common.go index ce40072c..c5c719f2 100644 --- a/adapters/outboundgroup/common.go +++ b/adapters/outboundgroup/common.go @@ -11,10 +11,14 @@ const ( defaultGetProxiesDuration = time.Second * 5 ) -func getProvidersProxies(providers []provider.ProxyProvider) []C.Proxy { +func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy { proxies := []C.Proxy{} for _, provider := range providers { - proxies = append(proxies, provider.Proxies()...) + if touch { + proxies = append(proxies, provider.ProxiesWithTouch()...) + } else { + proxies = append(proxies, provider.Proxies()...) + } } return proxies } diff --git a/adapters/outboundgroup/fallback.go b/adapters/outboundgroup/fallback.go index 8fb31c8e..756c4e81 100644 --- a/adapters/outboundgroup/fallback.go +++ b/adapters/outboundgroup/fallback.go @@ -18,12 +18,12 @@ type Fallback struct { } func (f *Fallback) Now() string { - proxy := f.findAliveProxy() + proxy := f.findAliveProxy(false) return proxy.Name() } func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - proxy := f.findAliveProxy() + proxy := f.findAliveProxy(true) c, err := proxy.DialContext(ctx, metadata) if err == nil { c.AppendToChains(f) @@ -32,7 +32,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con } func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { - proxy := f.findAliveProxy() + proxy := f.findAliveProxy(true) pc, err := proxy.DialUDP(metadata) if err == nil { pc.AppendToChains(f) @@ -45,13 +45,13 @@ func (f *Fallback) SupportUDP() bool { return false } - proxy := f.findAliveProxy() + proxy := f.findAliveProxy(false) return proxy.SupportUDP() } func (f *Fallback) MarshalJSON() ([]byte, error) { var all []string - for _, proxy := range f.proxies() { + for _, proxy := range f.proxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]interface{}{ @@ -62,27 +62,27 @@ func (f *Fallback) MarshalJSON() ([]byte, error) { } func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy { - proxy := f.findAliveProxy() + proxy := f.findAliveProxy(true) return proxy } -func (f *Fallback) proxies() []C.Proxy { +func (f *Fallback) proxies(touch bool) []C.Proxy { elm, _, _ := f.single.Do(func() (interface{}, error) { - return getProvidersProxies(f.providers), nil + return getProvidersProxies(f.providers, touch), nil }) return elm.([]C.Proxy) } -func (f *Fallback) findAliveProxy() C.Proxy { - proxies := f.proxies() +func (f *Fallback) findAliveProxy(touch bool) C.Proxy { + proxies := f.proxies(touch) for _, proxy := range proxies { if proxy.Alive() { return proxy } } - return f.proxies()[0] + return proxies[0] } func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { diff --git a/adapters/outboundgroup/loadbalance.go b/adapters/outboundgroup/loadbalance.go index 992b2c43..289d0b46 100644 --- a/adapters/outboundgroup/loadbalance.go +++ b/adapters/outboundgroup/loadbalance.go @@ -131,13 +131,13 @@ func strategyConsistentHashing() strategyFn { } func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy { - proxies := lb.proxies() + proxies := lb.proxies(true) return lb.strategyFn(proxies, metadata) } -func (lb *LoadBalance) proxies() []C.Proxy { +func (lb *LoadBalance) proxies(touch bool) []C.Proxy { elm, _, _ := lb.single.Do(func() (interface{}, error) { - return getProvidersProxies(lb.providers), nil + return getProvidersProxies(lb.providers, touch), nil }) return elm.([]C.Proxy) @@ -145,7 +145,7 @@ func (lb *LoadBalance) proxies() []C.Proxy { func (lb *LoadBalance) MarshalJSON() ([]byte, error) { var all []string - for _, proxy := range lb.proxies() { + for _, proxy := range lb.proxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]interface{}{ diff --git a/adapters/outboundgroup/parser.go b/adapters/outboundgroup/parser.go index a6c2c848..82ceaa79 100644 --- a/adapters/outboundgroup/parser.go +++ b/adapters/outboundgroup/parser.go @@ -24,13 +24,16 @@ type GroupCommonOption struct { Use []string `group:"use,omitempty"` URL string `group:"url,omitempty"` Interval int `group:"interval,omitempty"` + Lazy bool `group:"lazy,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"` } func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) { decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) - groupOption := &GroupCommonOption{} + groupOption := &GroupCommonOption{ + Lazy: true, + } if err := decoder.Decode(config, groupOption); err != nil { return nil, errFormat } @@ -55,7 +58,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, // if Use not empty, drop health check options if len(groupOption.Use) != 0 { - hc := provider.NewHealthCheck(ps, "", 0) + hc := provider.NewHealthCheck(ps, "", 0, true) pd, err := provider.NewCompatibleProvider(groupName, ps, hc) if err != nil { return nil, err @@ -69,7 +72,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, // select don't need health check if groupOption.Type == "select" || groupOption.Type == "relay" { - hc := provider.NewHealthCheck(ps, "", 0) + hc := provider.NewHealthCheck(ps, "", 0, true) pd, err := provider.NewCompatibleProvider(groupName, ps, hc) if err != nil { return nil, err @@ -82,7 +85,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, return nil, errMissHealthCheck } - hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval)) + hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy) pd, err := provider.NewCompatibleProvider(groupName, ps, hc) if err != nil { return nil, err diff --git a/adapters/outboundgroup/relay.go b/adapters/outboundgroup/relay.go index adc28881..d2eeba66 100644 --- a/adapters/outboundgroup/relay.go +++ b/adapters/outboundgroup/relay.go @@ -20,7 +20,7 @@ type Relay struct { } func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - proxies := r.proxies(metadata) + proxies := r.proxies(metadata, true) if len(proxies) == 0 { return nil, errors.New("proxy does not exist") } @@ -58,7 +58,7 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, func (r *Relay) MarshalJSON() ([]byte, error) { var all []string - for _, proxy := range r.rawProxies() { + for _, proxy := range r.rawProxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]interface{}{ @@ -67,16 +67,16 @@ func (r *Relay) MarshalJSON() ([]byte, error) { }) } -func (r *Relay) rawProxies() []C.Proxy { +func (r *Relay) rawProxies(touch bool) []C.Proxy { elm, _, _ := r.single.Do(func() (interface{}, error) { - return getProvidersProxies(r.providers), nil + return getProvidersProxies(r.providers, touch), nil }) return elm.([]C.Proxy) } -func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy { - proxies := r.rawProxies() +func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy { + proxies := r.rawProxies(touch) for n, proxy := range proxies { subproxy := proxy.Unwrap(metadata) diff --git a/adapters/outboundgroup/selector.go b/adapters/outboundgroup/selector.go index e4f11b2f..99640745 100644 --- a/adapters/outboundgroup/selector.go +++ b/adapters/outboundgroup/selector.go @@ -20,7 +20,7 @@ type Selector struct { } func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { - c, err := s.selectedProxy().DialContext(ctx, metadata) + c, err := s.selectedProxy(true).DialContext(ctx, metadata) if err == nil { c.AppendToChains(s) } @@ -28,7 +28,7 @@ func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con } func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { - pc, err := s.selectedProxy().DialUDP(metadata) + pc, err := s.selectedProxy(true).DialUDP(metadata) if err == nil { pc.AppendToChains(s) } @@ -40,12 +40,12 @@ func (s *Selector) SupportUDP() bool { return false } - return s.selectedProxy().SupportUDP() + return s.selectedProxy(false).SupportUDP() } func (s *Selector) MarshalJSON() ([]byte, error) { var all []string - for _, proxy := range getProvidersProxies(s.providers) { + for _, proxy := range getProvidersProxies(s.providers, false) { all = append(all, proxy.Name()) } @@ -57,11 +57,11 @@ func (s *Selector) MarshalJSON() ([]byte, error) { } func (s *Selector) Now() string { - return s.selectedProxy().Name() + return s.selectedProxy(false).Name() } func (s *Selector) Set(name string) error { - for _, proxy := range getProvidersProxies(s.providers) { + for _, proxy := range getProvidersProxies(s.providers, false) { if proxy.Name() == name { s.selected = name s.single.Reset() @@ -73,12 +73,12 @@ func (s *Selector) Set(name string) error { } func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy { - return s.selectedProxy() + return s.selectedProxy(true) } -func (s *Selector) selectedProxy() C.Proxy { +func (s *Selector) selectedProxy(touch bool) C.Proxy { elm, _, _ := s.single.Do(func() (interface{}, error) { - proxies := getProvidersProxies(s.providers) + proxies := getProvidersProxies(s.providers, touch) for _, proxy := range proxies { if proxy.Name() == s.selected { return proxy, nil diff --git a/adapters/outboundgroup/urltest.go b/adapters/outboundgroup/urltest.go index 1074a42a..4b9539f1 100644 --- a/adapters/outboundgroup/urltest.go +++ b/adapters/outboundgroup/urltest.go @@ -30,11 +30,11 @@ type URLTest struct { } func (u *URLTest) Now() string { - return u.fast().Name() + return u.fast(false).Name() } func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { - c, err = u.fast().DialContext(ctx, metadata) + c, err = u.fast(true).DialContext(ctx, metadata) if err == nil { c.AppendToChains(u) } @@ -42,7 +42,7 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Co } func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { - pc, err := u.fast().DialUDP(metadata) + pc, err := u.fast(true).DialUDP(metadata) if err == nil { pc.AppendToChains(u) } @@ -50,20 +50,20 @@ func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { } func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy { - return u.fast() + return u.fast(true) } -func (u *URLTest) proxies() []C.Proxy { +func (u *URLTest) proxies(touch bool) []C.Proxy { elm, _, _ := u.single.Do(func() (interface{}, error) { - return getProvidersProxies(u.providers), nil + return getProvidersProxies(u.providers, touch), nil }) return elm.([]C.Proxy) } -func (u *URLTest) fast() C.Proxy { +func (u *URLTest) fast(touch bool) C.Proxy { elm, _, _ := u.fastSingle.Do(func() (interface{}, error) { - proxies := u.proxies() + proxies := u.proxies(touch) fast := proxies[0] min := fast.LastDelay() for _, proxy := range proxies[1:] { @@ -94,12 +94,12 @@ func (u *URLTest) SupportUDP() bool { return false } - return u.fast().SupportUDP() + return u.fast(false).SupportUDP() } func (u *URLTest) MarshalJSON() ([]byte, error) { var all []string - for _, proxy := range u.proxies() { + for _, proxy := range u.proxies(false) { all = append(all, proxy.Name()) } return json.Marshal(map[string]interface{}{ diff --git a/adapters/provider/healthcheck.go b/adapters/provider/healthcheck.go index 61f343b1..d741ed7f 100644 --- a/adapters/provider/healthcheck.go +++ b/adapters/provider/healthcheck.go @@ -5,6 +5,8 @@ import ( "time" C "github.com/Dreamacro/clash/constant" + + "go.uber.org/atomic" ) const ( @@ -17,10 +19,12 @@ type HealthCheckOption struct { } type HealthCheck struct { - url string - proxies []C.Proxy - interval uint - done chan struct{} + url string + proxies []C.Proxy + interval uint + lazy bool + lastTouch *atomic.Int64 + done chan struct{} } func (hc *HealthCheck) process() { @@ -30,7 +34,10 @@ func (hc *HealthCheck) process() { for { select { case <-ticker.C: - hc.check() + now := time.Now().Unix() + if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) { + hc.check() + } case <-hc.done: ticker.Stop() return @@ -46,6 +53,10 @@ func (hc *HealthCheck) auto() bool { return hc.interval != 0 } +func (hc *HealthCheck) touch() { + hc.lastTouch.Store(time.Now().Unix()) +} + func (hc *HealthCheck) check() { ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) for _, proxy := range hc.proxies { @@ -60,11 +71,13 @@ func (hc *HealthCheck) close() { hc.done <- struct{}{} } -func NewHealthCheck(proxies []C.Proxy, url string, interval uint) *HealthCheck { +func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck { return &HealthCheck{ - proxies: proxies, - url: url, - interval: interval, - done: make(chan struct{}, 1), + proxies: proxies, + url: url, + interval: interval, + lazy: lazy, + lastTouch: atomic.NewInt64(0), + done: make(chan struct{}, 1), } } diff --git a/adapters/provider/parser.go b/adapters/provider/parser.go index e662b679..1c9b2e68 100644 --- a/adapters/provider/parser.go +++ b/adapters/provider/parser.go @@ -17,6 +17,7 @@ type healthCheckSchema struct { Enable bool `provider:"enable"` URL string `provider:"url"` Interval int `provider:"interval"` + Lazy bool `provider:"lazy,omitempty"` } type proxyProviderSchema struct { @@ -30,7 +31,11 @@ type proxyProviderSchema struct { func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) { decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) - schema := &proxyProviderSchema{} + schema := &proxyProviderSchema{ + HealthCheck: healthCheckSchema{ + Lazy: true, + }, + } if err := decoder.Decode(mapping, schema); err != nil { return nil, err } @@ -39,7 +44,7 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi if schema.HealthCheck.Enable { hcInterval = uint(schema.HealthCheck.Interval) } - hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval) + hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy) path := C.Path.Resolve(schema.Path) diff --git a/adapters/provider/provider.go b/adapters/provider/provider.go index 09d05430..756a89d1 100644 --- a/adapters/provider/provider.go +++ b/adapters/provider/provider.go @@ -50,6 +50,9 @@ type Provider interface { type ProxyProvider interface { Provider Proxies() []C.Proxy + // ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies. + // Commonly used in Dial and DialUDP + ProxiesWithTouch() []C.Proxy HealthCheck() } @@ -112,6 +115,11 @@ func (pp *proxySetProvider) Proxies() []C.Proxy { return pp.proxies } +func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy { + pp.healthCheck.touch() + return pp.Proxies() +} + func proxiesParse(buf []byte) (interface{}, error) { schema := &ProxySchema{} @@ -223,6 +231,11 @@ func (cp *compatibleProvider) Proxies() []C.Proxy { return cp.proxies } +func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy { + cp.healthCheck.touch() + return cp.Proxies() +} + func stopCompatibleProvider(pd *CompatibleProvider) { pd.healthCheck.close() } diff --git a/config/config.go b/config/config.go index 99d48b92..38a55915 100644 --- a/config/config.go +++ b/config/config.go @@ -345,7 +345,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[ for _, v := range proxyList { ps = append(ps, proxies[v]) } - hc := provider.NewHealthCheck(ps, "", 0) + hc := provider.NewHealthCheck(ps, "", 0, true) pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) providersMap[provider.ReservedName] = pd