Router: Refactor to fix API data race (#6678)

Fixes https://github.com/XTLS/Xray-core/pull/6673

---------

Co-authored-by: Kosta <makostadev@xyecoc.com>
This commit is contained in:
风扇滑翔翼
2026-08-25 18:00:14 +00:00
committed by GitHub
co-authored by Kosta
parent 9b373e39ca
commit 598bde7412
4 changed files with 79 additions and 157 deletions
+3 -3
View File
@@ -136,7 +136,7 @@ func (b *Balancer) SelectOutbounds() ([]string, error) {
// GetPrincipleTarget implements routing.BalancerPrincipleTarget // GetPrincipleTarget implements routing.BalancerPrincipleTarget
func (r *Router) GetPrincipleTarget(tag string) ([]string, error) { func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
if b, ok := r.balancers[tag]; ok { if b, ok := (*r.balancers.Load())[tag]; ok {
if s, ok := b.strategy.(BalancingPrincipleTarget); ok { if s, ok := b.strategy.(BalancingPrincipleTarget); ok {
candidates, err := b.SelectOutbounds() candidates, err := b.SelectOutbounds()
if err != nil { if err != nil {
@@ -151,7 +151,7 @@ func (r *Router) GetPrincipleTarget(tag string) ([]string, error) {
// SetOverrideTarget implements routing.BalancerOverrider // SetOverrideTarget implements routing.BalancerOverrider
func (r *Router) SetOverrideTarget(tag, target string) error { func (r *Router) SetOverrideTarget(tag, target string) error {
if b, ok := r.balancers[tag]; ok { if b, ok := (*r.balancers.Load())[tag]; ok {
b.override.Put(target) b.override.Put(target)
return nil return nil
} }
@@ -160,7 +160,7 @@ func (r *Router) SetOverrideTarget(tag, target string) error {
// GetOverrideTarget implements routing.BalancerOverrider // GetOverrideTarget implements routing.BalancerOverrider
func (r *Router) GetOverrideTarget(tag string) (string, error) { func (r *Router) GetOverrideTarget(tag string) (string, error) {
if b, ok := r.balancers[tag]; ok { if b, ok := (*r.balancers.Load())[tag]; ok {
return b.override.Get(), nil return b.override.Get(), nil
} }
return "", errors.New("cannot find tag") return "", errors.New("cannot find tag")
-17
View File
@@ -2,25 +2,8 @@ package router
import ( import (
sync "sync" sync "sync"
"github.com/xtls/xray-core/common/errors"
) )
func (r *Router) OverrideBalancer(balancer string, target string) error {
var b *Balancer
for tag, bl := range r.balancers {
if tag == balancer {
b = bl
break
}
}
if b == nil {
return errors.New("balancer '", balancer, "' not found")
}
b.override.Put(target)
return nil
}
type overrideSettings struct { type overrideSettings struct {
target string target string
} }
+59 -114
View File
@@ -2,7 +2,9 @@ package router
import ( import (
"context" "context"
"maps"
"sync" "sync"
"sync/atomic"
"github.com/xtls/xray-core/common" "github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -17,8 +19,8 @@ import (
// Router is an implementation of routing.Router. // Router is an implementation of routing.Router.
type Router struct { type Router struct {
domainStrategy Config_DomainStrategy domainStrategy Config_DomainStrategy
rules []*Rule rules atomic.Pointer[[]*Rule]
balancers map[string]*Balancer balancers atomic.Pointer[map[string]*Balancer]
dns dns.Client dns dns.Client
ctx context.Context ctx context.Context
@@ -43,52 +45,9 @@ func (r *Router) Init(ctx context.Context, config *Config, d dns.Client, ohm out
r.ohm = ohm r.ohm = ohm
r.dispatcher = dispatcher r.dispatcher = dispatcher
r.balancers = make(map[string]*Balancer, len(config.BalancingRule)) r.rules.Store(new([]*Rule))
for _, rule := range config.BalancingRule { r.balancers.Store(&map[string]*Balancer{})
balancer, err := rule.Build(ohm, dispatcher) return r.ReloadRules(config, false)
if err != nil {
return err
}
balancer.InjectContext(ctx)
r.balancers[rule.Tag] = balancer
}
r.rules = make([]*Rule, 0, len(config.Rule))
for _, rule := range config.Rule {
cond, err := rule.BuildCondition()
if err != nil {
r.closeWebhooks()
return err
}
rr := &Rule{
Condition: cond,
Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(),
}
if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh)
if err != nil {
r.closeWebhooks()
return err
}
rr.Webhook = notifier
}
btag := rule.GetBalancingTag()
if len(btag) > 0 {
brule, found := r.balancers[btag]
if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
r.closeWebhooks()
return errors.New("balancer ", btag, " not found")
}
rr.Balancer = brule
}
r.rules = append(r.rules, rr)
}
return nil
} }
// PickRoute implements routing.Router. // PickRoute implements routing.Router.
@@ -124,18 +83,22 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
if !shouldAppend { oldRules := *r.rules.Load()
for _, rule := range r.rules { oldBalancers := *r.balancers.Load()
if rule.Webhook != nil {
rule.Webhook.Close() var newRules []*Rule
} newBalancers := make(map[string]*Balancer)
existTags := make(map[string]bool, len(oldRules)+len(config.Rule))
if shouldAppend {
newRules = append(newRules, oldRules...)
maps.Copy(newBalancers, oldBalancers)
for _, rule := range oldRules {
existTags[rule.RuleTag] = true
} }
r.balancers = make(map[string]*Balancer, len(config.BalancingRule))
r.rules = make([]*Rule, 0, len(config.Rule))
} }
for _, rule := range config.BalancingRule { for _, rule := range config.BalancingRule {
_, found := r.balancers[rule.Tag] if _, found := newBalancers[rule.Tag]; found {
if found {
return errors.New("duplicate balancer tag") return errors.New("duplicate balancer tag")
} }
balancer, err := rule.Build(r.ohm, r.dispatcher) balancer, err := rule.Build(r.ohm, r.dispatcher)
@@ -143,27 +106,12 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
return err return err
} }
balancer.InjectContext(r.ctx) balancer.InjectContext(r.ctx)
r.balancers[rule.Tag] = balancer newBalancers[rule.Tag] = balancer
}
startIdx := len(r.rules)
closeNewWebhooks := func() {
for i := startIdx; i < len(r.rules); i++ {
if r.rules[i].Webhook != nil {
r.rules[i].Webhook.Close()
}
}
r.rules = r.rules[:startIdx]
} }
for _, rule := range config.Rule { for _, rule := range config.Rule {
if r.RuleExists(rule.GetRuleTag()) {
closeNewWebhooks()
return errors.New("duplicate ruleTag ", rule.GetRuleTag())
}
cond, err := rule.BuildCondition() cond, err := rule.BuildCondition()
if err != nil { if err != nil {
closeNewWebhooks()
return err return err
} }
rr := &Rule{ rr := &Rule{
@@ -171,69 +119,64 @@ func (r *Router) ReloadRules(config *Config, shouldAppend bool) error {
Tag: rule.GetTag(), Tag: rule.GetTag(),
RuleTag: rule.GetRuleTag(), RuleTag: rule.GetRuleTag(),
} }
if rr.RuleTag != "" && existTags[rr.RuleTag] {
return errors.New("duplicate ruleTag ", rr.RuleTag)
}
existTags[rr.RuleTag] = true
if wh := rule.GetWebhook(); wh != nil { if wh := rule.GetWebhook(); wh != nil {
notifier, err := NewWebhookNotifier(wh) notifier, err := NewWebhookNotifier(wh)
if err != nil { if err != nil {
closeNewWebhooks()
return err return err
} }
rr.Webhook = notifier rr.Webhook = notifier
} }
btag := rule.GetBalancingTag() if btag := rule.GetBalancingTag(); len(btag) > 0 {
if len(btag) > 0 { brule, found := newBalancers[btag]
brule, found := r.balancers[btag]
if !found { if !found {
if rr.Webhook != nil {
rr.Webhook.Close()
}
closeNewWebhooks()
return errors.New("balancer ", btag, " not found") return errors.New("balancer ", btag, " not found")
} }
rr.Balancer = brule rr.Balancer = brule
} }
r.rules = append(r.rules, rr) newRules = append(newRules, rr)
} }
r.balancers.Store(&newBalancers)
r.rules.Store(&newRules)
if !shouldAppend {
closeWebhooks(oldRules)
}
return nil return nil
} }
func (r *Router) RuleExists(tag string) bool {
if tag != "" {
for _, rule := range r.rules {
if rule.RuleTag == tag {
return true
}
}
}
return false
}
// RemoveRule implements routing.Router. // RemoveRule implements routing.Router.
func (r *Router) RemoveRule(tag string) error { func (r *Router) RemoveRule(tag string) error {
if tag == "" {
return errors.New("empty tag name!")
}
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
newRules := []*Rule{} oldRules := *r.rules.Load()
if tag != "" { newRules := make([]*Rule, 0, len(oldRules))
for _, rule := range r.rules { var removed []*Rule
if rule.RuleTag != tag { for _, rule := range oldRules {
newRules = append(newRules, rule) if rule.RuleTag != tag {
} else if rule.Webhook != nil { newRules = append(newRules, rule)
rule.Webhook.Close() } else {
} removed = append(removed, rule)
} }
r.rules = newRules
return nil
} }
return errors.New("empty tag name!") r.rules.Store(&newRules)
closeWebhooks(removed)
return nil
} }
// ListRule implements routing.Router // ListRule implements routing.Router
func (r *Router) ListRule() []routing.Route { func (r *Router) ListRule() []routing.Route {
r.mu.Lock() rules := *r.rules.Load()
defer r.mu.Unlock() ruleList := make([]routing.Route, 0, len(rules))
ruleList := make([]routing.Route, 0) for _, rule := range rules {
for _, rule := range r.rules {
ruleList = append(ruleList, &Route{ ruleList = append(ruleList, &Route{
outboundTag: rule.Tag, outboundTag: rule.Tag,
ruleTag: rule.RuleTag, ruleTag: rule.RuleTag,
@@ -252,7 +195,9 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
} }
for _, rule := range r.rules { rules := *r.rules.Load()
for _, rule := range rules {
if rule.Apply(ctx) { if rule.Apply(ctx) {
return rule, ctx, nil return rule, ctx, nil
} }
@@ -265,7 +210,7 @@ func (r *Router) pickRouteInternal(ctx routing.Context) (*Rule, routing.Context,
ctx = routing_dns.ContextWithDNSClient(ctx, r.dns) ctx = routing_dns.ContextWithDNSClient(ctx, r.dns)
// Try applying rules again if we have IPs. // Try applying rules again if we have IPs.
for _, rule := range r.rules { for _, rule := range rules {
if rule.Apply(ctx) { if rule.Apply(ctx) {
return rule, ctx, nil return rule, ctx, nil
} }
@@ -279,9 +224,9 @@ func (r *Router) Start() error {
return nil return nil
} }
// closeWebhooks closes all webhook notifiers in the current rule set. // closeWebhooks closes all webhook notifiers in the given rule set.
func (r *Router) closeWebhooks() { func closeWebhooks(rules []*Rule) {
for _, rule := range r.rules { for _, rule := range rules {
if rule.Webhook != nil { if rule.Webhook != nil {
rule.Webhook.Close() rule.Webhook.Close()
} }
@@ -292,7 +237,7 @@ func (r *Router) closeWebhooks() {
func (r *Router) Close() error { func (r *Router) Close() error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
r.closeWebhooks() closeWebhooks(*r.rules.Load())
return nil return nil
} }
+17 -23
View File
@@ -8,6 +8,7 @@ import (
"net" "net"
"net/http" "net/http"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
@@ -40,6 +41,7 @@ type WebhookNotifier struct {
deduplication uint32 deduplication uint32
client *http.Client client *http.Client
seen sync.Map seen sync.Map
lastSweep atomic.Int64
done chan struct{} done chan struct{}
wg sync.WaitGroup wg sync.WaitGroup
closeOnce sync.Once closeOnce sync.Once
@@ -77,11 +79,6 @@ func NewWebhookNotifier(cfg *WebhookConfig) (*WebhookNotifier, error) {
} }
} }
if h.deduplication > 0 {
h.wg.Add(1)
go h.cleanupLoop()
}
return h, nil return h, nil
} }
@@ -201,6 +198,7 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
} }
ttl := time.Duration(h.deduplication) * time.Second ttl := time.Duration(h.deduplication) * time.Second
now := time.Now() now := time.Now()
h.maybeSweep(now, ttl)
if v, loaded := h.seen.LoadOrStore(email, now); loaded { if v, loaded := h.seen.LoadOrStore(email, now); loaded {
if now.Sub(v.(time.Time)) < ttl { if now.Sub(v.(time.Time)) < ttl {
return true return true
@@ -210,27 +208,23 @@ func (h *WebhookNotifier) isDuplicate(email string) bool {
return false return false
} }
func (h *WebhookNotifier) cleanupLoop() { func (h *WebhookNotifier) maybeSweep(now time.Time, ttl time.Duration) {
defer h.wg.Done() last := h.lastSweep.Load()
ttl := time.Duration(h.deduplication) * time.Second if now.UnixNano()-last < int64(ttl) {
ticker := time.NewTicker(ttl) return
defer ticker.Stop()
for {
select {
case <-h.done:
return
case <-ticker.C:
now := time.Now()
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
}
} }
if !h.lastSweep.CompareAndSwap(last, now.UnixNano()) {
return // another goroutine did the sweep
}
h.seen.Range(func(key, value any) bool {
if now.Sub(value.(time.Time)) >= ttl {
h.seen.Delete(key)
}
return true
})
} }
// Only need to call if the Notifier is really used, otherwise GC can clean it
func (h *WebhookNotifier) Close() error { func (h *WebhookNotifier) Close() error {
h.closeOnce.Do(func() { h.closeOnce.Do(func() {
close(h.done) close(h.done)