diff --git a/constant/proxy.go b/constant/proxy.go index 278a46c2f..d46fc0f92 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -38,6 +38,13 @@ const ( TypeURLTest = "urltest" ) +const ( + BalancerStrategyLeastUsed = "least-used" + BalancerStrategyRoundRobin = "round-robin" + BalancerStrategyRandom = "random" + BalancerStrategyFallback = "fallback" +) + func ProxyDisplayName(proxyType string) string { switch proxyType { case TypeTun: diff --git a/option/ccm.go b/option/ccm.go index 6d9d14423..7a4f0709f 100644 --- a/option/ccm.go +++ b/option/ccm.go @@ -32,7 +32,6 @@ type _CCMCredential struct { DefaultOptions CCMDefaultCredentialOptions `json:"-"` ExternalOptions CCMExternalCredentialOptions `json:"-"` BalancerOptions CCMBalancerCredentialOptions `json:"-"` - FallbackOptions CCMFallbackCredentialOptions `json:"-"` } type CCMCredential _CCMCredential @@ -47,8 +46,6 @@ func (c CCMCredential) MarshalJSON() ([]byte, error) { v = c.ExternalOptions case "balancer": v = c.BalancerOptions - case "fallback": - v = c.FallbackOptions default: return nil, E.New("unknown credential type: ", c.Type) } @@ -72,8 +69,6 @@ func (c *CCMCredential) UnmarshalJSON(bytes []byte) error { v = &c.ExternalOptions case "balancer": v = &c.BalancerOptions - case "fallback": - v = &c.FallbackOptions default: return E.New("unknown credential type: ", c.Type) } @@ -106,8 +101,3 @@ type CCMExternalCredentialOptions struct { UsagesPath string `json:"usages_path,omitempty"` PollInterval badoption.Duration `json:"poll_interval,omitempty"` } - -type CCMFallbackCredentialOptions struct { - Credentials badoption.Listable[string] `json:"credentials"` - PollInterval badoption.Duration `json:"poll_interval,omitempty"` -} diff --git a/option/ocm.go b/option/ocm.go index 6015b506c..af937560b 100644 --- a/option/ocm.go +++ b/option/ocm.go @@ -32,7 +32,6 @@ type _OCMCredential struct { DefaultOptions OCMDefaultCredentialOptions `json:"-"` ExternalOptions OCMExternalCredentialOptions `json:"-"` BalancerOptions OCMBalancerCredentialOptions `json:"-"` - FallbackOptions OCMFallbackCredentialOptions `json:"-"` } type OCMCredential _OCMCredential @@ -47,8 +46,6 @@ func (c OCMCredential) MarshalJSON() ([]byte, error) { v = c.ExternalOptions case "balancer": v = c.BalancerOptions - case "fallback": - v = c.FallbackOptions default: return nil, E.New("unknown credential type: ", c.Type) } @@ -72,8 +69,6 @@ func (c *OCMCredential) UnmarshalJSON(bytes []byte) error { v = &c.ExternalOptions case "balancer": v = &c.BalancerOptions - case "fallback": - v = &c.FallbackOptions default: return E.New("unknown credential type: ", c.Type) } @@ -106,8 +101,3 @@ type OCMExternalCredentialOptions struct { UsagesPath string `json:"usages_path,omitempty"` PollInterval badoption.Duration `json:"poll_interval,omitempty"` } - -type OCMFallbackCredentialOptions struct { - Credentials badoption.Listable[string] `json:"credentials"` - PollInterval badoption.Duration `json:"poll_interval,omitempty"` -} diff --git a/service/ccm/credential_external.go b/service/ccm/credential_external.go index 97da6bd9e..b7e04bad3 100644 --- a/service/ccm/credential_external.go +++ b/service/ccm/credential_external.go @@ -47,9 +47,9 @@ type externalCredential struct { requestAccess sync.Mutex // Reverse proxy fields - reverse bool - reverseHttpClient *http.Client - reverseSession *yamux.Session + reverse bool + reverseHttpClient *http.Client + reverseSession *yamux.Session reverseAccess sync.RWMutex closed bool reverseContext context.Context diff --git a/service/ccm/credential_state.go b/service/ccm/credential_state.go index 81d559f3c..b07529eb0 100644 --- a/service/ccm/credential_state.go +++ b/service/ccm/credential_state.go @@ -19,6 +19,7 @@ import ( "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -1015,6 +1016,14 @@ func newBalancerProvider(credentials []credential, strategy string, pollInterval } func (p *balancerProvider) selectCredential(sessionID string, selection credentialSelection) (credential, bool, error) { + if p.strategy == C.BalancerStrategyFallback { + best := p.pickCredential(selection.filter) + if best == nil { + return nil, false, allCredentialsUnavailableError(p.credentials) + } + return best, false, nil + } + selectionScope := selection.scopeOrDefault() if sessionID != "" { p.sessionMutex.RLock() @@ -1024,7 +1033,7 @@ func (p *balancerProvider) selectCredential(sessionID string, selection credenti if entry.selectionScope == selectionScope { for _, cred := range p.credentials { if cred.tagName() == entry.tag && selection.allows(cred) && cred.isUsable() { - if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == "least_used") { + if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == C.BalancerStrategyLeastUsed) { better := p.pickLeastUsed(selection.filter) if better != nil && better.tagName() != cred.tagName() { effectiveThreshold := p.rebalanceThreshold / cred.planWeight() @@ -1086,6 +1095,9 @@ func (p *balancerProvider) rebalanceCredential(tag string, selectionScope creden } func (p *balancerProvider) linkProviderInterrupt(cred credential, selection credentialSelection, onInterrupt func()) func() bool { + if p.strategy == C.BalancerStrategyFallback { + return func() bool { return false } + } key := credentialInterruptKey{ tag: cred.tagName(), selectionScope: selection.scopeOrDefault(), @@ -1103,6 +1115,9 @@ func (p *balancerProvider) linkProviderInterrupt(cred credential, selection cred func (p *balancerProvider) onRateLimited(sessionID string, cred credential, resetAt time.Time, selection credentialSelection) credential { cred.markRateLimited(resetAt) + if p.strategy == C.BalancerStrategyFallback { + return p.pickCredential(selection.filter) + } if sessionID != "" { p.sessionMutex.Lock() delete(p.sessions, sessionID) @@ -1124,15 +1139,29 @@ func (p *balancerProvider) onRateLimited(sessionID string, cred credential, rese func (p *balancerProvider) pickCredential(filter func(credential) bool) credential { switch p.strategy { - case "round_robin": + case C.BalancerStrategyRoundRobin: return p.pickRoundRobin(filter) - case "random": + case C.BalancerStrategyRandom: return p.pickRandom(filter) + case C.BalancerStrategyFallback: + return p.pickFallback(filter) default: return p.pickLeastUsed(filter) } } +func (p *balancerProvider) pickFallback(filter func(credential) bool) credential { + for _, cred := range p.credentials { + if filter != nil && !filter(cred) { + continue + } + if cred.isUsable() { + return cred + } + } + return nil +} + func (p *balancerProvider) pickLeastUsed(filter func(credential) bool) credential { var best credential bestScore := float64(-1) @@ -1239,69 +1268,6 @@ func (p *balancerProvider) allCredentials() []credential { func (p *balancerProvider) close() {} -// fallbackProvider tries credentials in order. -type fallbackProvider struct { - credentials []credential - pollInterval time.Duration - logger log.ContextLogger -} - -func newFallbackProvider(credentials []credential, pollInterval time.Duration, logger log.ContextLogger) *fallbackProvider { - if pollInterval <= 0 { - pollInterval = defaultPollInterval - } - return &fallbackProvider{ - credentials: credentials, - pollInterval: pollInterval, - logger: logger, - } -} - -func (p *fallbackProvider) selectCredential(_ string, selection credentialSelection) (credential, bool, error) { - for _, cred := range p.credentials { - if !selection.allows(cred) { - continue - } - if cred.isUsable() { - return cred, false, nil - } - } - return nil, false, allCredentialsUnavailableError(p.credentials) -} - -func (p *fallbackProvider) onRateLimited(_ string, cred credential, resetAt time.Time, selection credentialSelection) credential { - cred.markRateLimited(resetAt) - for _, candidate := range p.credentials { - if !selection.allows(candidate) { - continue - } - if candidate.isUsable() { - return candidate - } - } - return nil -} - -func (p *fallbackProvider) pollIfStale(ctx context.Context) { - for _, cred := range p.credentials { - if time.Since(cred.lastUpdatedTime()) > cred.pollBackoff(p.pollInterval) { - cred.pollUsage(ctx) - } - } -} - -func (p *fallbackProvider) allCredentials() []credential { - return p.credentials -} - -func (p *fallbackProvider) linkProviderInterrupt(_ credential, _ credentialSelection, _ func()) func() bool { - return func() bool { - return false - } -} - -func (p *fallbackProvider) close() {} - func allCredentialsUnavailableError(credentials []credential) error { var hasUnavailable bool var earliest time.Time @@ -1373,21 +1339,14 @@ func buildCredentialProviders( } } - // Pass 2: create balancer and fallback providers + // Pass 2: create balancer providers for _, credOpt := range options.Credentials { - switch credOpt.Type { - case "balancer": + if credOpt.Type == "balancer" { subCredentials, err := resolveCredentialTags(credOpt.BalancerOptions.Credentials, allCredentialMap, credOpt.Tag) if err != nil { return nil, nil, err } providers[credOpt.Tag] = newBalancerProvider(subCredentials, credOpt.BalancerOptions.Strategy, time.Duration(credOpt.BalancerOptions.PollInterval), credOpt.BalancerOptions.RebalanceThreshold, logger) - case "fallback": - subCredentials, err := resolveCredentialTags(credOpt.FallbackOptions.Credentials, allCredentialMap, credOpt.Tag) - if err != nil { - return nil, nil, err - } - providers[credOpt.Tag] = newFallbackProvider(subCredentials, time.Duration(credOpt.FallbackOptions.PollInterval), logger) } } @@ -1476,7 +1435,7 @@ func validateCCMOptions(options option.CCMServiceOptions) error { } if cred.Type == "balancer" { switch cred.BalancerOptions.Strategy { - case "", "least_used", "round_robin", "random": + case "", C.BalancerStrategyLeastUsed, C.BalancerStrategyRoundRobin, C.BalancerStrategyRandom, C.BalancerStrategyFallback: default: return E.New("credential ", cred.Tag, ": unknown balancer strategy: ", cred.BalancerOptions.Strategy) } diff --git a/service/ocm/credential_external.go b/service/ocm/credential_external.go index fbe8f11c6..0e0556be7 100644 --- a/service/ocm/credential_external.go +++ b/service/ocm/credential_external.go @@ -49,10 +49,10 @@ type externalCredential struct { requestAccess sync.Mutex // Reverse proxy fields - reverse bool - reverseHttpClient *http.Client - reverseCredDialer N.Dialer - reverseSession *yamux.Session + reverse bool + reverseHttpClient *http.Client + reverseCredDialer N.Dialer + reverseSession *yamux.Session reverseAccess sync.RWMutex closed bool reverseContext context.Context diff --git a/service/ocm/credential_state.go b/service/ocm/credential_state.go index 58b734465..181132f09 100644 --- a/service/ocm/credential_state.go +++ b/service/ocm/credential_state.go @@ -18,6 +18,7 @@ import ( "github.com/sagernet/fswatch" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" @@ -1014,6 +1015,14 @@ func newBalancerProvider(credentials []credential, strategy string, pollInterval } func (p *balancerProvider) selectCredential(sessionID string, selection credentialSelection) (credential, bool, error) { + if p.strategy == C.BalancerStrategyFallback { + best := p.pickCredential(selection.filter) + if best == nil { + return nil, false, allRateLimitedError(p.credentials) + } + return best, false, nil + } + selectionScope := selection.scopeOrDefault() if sessionID != "" { p.sessionMutex.RLock() @@ -1023,7 +1032,7 @@ func (p *balancerProvider) selectCredential(sessionID string, selection credenti if entry.selectionScope == selectionScope { for _, cred := range p.credentials { if cred.tagName() == entry.tag && compositeCredentialSelectable(cred) && selection.allows(cred) && cred.isUsable() { - if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == "least_used") { + if p.rebalanceThreshold > 0 && (p.strategy == "" || p.strategy == C.BalancerStrategyLeastUsed) { better := p.pickLeastUsed(selection.filter) if better != nil && better.tagName() != cred.tagName() { effectiveThreshold := p.rebalanceThreshold / cred.planWeight() @@ -1085,6 +1094,9 @@ func (p *balancerProvider) rebalanceCredential(tag string, selectionScope creden } func (p *balancerProvider) linkProviderInterrupt(cred credential, selection credentialSelection, onInterrupt func()) func() bool { + if p.strategy == C.BalancerStrategyFallback { + return func() bool { return false } + } key := credentialInterruptKey{ tag: cred.tagName(), selectionScope: selection.scopeOrDefault(), @@ -1102,6 +1114,9 @@ func (p *balancerProvider) linkProviderInterrupt(cred credential, selection cred func (p *balancerProvider) onRateLimited(sessionID string, cred credential, resetAt time.Time, selection credentialSelection) credential { cred.markRateLimited(resetAt) + if p.strategy == C.BalancerStrategyFallback { + return p.pickCredential(selection.filter) + } if sessionID != "" { p.sessionMutex.Lock() delete(p.sessions, sessionID) @@ -1123,15 +1138,32 @@ func (p *balancerProvider) onRateLimited(sessionID string, cred credential, rese func (p *balancerProvider) pickCredential(filter func(credential) bool) credential { switch p.strategy { - case "round_robin": + case C.BalancerStrategyRoundRobin: return p.pickRoundRobin(filter) - case "random": + case C.BalancerStrategyRandom: return p.pickRandom(filter) + case C.BalancerStrategyFallback: + return p.pickFallback(filter) default: return p.pickLeastUsed(filter) } } +func (p *balancerProvider) pickFallback(filter func(credential) bool) credential { + for _, cred := range p.credentials { + if filter != nil && !filter(cred) { + continue + } + if !compositeCredentialSelectable(cred) { + continue + } + if cred.isUsable() { + return cred + } + } + return nil +} + func (p *balancerProvider) pickLeastUsed(filter func(credential) bool) credential { var best credential bestScore := float64(-1) @@ -1237,74 +1269,6 @@ func (p *balancerProvider) allCredentials() []credential { func (p *balancerProvider) close() {} -type fallbackProvider struct { - credentials []credential - pollInterval time.Duration - logger log.ContextLogger -} - -func newFallbackProvider(credentials []credential, pollInterval time.Duration, logger log.ContextLogger) *fallbackProvider { - if pollInterval <= 0 { - pollInterval = defaultPollInterval - } - return &fallbackProvider{ - credentials: credentials, - pollInterval: pollInterval, - logger: logger, - } -} - -func (p *fallbackProvider) selectCredential(_ string, selection credentialSelection) (credential, bool, error) { - for _, cred := range p.credentials { - if !selection.allows(cred) { - continue - } - if !compositeCredentialSelectable(cred) { - continue - } - if cred.isUsable() { - return cred, false, nil - } - } - return nil, false, allRateLimitedError(p.credentials) -} - -func (p *fallbackProvider) onRateLimited(_ string, cred credential, resetAt time.Time, selection credentialSelection) credential { - cred.markRateLimited(resetAt) - for _, candidate := range p.credentials { - if !selection.allows(candidate) { - continue - } - if !compositeCredentialSelectable(candidate) { - continue - } - if candidate.isUsable() { - return candidate - } - } - return nil -} - -func (p *fallbackProvider) pollIfStale(ctx context.Context) { - for _, cred := range p.credentials { - if time.Since(cred.lastUpdatedTime()) > cred.pollBackoff(p.pollInterval) { - cred.pollUsage(ctx) - } - } -} - -func (p *fallbackProvider) allCredentials() []credential { - return p.credentials -} - -func (p *fallbackProvider) linkProviderInterrupt(_ credential, _ credentialSelection, _ func()) func() bool { - return func() bool { - return false - } -} - -func (p *fallbackProvider) close() {} - func allRateLimitedError(credentials []credential) error { var hasUnavailable bool var earliest time.Time @@ -1358,21 +1322,14 @@ func buildOCMCredentialProviders( } } - // Pass 2: create balancer and fallback providers + // Pass 2: create balancer providers for _, credOpt := range options.Credentials { - switch credOpt.Type { - case "balancer": + if credOpt.Type == "balancer" { subCredentials, err := resolveCredentialTags(credOpt.BalancerOptions.Credentials, allCredentialMap, credOpt.Tag) if err != nil { return nil, nil, err } providers[credOpt.Tag] = newBalancerProvider(subCredentials, credOpt.BalancerOptions.Strategy, time.Duration(credOpt.BalancerOptions.PollInterval), credOpt.BalancerOptions.RebalanceThreshold, logger) - case "fallback": - subCredentials, err := resolveCredentialTags(credOpt.FallbackOptions.Credentials, allCredentialMap, credOpt.Tag) - if err != nil { - return nil, nil, err - } - providers[credOpt.Tag] = newFallbackProvider(subCredentials, time.Duration(credOpt.FallbackOptions.PollInterval), logger) } } @@ -1469,7 +1426,7 @@ func validateOCMOptions(options option.OCMServiceOptions) error { } if cred.Type == "balancer" { switch cred.BalancerOptions.Strategy { - case "", "least_used", "round_robin", "random": + case "", C.BalancerStrategyLeastUsed, C.BalancerStrategyRoundRobin, C.BalancerStrategyRandom, C.BalancerStrategyFallback: default: return E.New("credential ", cred.Tag, ": unknown balancer strategy: ", cred.BalancerOptions.Strategy) } @@ -1505,7 +1462,7 @@ func validateOCMCompositeCredentialModes( providers map[string]credentialProvider, ) error { for _, credOpt := range options.Credentials { - if credOpt.Type != "balancer" && credOpt.Type != "fallback" { + if credOpt.Type != "balancer" { continue }