mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
ccm,ocm: merge fallback into balancer strategy, use hyphenated constant names
Merge the fallback credential type into balancer as a strategy (C.BalancerStrategyFallback). Replace raw string literals with C.BalancerStrategyXxx constants and switch to hyphens (least-used, round-robin) per project convention.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user