Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 40 additions & 12 deletions adapter/outboundgroup/fallback.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"sync"
"time"

"github.com/metacubex/mihomo/common/callback"
Expand All @@ -15,6 +16,7 @@ import (

type Fallback struct {
*GroupBase
stateMux sync.RWMutex
disableUDP bool
testUrl string
selected string
Expand Down Expand Up @@ -89,7 +91,7 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
"all": all,
"testUrl": f.testUrl,
"expectedStatus": f.expectedStatus,
"fixed": f.selected,
"fixed": f.getSelected(),
"hidden": f.Hidden,
"icon": f.Icon,
})
Expand All @@ -103,19 +105,24 @@ func (f *Fallback) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {

func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.GetProxies(touch)
for _, proxy := range proxies {
if len(f.selected) == 0 {
selected := f.getSelected()

if len(selected) != 0 {
for _, proxy := range proxies {
if proxy.Name() != selected {
continue
}
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
}
} else {
if proxy.Name() == f.selected {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
} else {
f.selected = ""
}
}
f.clearSelectedIf(selected)
break
}
}

for _, proxy := range proxies {
if proxy.AliveForTestUrl(f.testUrl) {
return proxy
}
}

Expand All @@ -135,7 +142,7 @@ func (f *Fallback) Set(name string) error {
return errors.New("proxy not exist")
}

f.selected = name
f.setSelected(name)
if !p.AliveForTestUrl(f.testUrl) {
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(5000))
defer cancel()
Expand All @@ -147,7 +154,28 @@ func (f *Fallback) Set(name string) error {
}

func (f *Fallback) ForceSet(name string) {
f.setSelected(name)
}

func (f *Fallback) getSelected() string {
f.stateMux.RLock()
defer f.stateMux.RUnlock()

return f.selected
}

func (f *Fallback) setSelected(name string) {
f.stateMux.Lock()
f.selected = name
f.stateMux.Unlock()
}

func (f *Fallback) clearSelectedIf(selected string) {
f.stateMux.Lock()
if f.selected == selected {
f.selected = ""
}
f.stateMux.Unlock()
}

func (f *Fallback) Providers() []P.ProxyProvider {
Expand Down
2 changes: 1 addition & 1 deletion adapter/outboundgroup/groupbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func (gb *GroupBase) URLTest(ctx context.Context, url string, expectedStatus uti
wg.Add(1)
go func() {
delay, err := proxy.URLTest(ctx, url, expectedStatus)
if err == nil {
if err == nil && proxy.AliveForTestUrl(url) {
lock.Lock()
mp[proxy.Name()] = delay
lock.Unlock()
Expand Down
75 changes: 58 additions & 17 deletions adapter/outboundgroup/urltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"errors"
"sync"
"time"

"github.com/metacubex/mihomo/common/callback"
Expand All @@ -24,6 +25,7 @@ func urlTestWithTolerance(tolerance uint16) urlTestOption {

type URLTest struct {
*GroupBase
stateMux sync.RWMutex
selected string
testUrl string
expectedStatus string
Expand Down Expand Up @@ -55,7 +57,9 @@ func (u *URLTest) Set(name string) error {
}

func (u *URLTest) ForceSet(name string) {
u.stateMux.Lock()
u.selected = name
u.stateMux.Unlock()
u.fastSingle.Reset()
}

Expand Down Expand Up @@ -109,24 +113,29 @@ func (u *URLTest) healthCheck() {
func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, shared := u.fastSingle.Do(func() (C.Proxy, error) {
proxies := u.GetProxies(touch)
if u.selected != "" {
selected, fastNode := u.snapshotState()

if selected != "" {
for _, proxy := range proxies {
if !proxy.AliveForTestUrl(u.testUrl) {
continue
}
if proxy.Name() == u.selected {
u.fastNode = proxy
if proxy.Name() == selected {
u.setFastNode(proxy)
return proxy, nil
}
}
}

fast := proxies[0]
minDelay := fast.LastDelayForTestUrl(u.testUrl)
fastNotExist := true
var (
fast C.Proxy
fastDelay uint16
hasAliveFast bool
fastNotExist = true
)

for _, proxy := range proxies[1:] {
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
for _, proxy := range proxies {
if fastNode != nil && proxy.Name() == fastNode.Name() {
fastNotExist = false
}

Expand All @@ -135,17 +144,25 @@ func (u *URLTest) fast(touch bool) C.Proxy {
}

delay := proxy.LastDelayForTestUrl(u.testUrl)
if delay < minDelay {
if !hasAliveFast || delay < fastDelay {
fast = proxy
minDelay = delay
fastDelay = delay
hasAliveFast = true
}

}
// 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

// Do not fall back to timeout nodes when at least one alive node exists.
if hasAliveFast {
// tolerance
if fastNode == nil || fastNotExist || !fastNode.AliveForTestUrl(u.testUrl) || fastNode.LastDelayForTestUrl(u.testUrl) > fastDelay+u.tolerance {
fastNode = fast
}
} else if fastNode == nil || fastNotExist || !fastNode.AliveForTestUrl(u.testUrl) {
fastNode = proxies[0]
}
return u.fastNode, nil

u.setFastNode(fastNode)
return fastNode, nil
})
if shared && touch { // a shared fastSingle.Do() may cause providers untouched, so we touch them again
u.Touch()
Expand All @@ -154,6 +171,26 @@ func (u *URLTest) fast(touch bool) C.Proxy {
return elm
}

func (u *URLTest) snapshotState() (string, C.Proxy) {
u.stateMux.RLock()
defer u.stateMux.RUnlock()

return u.selected, u.fastNode
}

func (u *URLTest) getSelected() string {
u.stateMux.RLock()
defer u.stateMux.RUnlock()

return u.selected
}

func (u *URLTest) setFastNode(proxy C.Proxy) {
u.stateMux.Lock()
u.fastNode = proxy
u.stateMux.Unlock()
}

// SupportUDP implements C.ProxyAdapter
func (u *URLTest) SupportUDP() bool {
if u.disableUDP {
Expand All @@ -179,7 +216,7 @@ func (u *URLTest) MarshalJSON() ([]byte, error) {
"all": all,
"testUrl": u.testUrl,
"expectedStatus": u.expectedStatus,
"fixed": u.selected,
"fixed": u.getSelected(),
"hidden": u.Hidden,
"icon": u.Icon,
})
Expand All @@ -194,7 +231,11 @@ func (u *URLTest) Proxies() []C.Proxy {
}

func (u *URLTest) URLTest(ctx context.Context, url string, expectedStatus utils.IntRanges[uint16]) (map[string]uint16, error) {
return u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus)
delays, err := u.GroupBase.URLTest(ctx, u.testUrl, expectedStatus)
// URL tests update alive/delay history; reset cache so next routing picks fresh best node.
u.fastSingle.Reset()
_ = u.fast(false)
return delays, err
}

func parseURLTestOption(config map[string]any) []urlTestOption {
Expand Down