[Feature] Proxy stores delay data of different URLs. And supports specifying different test URLs and expected statue by group (#588)
Co-authored-by: Larvan2 <78135608+Larvan2@users.noreply.github.com> Co-authored-by: wwqgtxx <wwqgtxx@gmail.com>
This commit is contained in:
parent
4e46cbfbde
commit
4ec66d299a
21 changed files with 570 additions and 252 deletions
|
@ -3,6 +3,7 @@ package adapter
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -12,16 +13,28 @@ import (
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/atomic"
|
"github.com/Dreamacro/clash/common/atomic"
|
||||||
"github.com/Dreamacro/clash/common/queue"
|
"github.com/Dreamacro/clash/common/queue"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var UnifiedDelay = atomic.NewBool(false)
|
var UnifiedDelay = atomic.NewBool(false)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultHistoriesNum = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
type extraProxyState struct {
|
||||||
|
history *queue.Queue[C.DelayHistory]
|
||||||
|
alive *atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
type Proxy struct {
|
type Proxy struct {
|
||||||
C.ProxyAdapter
|
C.ProxyAdapter
|
||||||
history *queue.Queue[C.DelayHistory]
|
history *queue.Queue[C.DelayHistory]
|
||||||
alive *atomic.Bool
|
alive *atomic.Bool
|
||||||
|
extra map[string]*extraProxyState
|
||||||
}
|
}
|
||||||
|
|
||||||
// Alive implements C.Proxy
|
// Alive implements C.Proxy
|
||||||
|
@ -29,6 +42,17 @@ func (p *Proxy) Alive() bool {
|
||||||
return p.alive.Load()
|
return p.alive.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AliveForTestUrl implements C.Proxy
|
||||||
|
func (p *Proxy) AliveForTestUrl(url string) bool {
|
||||||
|
if p.extra != nil {
|
||||||
|
if state, ok := p.extra[url]; ok {
|
||||||
|
return state.alive.Load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.alive.Load()
|
||||||
|
}
|
||||||
|
|
||||||
// Dial implements C.Proxy
|
// Dial implements C.Proxy
|
||||||
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||||
|
@ -65,6 +89,42 @@ func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||||
return histories
|
return histories
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DelayHistoryForTestUrl implements C.Proxy
|
||||||
|
func (p *Proxy) DelayHistoryForTestUrl(url string) []C.DelayHistory {
|
||||||
|
var queueM []C.DelayHistory
|
||||||
|
if p.extra != nil {
|
||||||
|
if state, ok := p.extra[url]; ok {
|
||||||
|
queueM = state.history.Copy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if queueM == nil {
|
||||||
|
queueM = p.history.Copy()
|
||||||
|
}
|
||||||
|
|
||||||
|
histories := []C.DelayHistory{}
|
||||||
|
for _, item := range queueM {
|
||||||
|
histories = append(histories, item)
|
||||||
|
}
|
||||||
|
return histories
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Proxy) ExtraDelayHistory() map[string][]C.DelayHistory {
|
||||||
|
extra := map[string][]C.DelayHistory{}
|
||||||
|
if p.extra != nil && len(p.extra) != 0 {
|
||||||
|
for url, option := range p.extra {
|
||||||
|
histories := []C.DelayHistory{}
|
||||||
|
queueM := option.history.Copy()
|
||||||
|
for _, item := range queueM {
|
||||||
|
histories = append(histories, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
extra[url] = histories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extra
|
||||||
|
}
|
||||||
|
|
||||||
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
||||||
// implements C.Proxy
|
// implements C.Proxy
|
||||||
func (p *Proxy) LastDelay() (delay uint16) {
|
func (p *Proxy) LastDelay() (delay uint16) {
|
||||||
|
@ -80,6 +140,30 @@ func (p *Proxy) LastDelay() (delay uint16) {
|
||||||
return history.Delay
|
return history.Delay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LastDelayForTestUrl implements C.Proxy
|
||||||
|
func (p *Proxy) LastDelayForTestUrl(url string) (delay uint16) {
|
||||||
|
var max uint16 = 0xffff
|
||||||
|
|
||||||
|
alive := p.alive.Load()
|
||||||
|
history := p.history.Last()
|
||||||
|
|
||||||
|
if p.extra != nil {
|
||||||
|
if state, ok := p.extra[url]; ok {
|
||||||
|
alive = state.alive.Load()
|
||||||
|
history = state.history.Last()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !alive {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
|
||||||
|
if history.Delay == 0 {
|
||||||
|
return max
|
||||||
|
}
|
||||||
|
return history.Delay
|
||||||
|
}
|
||||||
|
|
||||||
// MarshalJSON implements C.ProxyAdapter
|
// MarshalJSON implements C.ProxyAdapter
|
||||||
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||||
inner, err := p.ProxyAdapter.MarshalJSON()
|
inner, err := p.ProxyAdapter.MarshalJSON()
|
||||||
|
@ -90,6 +174,7 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||||
mapping := map[string]any{}
|
mapping := map[string]any{}
|
||||||
_ = json.Unmarshal(inner, &mapping)
|
_ = json.Unmarshal(inner, &mapping)
|
||||||
mapping["history"] = p.DelayHistory()
|
mapping["history"] = p.DelayHistory()
|
||||||
|
mapping["extra"] = p.ExtraDelayHistory()
|
||||||
mapping["name"] = p.Name()
|
mapping["name"] = p.Name()
|
||||||
mapping["udp"] = p.SupportUDP()
|
mapping["udp"] = p.SupportUDP()
|
||||||
mapping["xudp"] = p.SupportXUDP()
|
mapping["xudp"] = p.SupportXUDP()
|
||||||
|
@ -99,17 +184,47 @@ func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
// URLTest get the delay for the specified URL
|
// URLTest get the delay for the specified URL
|
||||||
// implements C.Proxy
|
// implements C.Proxy
|
||||||
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store C.DelayHistoryStoreType) (t uint16, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
p.alive.Store(err == nil)
|
alive := err == nil
|
||||||
|
switch store {
|
||||||
|
case C.OriginalHistory:
|
||||||
|
p.alive.Store(alive)
|
||||||
record := C.DelayHistory{Time: time.Now()}
|
record := C.DelayHistory{Time: time.Now()}
|
||||||
if err == nil {
|
if alive {
|
||||||
record.Delay = t
|
record.Delay = t
|
||||||
}
|
}
|
||||||
p.history.Put(record)
|
p.history.Put(record)
|
||||||
if p.history.Len() > 10 {
|
if p.history.Len() > defaultHistoriesNum {
|
||||||
p.history.Pop()
|
p.history.Pop()
|
||||||
}
|
}
|
||||||
|
case C.ExtraHistory:
|
||||||
|
record := C.DelayHistory{Time: time.Now()}
|
||||||
|
if alive {
|
||||||
|
record.Delay = t
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.extra == nil {
|
||||||
|
p.extra = map[string]*extraProxyState{}
|
||||||
|
}
|
||||||
|
|
||||||
|
state, ok := p.extra[url]
|
||||||
|
if !ok {
|
||||||
|
state = &extraProxyState{
|
||||||
|
history: queue.New[C.DelayHistory](defaultHistoriesNum),
|
||||||
|
alive: atomic.NewBool(true),
|
||||||
|
}
|
||||||
|
p.extra[url] = state
|
||||||
|
}
|
||||||
|
|
||||||
|
state.alive.Store(alive)
|
||||||
|
state.history.Put(record)
|
||||||
|
if state.history.Len() > defaultHistoriesNum {
|
||||||
|
state.history.Pop()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log.Debugln("health check result will be discarded, url: %s alive: %t, delay: %d", url, alive, t)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
unifiedDelay := UnifiedDelay.Load()
|
unifiedDelay := UnifiedDelay.Load()
|
||||||
|
@ -172,12 +287,17 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !expectedStatus.Check(uint16(resp.StatusCode)) {
|
||||||
|
// maybe another value should be returned for differentiation
|
||||||
|
err = errors.New("response status is inconsistent with the expected status")
|
||||||
|
}
|
||||||
|
|
||||||
t = uint16(time.Since(start) / time.Millisecond)
|
t = uint16(time.Since(start) / time.Millisecond)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||||
return &Proxy{adapter, queue.New[C.DelayHistory](10), atomic.NewBool(true)}
|
return &Proxy{adapter, queue.New[C.DelayHistory](defaultHistoriesNum), atomic.NewBool(true), map[string]*extraProxyState{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/callback"
|
"github.com/Dreamacro/clash/common/callback"
|
||||||
N "github.com/Dreamacro/clash/common/net"
|
N "github.com/Dreamacro/clash/common/net"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
|
@ -19,6 +20,7 @@ type Fallback struct {
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
testUrl string
|
testUrl string
|
||||||
selected string
|
selected string
|
||||||
|
expectedStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fallback) Now() string {
|
func (f *Fallback) Now() string {
|
||||||
|
@ -85,6 +87,8 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||||
"type": f.Type().String(),
|
"type": f.Type().String(),
|
||||||
"now": f.Now(),
|
"now": f.Now(),
|
||||||
"all": all,
|
"all": all,
|
||||||
|
"testUrl": f.testUrl,
|
||||||
|
"expected": f.expectedStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,12 +102,14 @@ func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
||||||
proxies := f.GetProxies(touch)
|
proxies := f.GetProxies(touch)
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if len(f.selected) == 0 {
|
if len(f.selected) == 0 {
|
||||||
if proxy.Alive() {
|
// if proxy.Alive() {
|
||||||
|
if proxy.AliveForTestUrl(f.testUrl) {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if proxy.Name() == f.selected {
|
if proxy.Name() == f.selected {
|
||||||
if proxy.Alive() {
|
// if proxy.Alive() {
|
||||||
|
if proxy.AliveForTestUrl(f.testUrl) {
|
||||||
return proxy
|
return proxy
|
||||||
} else {
|
} else {
|
||||||
f.selected = ""
|
f.selected = ""
|
||||||
|
@ -129,10 +135,12 @@ func (f *Fallback) Set(name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
f.selected = name
|
f.selected = name
|
||||||
if !p.Alive() {
|
// if !p.Alive() {
|
||||||
|
if !p.AliveForTestUrl(f.testUrl) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
_, _ = p.URLTest(ctx, f.testUrl)
|
expectedStatus, _ := utils.NewIntRanges[uint16](f.expectedStatus)
|
||||||
|
_, _ = p.URLTest(ctx, f.testUrl, expectedStatus, C.ExtraHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -158,5 +166,6 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider)
|
||||||
}),
|
}),
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
testUrl: option.URL,
|
testUrl: option.URL,
|
||||||
|
expectedStatus: option.ExpectedStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/common/atomic"
|
"github.com/Dreamacro/clash/common/atomic"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/constant/provider"
|
"github.com/Dreamacro/clash/constant/provider"
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
|
@ -192,7 +193,7 @@ func (gb *GroupBase) GetProxies(touch bool) []C.Proxy {
|
||||||
return proxies
|
return proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16, error) {
|
func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var lock sync.Mutex
|
var lock sync.Mutex
|
||||||
mp := map[string]uint16{}
|
mp := map[string]uint16{}
|
||||||
|
@ -201,7 +202,7 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string) (map[string]uint16
|
||||||
proxy := proxy
|
proxy := proxy
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
delay, err := proxy.URLTest(ctx, url)
|
delay, err := proxy.URLTest(ctx, url, expectedStatus, C.DropHistory)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
lock.Lock()
|
lock.Lock()
|
||||||
mp[proxy.Name()] = delay
|
mp[proxy.Name()] = delay
|
||||||
|
|
|
@ -27,6 +27,8 @@ type LoadBalance struct {
|
||||||
*GroupBase
|
*GroupBase
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
strategyFn strategyFn
|
strategyFn strategyFn
|
||||||
|
testUrl string
|
||||||
|
expectedStatus string
|
||||||
}
|
}
|
||||||
|
|
||||||
var errStrategy = errors.New("unsupported strategy")
|
var errStrategy = errors.New("unsupported strategy")
|
||||||
|
@ -129,7 +131,7 @@ func (lb *LoadBalance) IsL3Protocol(metadata *C.Metadata) bool {
|
||||||
return lb.Unwrap(metadata, false).IsL3Protocol(metadata)
|
return lb.Unwrap(metadata, false).IsL3Protocol(metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
func strategyRoundRobin() strategyFn {
|
func strategyRoundRobin(url string) strategyFn {
|
||||||
idx := 0
|
idx := 0
|
||||||
idxMutex := sync.Mutex{}
|
idxMutex := sync.Mutex{}
|
||||||
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
|
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
|
||||||
|
@ -148,7 +150,8 @@ func strategyRoundRobin() strategyFn {
|
||||||
for ; i < length; i++ {
|
for ; i < length; i++ {
|
||||||
id := (idx + i) % length
|
id := (idx + i) % length
|
||||||
proxy := proxies[id]
|
proxy := proxies[id]
|
||||||
if proxy.Alive() {
|
// if proxy.Alive() {
|
||||||
|
if proxy.AliveForTestUrl(url) {
|
||||||
i++
|
i++
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
|
@ -158,7 +161,7 @@ func strategyRoundRobin() strategyFn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strategyConsistentHashing() strategyFn {
|
func strategyConsistentHashing(url string) strategyFn {
|
||||||
maxRetry := 5
|
maxRetry := 5
|
||||||
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
|
return func(proxies []C.Proxy, metadata *C.Metadata, touch bool) C.Proxy {
|
||||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||||
|
@ -166,14 +169,16 @@ func strategyConsistentHashing() strategyFn {
|
||||||
for i := 0; i < maxRetry; i, key = i+1, key+1 {
|
for i := 0; i < maxRetry; i, key = i+1, key+1 {
|
||||||
idx := jumpHash(key, buckets)
|
idx := jumpHash(key, buckets)
|
||||||
proxy := proxies[idx]
|
proxy := proxies[idx]
|
||||||
if proxy.Alive() {
|
// if proxy.Alive() {
|
||||||
|
if proxy.AliveForTestUrl(url) {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// when availability is poor, traverse the entire list to get the available nodes
|
// when availability is poor, traverse the entire list to get the available nodes
|
||||||
for _, proxy := range proxies {
|
for _, proxy := range proxies {
|
||||||
if proxy.Alive() {
|
// if proxy.Alive() {
|
||||||
|
if proxy.AliveForTestUrl(url) {
|
||||||
return proxy
|
return proxy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -182,7 +187,7 @@ func strategyConsistentHashing() strategyFn {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func strategyStickySessions() strategyFn {
|
func strategyStickySessions(url string) strategyFn {
|
||||||
ttl := time.Minute * 10
|
ttl := time.Minute * 10
|
||||||
maxRetry := 5
|
maxRetry := 5
|
||||||
lruCache := cache.New[uint64, int](
|
lruCache := cache.New[uint64, int](
|
||||||
|
@ -199,7 +204,8 @@ func strategyStickySessions() strategyFn {
|
||||||
nowIdx := idx
|
nowIdx := idx
|
||||||
for i := 1; i < maxRetry; i++ {
|
for i := 1; i < maxRetry; i++ {
|
||||||
proxy := proxies[nowIdx]
|
proxy := proxies[nowIdx]
|
||||||
if proxy.Alive() {
|
// if proxy.Alive() {
|
||||||
|
if proxy.AliveForTestUrl(url) {
|
||||||
if nowIdx != idx {
|
if nowIdx != idx {
|
||||||
lruCache.Delete(key)
|
lruCache.Delete(key)
|
||||||
lruCache.Set(key, nowIdx)
|
lruCache.Set(key, nowIdx)
|
||||||
|
@ -232,6 +238,8 @@ func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"type": lb.Type().String(),
|
"type": lb.Type().String(),
|
||||||
"all": all,
|
"all": all,
|
||||||
|
"testUrl": lb.testUrl,
|
||||||
|
"expectedStatus": lb.expectedStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,11 +247,11 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
|
||||||
var strategyFn strategyFn
|
var strategyFn strategyFn
|
||||||
switch strategy {
|
switch strategy {
|
||||||
case "consistent-hashing":
|
case "consistent-hashing":
|
||||||
strategyFn = strategyConsistentHashing()
|
strategyFn = strategyConsistentHashing(option.URL)
|
||||||
case "round-robin":
|
case "round-robin":
|
||||||
strategyFn = strategyRoundRobin()
|
strategyFn = strategyRoundRobin(option.URL)
|
||||||
case "sticky-sessions":
|
case "sticky-sessions":
|
||||||
strategyFn = strategyStickySessions()
|
strategyFn = strategyStickySessions(option.URL)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||||
}
|
}
|
||||||
|
@ -262,5 +270,7 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide
|
||||||
}),
|
}),
|
||||||
strategyFn: strategyFn,
|
strategyFn: strategyFn,
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
|
testUrl: option.URL,
|
||||||
|
expectedStatus: option.ExpectedStatus,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,19 @@ package outboundgroup
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter/outbound"
|
"github.com/Dreamacro/clash/adapter/outbound"
|
||||||
"github.com/Dreamacro/clash/adapter/provider"
|
"github.com/Dreamacro/clash/adapter/provider"
|
||||||
"github.com/Dreamacro/clash/common/structure"
|
"github.com/Dreamacro/clash/common/structure"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
types "github.com/Dreamacro/clash/constant/provider"
|
types "github.com/Dreamacro/clash/constant/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errFormat = errors.New("format error")
|
errFormat = errors.New("format error")
|
||||||
errType = errors.New("unsupport type")
|
errType = errors.New("unsupported type")
|
||||||
errMissProxy = errors.New("`use` or `proxies` missing")
|
errMissProxy = errors.New("`use` or `proxies` missing")
|
||||||
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
||||||
errDuplicateProvider = errors.New("duplicate provider name")
|
errDuplicateProvider = errors.New("duplicate provider name")
|
||||||
|
@ -32,6 +34,7 @@ type GroupCommonOption struct {
|
||||||
Filter string `group:"filter,omitempty"`
|
Filter string `group:"filter,omitempty"`
|
||||||
ExcludeFilter string `group:"exclude-filter,omitempty"`
|
ExcludeFilter string `group:"exclude-filter,omitempty"`
|
||||||
ExcludeType string `group:"exclude-type,omitempty"`
|
ExcludeType string `group:"exclude-type,omitempty"`
|
||||||
|
ExpectedStatus string `group:"expected-status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
|
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]types.ProxyProvider) (C.ProxyAdapter, error) {
|
||||||
|
@ -56,6 +59,18 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
||||||
return nil, errMissProxy
|
return nil, errMissProxy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedStatus, err := utils.NewIntRanges[uint16](groupOption.ExpectedStatus)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
status := strings.TrimSpace(groupOption.ExpectedStatus)
|
||||||
|
if status == "" {
|
||||||
|
status = "*"
|
||||||
|
}
|
||||||
|
groupOption.ExpectedStatus = status
|
||||||
|
testUrl := groupOption.URL
|
||||||
|
|
||||||
if len(groupOption.Proxies) != 0 {
|
if len(groupOption.Proxies) != 0 {
|
||||||
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -66,17 +81,14 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
||||||
return nil, errDuplicateProvider
|
return nil, errDuplicateProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
// select don't need health check
|
|
||||||
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
|
||||||
hc := provider.NewHealthCheck(ps, "", 0, true)
|
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
providers = append(providers, pd)
|
// select don't need health check
|
||||||
providersMap[groupName] = pd
|
if groupOption.Type != "select" && groupOption.Type != "relay" {
|
||||||
} else {
|
|
||||||
if groupOption.URL == "" {
|
if groupOption.URL == "" {
|
||||||
groupOption.URL = "https://cp.cloudflare.com/generate_204"
|
groupOption.URL = "https://cp.cloudflare.com/generate_204"
|
||||||
}
|
}
|
||||||
|
@ -85,22 +97,22 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
||||||
groupOption.Interval = 300
|
groupOption.Interval = 300
|
||||||
}
|
}
|
||||||
|
|
||||||
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
pd.RegisterHealthCheckTask(groupOption.URL, expectedStatus, "", uint(groupOption.Interval))
|
||||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
providers = append(providers, pd)
|
providers = append(providers, pd)
|
||||||
providersMap[groupName] = pd
|
providersMap[groupName] = pd
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(groupOption.Use) != 0 {
|
if len(groupOption.Use) != 0 {
|
||||||
list, err := getProviders(providersMap, groupOption.Use)
|
list, err := getProviders(providersMap, groupOption.Use)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// different proxy groups use different test URL
|
||||||
|
addTestUrlToProviders(list, testUrl, expectedStatus, groupOption.Filter, uint(groupOption.Interval))
|
||||||
|
|
||||||
providers = append(providers, list...)
|
providers = append(providers, list...)
|
||||||
} else {
|
} else {
|
||||||
groupOption.Filter = ""
|
groupOption.Filter = ""
|
||||||
|
@ -154,3 +166,13 @@ func getProviders(mapping map[string]types.ProxyProvider, list []string) ([]type
|
||||||
}
|
}
|
||||||
return ps, nil
|
return ps, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addTestUrlToProviders(providers []types.ProxyProvider, url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
|
||||||
|
if len(providers) == 0 || len(url) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pd := range providers {
|
||||||
|
pd.RegisterHealthCheckTask(url, expectedStatus, filter, interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ type URLTest struct {
|
||||||
*GroupBase
|
*GroupBase
|
||||||
selected string
|
selected string
|
||||||
testUrl string
|
testUrl string
|
||||||
|
expectedStatus string
|
||||||
tolerance uint16
|
tolerance uint16
|
||||||
disableUDP bool
|
disableUDP bool
|
||||||
fastNode C.Proxy
|
fastNode C.Proxy
|
||||||
|
@ -112,7 +113,8 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
||||||
|
|
||||||
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
|
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
|
||||||
fast := proxies[0]
|
fast := proxies[0]
|
||||||
min := fast.LastDelay()
|
// min := fast.LastDelay()
|
||||||
|
min := fast.LastDelayForTestUrl(u.testUrl)
|
||||||
fastNotExist := true
|
fastNotExist := true
|
||||||
|
|
||||||
for _, proxy := range proxies[1:] {
|
for _, proxy := range proxies[1:] {
|
||||||
|
@ -120,11 +122,13 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
||||||
fastNotExist = false
|
fastNotExist = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !proxy.Alive() {
|
// if !proxy.Alive() {
|
||||||
|
if !proxy.AliveForTestUrl(u.testUrl) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
delay := proxy.LastDelay()
|
// delay := proxy.LastDelay()
|
||||||
|
delay := proxy.LastDelayForTestUrl(u.testUrl)
|
||||||
if delay < min {
|
if delay < min {
|
||||||
fast = proxy
|
fast = proxy
|
||||||
min = delay
|
min = delay
|
||||||
|
@ -132,7 +136,8 @@ func (u *URLTest) fast(touch bool) C.Proxy {
|
||||||
|
|
||||||
}
|
}
|
||||||
// tolerance
|
// tolerance
|
||||||
if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
|
// if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.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
|
u.fastNode = fast
|
||||||
}
|
}
|
||||||
return u.fastNode, nil
|
return u.fastNode, nil
|
||||||
|
@ -167,6 +172,8 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||||
"type": u.Type().String(),
|
"type": u.Type().String(),
|
||||||
"now": u.Now(),
|
"now": u.Now(),
|
||||||
"all": all,
|
"all": all,
|
||||||
|
"testUrl": u.testUrl,
|
||||||
|
"expected": u.expectedStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,6 +208,7 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o
|
||||||
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
|
fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10),
|
||||||
disableUDP: option.DisableUDP,
|
disableUDP: option.DisableUDP,
|
||||||
testUrl: option.URL,
|
testUrl: option.URL,
|
||||||
|
expectedStatus: option.ExpectedStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, option := range options {
|
for _, option := range options {
|
||||||
|
|
|
@ -2,6 +2,8 @@ package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/atomic"
|
"github.com/Dreamacro/clash/common/atomic"
|
||||||
|
@ -10,10 +12,13 @@ import (
|
||||||
"github.com/Dreamacro/clash/common/utils"
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultURLTestTimeout = time.Second * 5
|
defaultURLTestTimeout = time.Second * 5
|
||||||
|
defaultMaxTestUrlNum = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
type HealthCheckOption struct {
|
type HealthCheckOption struct {
|
||||||
|
@ -21,8 +26,16 @@ type HealthCheckOption struct {
|
||||||
Interval uint
|
Interval uint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type extraOption struct {
|
||||||
|
expectedStatus utils.IntRanges[uint16]
|
||||||
|
filters map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
type HealthCheck struct {
|
type HealthCheck struct {
|
||||||
url string
|
url string
|
||||||
|
extra map[string]*extraOption
|
||||||
|
mu sync.Mutex
|
||||||
|
started *atomic.Bool
|
||||||
proxies []C.Proxy
|
proxies []C.Proxy
|
||||||
interval uint
|
interval uint
|
||||||
lazy bool
|
lazy bool
|
||||||
|
@ -32,7 +45,13 @@ type HealthCheck struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) process() {
|
func (hc *HealthCheck) process() {
|
||||||
|
if hc.started.Load() {
|
||||||
|
log.Warnln("Skip start health check timer due to it's started")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||||
|
hc.start()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
@ -44,6 +63,7 @@ func (hc *HealthCheck) process() {
|
||||||
}
|
}
|
||||||
case <-hc.done:
|
case <-hc.done:
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
|
hc.stop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +73,63 @@ func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
||||||
hc.proxies = proxies
|
hc.proxies = proxies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
|
||||||
|
url = strings.TrimSpace(url)
|
||||||
|
if len(url) == 0 || url == hc.url {
|
||||||
|
log.Debugln("ignore invalid health check url: %s", url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hc.mu.Lock()
|
||||||
|
defer hc.mu.Unlock()
|
||||||
|
|
||||||
|
// if the provider has not set up health checks, then modify it to be the same as the group's interval
|
||||||
|
if hc.interval == 0 {
|
||||||
|
hc.interval = interval
|
||||||
|
}
|
||||||
|
|
||||||
|
if hc.extra == nil {
|
||||||
|
hc.extra = make(map[string]*extraOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
// prioritize the use of previously registered configurations, especially those from provider
|
||||||
|
if _, ok := hc.extra[url]; ok {
|
||||||
|
// provider default health check does not set filter
|
||||||
|
if url != hc.url && len(filter) != 0 {
|
||||||
|
splitAndAddFiltersToExtra(filter, hc.extra[url])
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugln("health check url: %s exists", url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// due to the time-consuming nature of health checks, a maximum of defaultMaxTestURLNum URLs can be set for testing
|
||||||
|
if len(hc.extra) > defaultMaxTestUrlNum {
|
||||||
|
log.Debugln("skip add url: %s to health check because it has reached the maximum limit: %d", url, defaultMaxTestUrlNum)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
|
||||||
|
splitAndAddFiltersToExtra(filter, option)
|
||||||
|
hc.extra[url] = option
|
||||||
|
|
||||||
|
if hc.auto() && !hc.started.Load() {
|
||||||
|
go hc.process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitAndAddFiltersToExtra(filter string, option *extraOption) {
|
||||||
|
filter = strings.TrimSpace(filter)
|
||||||
|
if len(filter) != 0 {
|
||||||
|
for _, regex := range strings.Split(filter, "`") {
|
||||||
|
regex = strings.TrimSpace(regex)
|
||||||
|
if len(regex) != 0 {
|
||||||
|
option.filters[regex] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) auto() bool {
|
func (hc *HealthCheck) auto() bool {
|
||||||
return hc.interval != 0
|
return hc.interval != 0
|
||||||
}
|
}
|
||||||
|
@ -61,29 +138,78 @@ func (hc *HealthCheck) touch() {
|
||||||
hc.lastTouch.Store(time.Now().Unix())
|
hc.lastTouch.Store(time.Now().Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) start() {
|
||||||
|
hc.started.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) stop() {
|
||||||
|
hc.started.Store(false)
|
||||||
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) check() {
|
func (hc *HealthCheck) check() {
|
||||||
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
|
_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
|
||||||
id := utils.NewUUIDV4().String()
|
id := utils.NewUUIDV4().String()
|
||||||
log.Debugln("Start New Health Checking {%s}", id)
|
log.Debugln("Start New Health Checking {%s}", id)
|
||||||
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
|
b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
|
||||||
for _, proxy := range hc.proxies {
|
|
||||||
p := proxy
|
|
||||||
b.Go(p.Name(), func() (bool, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
|
||||||
defer cancel()
|
|
||||||
log.Debugln("Health Checking %s {%s}", p.Name(), id)
|
|
||||||
_, _ = p.URLTest(ctx, hc.url)
|
|
||||||
log.Debugln("Health Checked %s : %t %d ms {%s}", p.Name(), p.Alive(), p.LastDelay(), id)
|
|
||||||
return false, nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// execute default health check
|
||||||
|
hc.execute(b, hc.url, id, nil)
|
||||||
|
|
||||||
|
// execute extra health check
|
||||||
|
if len(hc.extra) != 0 {
|
||||||
|
for url, option := range hc.extra {
|
||||||
|
hc.execute(b, url, id, option)
|
||||||
|
}
|
||||||
|
}
|
||||||
b.Wait()
|
b.Wait()
|
||||||
log.Debugln("Finish A Health Checking {%s}", id)
|
log.Debugln("Finish A Health Checking {%s}", id)
|
||||||
return struct{}{}, nil
|
return struct{}{}, nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) {
|
||||||
|
url = strings.TrimSpace(url)
|
||||||
|
if len(url) == 0 {
|
||||||
|
log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filterReg *regexp2.Regexp
|
||||||
|
var store = C.OriginalHistory
|
||||||
|
var expectedStatus utils.IntRanges[uint16]
|
||||||
|
if option != nil {
|
||||||
|
store = C.ExtraHistory
|
||||||
|
expectedStatus = option.expectedStatus
|
||||||
|
if len(option.filters) != 0 {
|
||||||
|
filters := make([]string, 0, len(option.filters))
|
||||||
|
for filter := range option.filters {
|
||||||
|
filters = append(filters, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterReg = regexp2.MustCompile(strings.Join(filters, "|"), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, proxy := range hc.proxies {
|
||||||
|
// skip proxies that do not require health check
|
||||||
|
if filterReg != nil {
|
||||||
|
if match, _ := filterReg.FindStringMatch(proxy.Name()); match == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := proxy
|
||||||
|
b.Go(p.Name(), func() (bool, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||||
|
defer cancel()
|
||||||
|
log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
|
||||||
|
_, _ = p.URLTest(ctx, url, expectedStatus, store)
|
||||||
|
log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid)
|
||||||
|
return false, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (hc *HealthCheck) close() {
|
func (hc *HealthCheck) close() {
|
||||||
hc.done <- struct{}{}
|
hc.done <- struct{}{}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +218,8 @@ func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *He
|
||||||
return &HealthCheck{
|
return &HealthCheck{
|
||||||
proxies: proxies,
|
proxies: proxies,
|
||||||
url: url,
|
url: url,
|
||||||
|
extra: map[string]*extraOption{},
|
||||||
|
started: atomic.NewBool(false),
|
||||||
interval: interval,
|
interval: interval,
|
||||||
lazy: lazy,
|
lazy: lazy,
|
||||||
lastTouch: atomic.NewInt64(0),
|
lastTouch: atomic.NewInt64(0),
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter"
|
"github.com/Dreamacro/clash/adapter"
|
||||||
"github.com/Dreamacro/clash/common/convert"
|
"github.com/Dreamacro/clash/common/convert"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
clashHttp "github.com/Dreamacro/clash/component/http"
|
clashHttp "github.com/Dreamacro/clash/component/http"
|
||||||
"github.com/Dreamacro/clash/component/resource"
|
"github.com/Dreamacro/clash/component/resource"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
@ -50,6 +51,7 @@ func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||||
"type": pp.Type().String(),
|
"type": pp.Type().String(),
|
||||||
"vehicleType": pp.VehicleType().String(),
|
"vehicleType": pp.VehicleType().String(),
|
||||||
"proxies": pp.Proxies(),
|
"proxies": pp.Proxies(),
|
||||||
|
"testUrl": pp.healthCheck.url,
|
||||||
"updatedAt": pp.UpdatedAt,
|
"updatedAt": pp.UpdatedAt,
|
||||||
"subscriptionInfo": pp.subscriptionInfo,
|
"subscriptionInfo": pp.subscriptionInfo,
|
||||||
})
|
})
|
||||||
|
@ -98,6 +100,10 @@ func (pp *proxySetProvider) Touch() {
|
||||||
pp.healthCheck.touch()
|
pp.healthCheck.touch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pp *proxySetProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
|
||||||
|
pp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
|
||||||
|
}
|
||||||
|
|
||||||
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
||||||
pp.proxies = proxies
|
pp.proxies = proxies
|
||||||
pp.healthCheck.setProxy(proxies)
|
pp.healthCheck.setProxy(proxies)
|
||||||
|
@ -210,6 +216,7 @@ func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||||
"type": cp.Type().String(),
|
"type": cp.Type().String(),
|
||||||
"vehicleType": cp.VehicleType().String(),
|
"vehicleType": cp.VehicleType().String(),
|
||||||
"proxies": cp.Proxies(),
|
"proxies": cp.Proxies(),
|
||||||
|
"testUrl": cp.healthCheck.url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,6 +256,10 @@ func (cp *compatibleProvider) Touch() {
|
||||||
cp.healthCheck.touch()
|
cp.healthCheck.touch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cp *compatibleProvider) RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
|
||||||
|
cp.healthCheck.registerHealthCheckTask(url, expectedStatus, filter, interval)
|
||||||
|
}
|
||||||
|
|
||||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||||
pd.healthCheck.close()
|
pd.healthCheck.close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,36 +9,36 @@ type Range[T constraints.Ordered] struct {
|
||||||
end T
|
end T
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRange[T constraints.Ordered](start, end T) *Range[T] {
|
func NewRange[T constraints.Ordered](start, end T) Range[T] {
|
||||||
if start > end {
|
if start > end {
|
||||||
return &Range[T]{
|
return Range[T]{
|
||||||
start: end,
|
start: end,
|
||||||
end: start,
|
end: start,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Range[T]{
|
return Range[T]{
|
||||||
start: start,
|
start: start,
|
||||||
end: end,
|
end: end,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range[T]) Contains(t T) bool {
|
func (r Range[T]) Contains(t T) bool {
|
||||||
return t >= r.start && t <= r.end
|
return t >= r.start && t <= r.end
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range[T]) LeftContains(t T) bool {
|
func (r Range[T]) LeftContains(t T) bool {
|
||||||
return t >= r.start && t < r.end
|
return t >= r.start && t < r.end
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range[T]) RightContains(t T) bool {
|
func (r Range[T]) RightContains(t T) bool {
|
||||||
return t > r.start && t <= r.end
|
return t > r.start && t <= r.end
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range[T]) Start() T {
|
func (r Range[T]) Start() T {
|
||||||
return r.start
|
return r.start
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Range[T]) End() T {
|
func (r Range[T]) End() T {
|
||||||
return r.end
|
return r.end
|
||||||
}
|
}
|
||||||
|
|
77
common/utils/ranges.go
Normal file
77
common/utils/ranges.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/constraints"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IntRanges[T constraints.Integer] []Range[T]
|
||||||
|
|
||||||
|
var errIntRanges = errors.New("intRanges error")
|
||||||
|
|
||||||
|
func NewIntRanges[T constraints.Integer](expected string) (IntRanges[T], error) {
|
||||||
|
// example: 200 or 200/302 or 200-400 or 200/204/401-429/501-503
|
||||||
|
expected = strings.TrimSpace(expected)
|
||||||
|
if len(expected) == 0 || expected == "*" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
list := strings.Split(expected, "/")
|
||||||
|
if len(list) > 28 {
|
||||||
|
return nil, fmt.Errorf("%w, too many ranges to use, maximum support 28 ranges", errIntRanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewIntRangesFromList[T](list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIntRangesFromList[T constraints.Integer](list []string) (IntRanges[T], error) {
|
||||||
|
var ranges IntRanges[T]
|
||||||
|
for _, s := range list {
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status := strings.Split(s, "-")
|
||||||
|
statusLen := len(status)
|
||||||
|
if statusLen > 2 {
|
||||||
|
return nil, errIntRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := strconv.ParseInt(strings.Trim(status[0], "[ ]"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errIntRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
switch statusLen {
|
||||||
|
case 1:
|
||||||
|
ranges = append(ranges, NewRange(T(start), T(start)))
|
||||||
|
case 2:
|
||||||
|
end, err := strconv.ParseUint(strings.Trim(status[1], "[ ]"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errIntRanges
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges = append(ranges, NewRange(T(start), T(end)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ranges IntRanges[T]) Check(status T) bool {
|
||||||
|
if ranges == nil || len(ranges) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, segment := range ranges {
|
||||||
|
if segment.Contains(status) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -10,11 +10,11 @@ import (
|
||||||
|
|
||||||
type SnifferConfig struct {
|
type SnifferConfig struct {
|
||||||
OverrideDest bool
|
OverrideDest bool
|
||||||
Ports []utils.Range[uint16]
|
Ports utils.IntRanges[uint16]
|
||||||
}
|
}
|
||||||
|
|
||||||
type BaseSniffer struct {
|
type BaseSniffer struct {
|
||||||
ports []utils.Range[uint16]
|
ports utils.IntRanges[uint16]
|
||||||
supportNetworkType constant.NetWork
|
supportNetworkType constant.NetWork
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,15 +35,10 @@ func (bs *BaseSniffer) SupportNetwork() constant.NetWork {
|
||||||
|
|
||||||
// SupportPort implements sniffer.Sniffer
|
// SupportPort implements sniffer.Sniffer
|
||||||
func (bs *BaseSniffer) SupportPort(port uint16) bool {
|
func (bs *BaseSniffer) SupportPort(port uint16) bool {
|
||||||
for _, portRange := range bs.ports {
|
return bs.ports.Check(port)
|
||||||
if portRange.Contains(port) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBaseSniffer(ports []utils.Range[uint16], networkType constant.NetWork) *BaseSniffer {
|
func NewBaseSniffer(ports utils.IntRanges[uint16], networkType constant.NetWork) *BaseSniffer {
|
||||||
return &BaseSniffer{
|
return &BaseSniffer{
|
||||||
ports: ports,
|
ports: ports,
|
||||||
supportNetworkType: networkType,
|
supportNetworkType: networkType,
|
||||||
|
|
|
@ -34,11 +34,9 @@ type HTTPSniffer struct {
|
||||||
var _ sniffer.Sniffer = (*HTTPSniffer)(nil)
|
var _ sniffer.Sniffer = (*HTTPSniffer)(nil)
|
||||||
|
|
||||||
func NewHTTPSniffer(snifferConfig SnifferConfig) (*HTTPSniffer, error) {
|
func NewHTTPSniffer(snifferConfig SnifferConfig) (*HTTPSniffer, error) {
|
||||||
ports := make([]utils.Range[uint16], 0)
|
ports := snifferConfig.Ports
|
||||||
if len(snifferConfig.Ports) == 0 {
|
if len(ports) == 0 {
|
||||||
ports = append(ports, *utils.NewRange[uint16](80, 80))
|
ports = utils.IntRanges[uint16]{utils.NewRange[uint16](80, 80)}
|
||||||
} else {
|
|
||||||
ports = append(ports, snifferConfig.Ports...)
|
|
||||||
}
|
}
|
||||||
return &HTTPSniffer{
|
return &HTTPSniffer{
|
||||||
BaseSniffer: NewBaseSniffer(ports, C.TCP),
|
BaseSniffer: NewBaseSniffer(ports, C.TCP),
|
||||||
|
|
|
@ -22,11 +22,9 @@ type TLSSniffer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTLSSniffer(snifferConfig SnifferConfig) (*TLSSniffer, error) {
|
func NewTLSSniffer(snifferConfig SnifferConfig) (*TLSSniffer, error) {
|
||||||
ports := make([]utils.Range[uint16], 0)
|
ports := snifferConfig.Ports
|
||||||
if len(snifferConfig.Ports) == 0 {
|
if len(ports) == 0 {
|
||||||
ports = append(ports, *utils.NewRange[uint16](443, 443))
|
ports = utils.IntRanges[uint16]{utils.NewRange[uint16](443, 443)}
|
||||||
} else {
|
|
||||||
ports = append(ports, snifferConfig.Ports...)
|
|
||||||
}
|
}
|
||||||
return &TLSSniffer{
|
return &TLSSniffer{
|
||||||
BaseSniffer: NewBaseSniffer(ports, C.TCP),
|
BaseSniffer: NewBaseSniffer(ports, C.TCP),
|
||||||
|
|
|
@ -17,7 +17,7 @@ import (
|
||||||
var trustCerts []*x509.Certificate
|
var trustCerts []*x509.Certificate
|
||||||
var certPool *x509.CertPool
|
var certPool *x509.CertPool
|
||||||
var mutex sync.RWMutex
|
var mutex sync.RWMutex
|
||||||
var errNotMacth error = errors.New("certificate fingerprints do not match")
|
var errNotMatch = errors.New("certificate fingerprints do not match")
|
||||||
|
|
||||||
func AddCertificate(certificate string) error {
|
func AddCertificate(certificate string) error {
|
||||||
mutex.Lock()
|
mutex.Lock()
|
||||||
|
@ -79,7 +79,7 @@ func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedCh
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errNotMacth
|
return errNotMatch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -1304,7 +1303,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
|
||||||
if len(snifferRaw.Sniff) != 0 {
|
if len(snifferRaw.Sniff) != 0 {
|
||||||
for sniffType, sniffConfig := range snifferRaw.Sniff {
|
for sniffType, sniffConfig := range snifferRaw.Sniff {
|
||||||
find := false
|
find := false
|
||||||
ports, err := parsePortRange(sniffConfig.Ports)
|
ports, err := utils.NewIntRangesFromList[uint16](sniffConfig.Ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1331,7 +1330,7 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
|
||||||
// Deprecated: Use Sniff instead
|
// Deprecated: Use Sniff instead
|
||||||
log.Warnln("Deprecated: Use Sniff instead")
|
log.Warnln("Deprecated: Use Sniff instead")
|
||||||
}
|
}
|
||||||
globalPorts, err := parsePortRange(snifferRaw.Ports)
|
globalPorts, err := utils.NewIntRangesFromList[uint16](snifferRaw.Ports)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -1376,28 +1375,3 @@ func parseSniffer(snifferRaw RawSniffer) (*Sniffer, error) {
|
||||||
|
|
||||||
return sniffer, nil
|
return sniffer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parsePortRange(portRanges []string) ([]utils.Range[uint16], error) {
|
|
||||||
ports := make([]utils.Range[uint16], 0)
|
|
||||||
for _, portRange := range portRanges {
|
|
||||||
portRaws := strings.Split(portRange, "-")
|
|
||||||
p, err := strconv.ParseUint(portRaws[0], 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s format error", portRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
start := uint16(p)
|
|
||||||
if len(portRaws) > 1 {
|
|
||||||
p, err = strconv.ParseUint(portRaws[1], 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s format error", portRange)
|
|
||||||
}
|
|
||||||
|
|
||||||
end := uint16(p)
|
|
||||||
ports = append(ports, *utils.NewRange(start, end))
|
|
||||||
} else {
|
|
||||||
ports = append(ports, *utils.NewRange(start, start))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ports, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
N "github.com/Dreamacro/clash/common/net"
|
N "github.com/Dreamacro/clash/common/net"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
"github.com/Dreamacro/clash/component/dialer"
|
"github.com/Dreamacro/clash/component/dialer"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -132,7 +133,7 @@ type ProxyAdapter interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Group interface {
|
type Group interface {
|
||||||
URLTest(ctx context.Context, url string) (mp map[string]uint16, err error)
|
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (mp map[string]uint16, err error)
|
||||||
GetProxies(touch bool) []Proxy
|
GetProxies(touch bool) []Proxy
|
||||||
Touch()
|
Touch()
|
||||||
}
|
}
|
||||||
|
@ -142,12 +143,23 @@ type DelayHistory struct {
|
||||||
Delay uint16 `json:"delay"`
|
Delay uint16 `json:"delay"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DelayHistoryStoreType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OriginalHistory DelayHistoryStoreType = iota
|
||||||
|
ExtraHistory
|
||||||
|
DropHistory
|
||||||
|
)
|
||||||
|
|
||||||
type Proxy interface {
|
type Proxy interface {
|
||||||
ProxyAdapter
|
ProxyAdapter
|
||||||
Alive() bool
|
Alive() bool
|
||||||
|
AliveForTestUrl(url string) bool
|
||||||
DelayHistory() []DelayHistory
|
DelayHistory() []DelayHistory
|
||||||
|
ExtraDelayHistory() map[string][]DelayHistory
|
||||||
LastDelay() uint16
|
LastDelay() uint16
|
||||||
URLTest(ctx context.Context, url string) (uint16, error)
|
LastDelayForTestUrl(url string) uint16
|
||||||
|
URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16], store DelayHistoryStoreType) (uint16, error)
|
||||||
|
|
||||||
// Deprecated: use DialContext instead.
|
// Deprecated: use DialContext instead.
|
||||||
Dial(metadata *Metadata) (Conn, error)
|
Dial(metadata *Metadata) (Conn, error)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
"github.com/Dreamacro/clash/constant"
|
"github.com/Dreamacro/clash/constant"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,6 +72,7 @@ type ProxyProvider interface {
|
||||||
Touch()
|
Touch()
|
||||||
HealthCheck()
|
HealthCheck()
|
||||||
Version() uint32
|
Version() uint32
|
||||||
|
RegisterHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuleProvider interface
|
// RuleProvider interface
|
||||||
|
|
|
@ -2,14 +2,16 @@ package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"github.com/Dreamacro/clash/adapter"
|
|
||||||
C "github.com/Dreamacro/clash/constant"
|
|
||||||
"github.com/Dreamacro/clash/tunnel"
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/Dreamacro/clash/adapter"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
"github.com/Dreamacro/clash/tunnel"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GroupRouter() http.Handler {
|
func GroupRouter() http.Handler {
|
||||||
|
@ -64,10 +66,17 @@ func getGroupDelay(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedStatus, err := utils.NewIntRanges[uint16](query.Get("expected"))
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, ErrBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout))
|
ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
dm, err := group.URLTest(ctx, url)
|
dm, err := group.URLTest(ctx, url, expectedStatus)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
render.Status(r, http.StatusGatewayTimeout)
|
render.Status(r, http.StatusGatewayTimeout)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/adapter"
|
"github.com/Dreamacro/clash/adapter"
|
||||||
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
"github.com/Dreamacro/clash/adapter/outboundgroup"
|
||||||
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
"github.com/Dreamacro/clash/component/profile/cachefile"
|
"github.com/Dreamacro/clash/component/profile/cachefile"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/tunnel"
|
"github.com/Dreamacro/clash/tunnel"
|
||||||
|
@ -112,12 +113,19 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedStatus, err := utils.NewIntRanges[uint16](query.Get("expected"))
|
||||||
|
if err != nil {
|
||||||
|
render.Status(r, http.StatusBadRequest)
|
||||||
|
render.JSON(w, r, ErrBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
delay, err := proxy.URLTest(ctx, url)
|
delay, err := proxy.URLTest(ctx, url, expectedStatus, C.DropHistory)
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
render.Status(r, http.StatusGatewayTimeout)
|
render.Status(r, http.StatusGatewayTimeout)
|
||||||
render.JSON(w, r, ErrRequestTimeout)
|
render.JSON(w, r, ErrRequestTimeout)
|
||||||
|
@ -126,7 +134,11 @@ func getProxyDelay(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if err != nil || delay == 0 {
|
if err != nil || delay == 0 {
|
||||||
render.Status(r, http.StatusServiceUnavailable)
|
render.Status(r, http.StatusServiceUnavailable)
|
||||||
|
if err != nil && delay != 0 {
|
||||||
|
render.JSON(w, r, err)
|
||||||
|
} else {
|
||||||
render.JSON(w, r, newError("An error occurred in the delay test"))
|
render.JSON(w, r, newError("An error occurred in the delay test"))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package common
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/utils"
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
|
@ -14,7 +13,7 @@ type Port struct {
|
||||||
adapter string
|
adapter string
|
||||||
port string
|
port string
|
||||||
ruleType C.RuleType
|
ruleType C.RuleType
|
||||||
portList []utils.Range[uint16]
|
portRanges utils.IntRanges[uint16]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Port) RuleType() C.RuleType {
|
func (p *Port) RuleType() C.RuleType {
|
||||||
|
@ -43,52 +42,16 @@ func (p *Port) Payload() string {
|
||||||
func (p *Port) matchPortReal(portRef string) bool {
|
func (p *Port) matchPortReal(portRef string) bool {
|
||||||
port, _ := strconv.Atoi(portRef)
|
port, _ := strconv.Atoi(portRef)
|
||||||
|
|
||||||
for _, pr := range p.portList {
|
return p.portRanges.Check(uint16(port))
|
||||||
if pr.Contains(uint16(port)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) {
|
func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) {
|
||||||
ports := strings.Split(port, "/")
|
portRanges, err := utils.NewIntRanges[uint16](port)
|
||||||
if len(ports) > 28 {
|
|
||||||
return nil, fmt.Errorf("%s, too many ports to use, maximum support 28 ports", errPayload.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var portRange []utils.Range[uint16]
|
|
||||||
for _, p := range ports {
|
|
||||||
if p == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
subPorts := strings.Split(p, "-")
|
|
||||||
subPortsLen := len(subPorts)
|
|
||||||
if subPortsLen > 2 {
|
|
||||||
return nil, errPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
portStart, err := strconv.ParseUint(strings.Trim(subPorts[0], "[ ]"), 10, 16)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errPayload
|
return nil, fmt.Errorf("%w, %s", errPayload, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
switch subPortsLen {
|
if len(portRanges) == 0 {
|
||||||
case 1:
|
|
||||||
portRange = append(portRange, *utils.NewRange(uint16(portStart), uint16(portStart)))
|
|
||||||
case 2:
|
|
||||||
portEnd, err := strconv.ParseUint(strings.Trim(subPorts[1], "[ ]"), 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
portRange = append(portRange, *utils.NewRange(uint16(portStart), uint16(portEnd)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(portRange) == 0 {
|
|
||||||
return nil, errPayload
|
return nil, errPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +60,7 @@ func NewPort(port string, adapter string, ruleType C.RuleType) (*Port, error) {
|
||||||
adapter: adapter,
|
adapter: adapter,
|
||||||
port: port,
|
port: port,
|
||||||
ruleType: ruleType,
|
ruleType: ruleType,
|
||||||
portList: portRange,
|
portRanges: portRanges,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,57 +2,28 @@ package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
"github.com/Dreamacro/clash/common/utils"
|
"github.com/Dreamacro/clash/common/utils"
|
||||||
C "github.com/Dreamacro/clash/constant"
|
C "github.com/Dreamacro/clash/constant"
|
||||||
"github.com/Dreamacro/clash/log"
|
"github.com/Dreamacro/clash/log"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Uid struct {
|
type Uid struct {
|
||||||
*Base
|
*Base
|
||||||
uids []utils.Range[uint32]
|
uids utils.IntRanges[uint32]
|
||||||
oUid string
|
oUid string
|
||||||
adapter string
|
adapter string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUid(oUid, adapter string) (*Uid, error) {
|
func NewUid(oUid, adapter string) (*Uid, error) {
|
||||||
//if len(_uids) > 28 {
|
|
||||||
// return nil, fmt.Errorf("%s, too many uid to use, maximum support 28 uid", errPayload.Error())
|
|
||||||
//}
|
|
||||||
if !(runtime.GOOS == "linux" || runtime.GOOS == "android") {
|
if !(runtime.GOOS == "linux" || runtime.GOOS == "android") {
|
||||||
return nil, fmt.Errorf("uid rule not support this platform")
|
return nil, fmt.Errorf("uid rule not support this platform")
|
||||||
}
|
}
|
||||||
|
|
||||||
var uidRange []utils.Range[uint32]
|
uidRange, err := utils.NewIntRanges[uint32](oUid)
|
||||||
for _, u := range strings.Split(oUid, "/") {
|
|
||||||
if u == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
subUids := strings.Split(u, "-")
|
|
||||||
subUidsLen := len(subUids)
|
|
||||||
if subUidsLen > 2 {
|
|
||||||
return nil, errPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
uidStart, err := strconv.ParseUint(strings.Trim(subUids[0], "[ ]"), 10, 32)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errPayload
|
return nil, fmt.Errorf("%w, %s", errPayload, err.Error())
|
||||||
}
|
|
||||||
|
|
||||||
switch subUidsLen {
|
|
||||||
case 1:
|
|
||||||
uidRange = append(uidRange, *utils.NewRange(uint32(uidStart), uint32(uidStart)))
|
|
||||||
case 2:
|
|
||||||
uidEnd, err := strconv.ParseUint(strings.Trim(subUids[1], "[ ]"), 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errPayload
|
|
||||||
}
|
|
||||||
|
|
||||||
uidRange = append(uidRange, *utils.NewRange(uint32(uidStart), uint32(uidEnd)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(uidRange) == 0 {
|
if len(uidRange) == 0 {
|
||||||
|
@ -72,12 +43,10 @@ func (u *Uid) RuleType() C.RuleType {
|
||||||
|
|
||||||
func (u *Uid) Match(metadata *C.Metadata) (bool, string) {
|
func (u *Uid) Match(metadata *C.Metadata) (bool, string) {
|
||||||
if metadata.Uid != 0 {
|
if metadata.Uid != 0 {
|
||||||
for _, uid := range u.uids {
|
if u.uids.Check(metadata.Uid) {
|
||||||
if uid.Contains(metadata.Uid) {
|
|
||||||
return true, u.adapter
|
return true, u.adapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
log.Warnln("[UID] could not get uid from %s", metadata.String())
|
log.Warnln("[UID] could not get uid from %s", metadata.String())
|
||||||
return false, ""
|
return false, ""
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue