package route import ( "context" "slices" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-box/experimental/clashmode" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/observable" "github.com/sagernet/sing/service" ) var _ adapter.LifecycleService = (*ReferenceManager)(nil) type ReferenceManager struct { ctx context.Context logger log.ContextLogger rules []option.Rule dnsRules []option.DNSRule staticOutbounds []string staticTransports []string subscriber *observable.Subscriber[struct{}] keepIdle map[any]bool unreferencedTransports map[string]bool } func NewReferenceManager(ctx context.Context, logger log.ContextLogger, options option.Options) *ReferenceManager { var ( staticOutbounds []string staticTransports []string ) if options.NTP != nil && options.NTP.Enabled && options.NTP.Detour != "" { staticOutbounds = append(staticOutbounds, options.NTP.Detour) } for _, outboundOptions := range options.Outbounds { staticTransports = appendDomainResolver(staticTransports, outboundOptions.Options) } for _, endpointOptions := range options.Endpoints { staticTransports = appendDomainResolver(staticTransports, endpointOptions.Options) } var ( rules []option.Rule dnsRules []option.DNSRule ) if options.Route != nil { rules = options.Route.Rules } if options.DNS != nil { dnsRules = options.DNS.Rules } return &ReferenceManager{ ctx: ctx, logger: logger, rules: rules, dnsRules: dnsRules, staticOutbounds: staticOutbounds, staticTransports: staticTransports, } } func appendDomainResolver(transports []string, rawOptions any) []string { dialerOptionsWrapper, isDialerOptionsWrapper := rawOptions.(option.DialerOptionsWrapper) if !isDialerOptionsWrapper { return transports } dialerOptions := dialerOptionsWrapper.TakeDialerOptions() if dialerOptions.DomainResolver == nil || dialerOptions.DomainResolver.Server == "" { return transports } return append(transports, dialerOptions.DomainResolver.Server) } func (m *ReferenceManager) Name() string { return "reference manager" } func (m *ReferenceManager) Start(stage adapter.StartStage, scope *adapter.Scope) error { if stage != adapter.StartStateStarted { return nil } m.subscriber = observable.NewSubscriber[struct{}](1) scope.Add(m.subscriber.Close) history := service.PtrFromContext[urltest.HistoryStorage](m.ctx) if history != nil { history.AddUpdateHook(m.subscriber) } clashMode := service.PtrFromContext[clashmode.Manager](m.ctx) if clashMode != nil { clashMode.AddUpdateHook(m.subscriber) } m.update() go m.loop() return nil } func (m *ReferenceManager) loop() { subscription, done := m.subscriber.Subscription() for { select { case <-subscription: case <-done: return } m.update() } } func (m *ReferenceManager) update() { var mode string clashMode := service.PtrFromContext[clashmode.Manager](m.ctx) if clashMode != nil { mode = clashMode.Mode() } outboundManager := service.FromContext[adapter.OutboundManager](m.ctx) endpointManager := service.FromContext[adapter.EndpointManager](m.ctx) inboundManager := service.FromContext[adapter.InboundManager](m.ctx) serviceManager := service.FromContext[adapter.ServiceManager](m.ctx) transportManager := service.FromContext[adapter.DNSTransportManager](m.ctx) networkManager := service.FromContext[adapter.NetworkManager](m.ctx) httpClientManager := service.FromContext[adapter.HTTPClientManager](m.ctx) transportQueue := slices.Clone(m.staticTransports) outboundQueue := slices.Clone(m.staticOutbounds) if !collectDNSRuleReferences(m.dnsRules, mode, &transportQueue) { defaultTransport := transportManager.Default() if defaultTransport != nil { transportQueue = append(transportQueue, defaultTransport.Tag()) } } transportQueue = append(transportQueue, networkManager.DefaultOptions().DomainResolver) if !collectRuleReferences(m.rules, mode, &outboundQueue, &transportQueue) { defaultOutbound := outboundManager.Default() if defaultOutbound != nil { outboundQueue = append(outboundQueue, defaultOutbound.Tag()) } } for _, endpoint := range endpointManager.Endpoints() { outboundQueue = append(outboundQueue, endpoint.Tag()) } for _, inbound := range inboundManager.Inbounds() { referrer, isReferrer := inbound.(adapter.Referrer) if isReferrer { outboundQueue = append(outboundQueue, referrer.References()...) } } for _, boxService := range serviceManager.Services() { referrer, isReferrer := boxService.(adapter.Referrer) if isReferrer { outboundQueue = append(outboundQueue, referrer.References()...) } } referrer, isReferrer := httpClientManager.(adapter.Referrer) if isReferrer { outboundQueue = append(outboundQueue, referrer.References()...) } referencedTransports := make(map[string]bool) for len(transportQueue) > 0 { tag := transportQueue[0] transportQueue = transportQueue[1:] if tag == "" || referencedTransports[tag] { continue } transport, loaded := transportManager.Transport(tag) if !loaded { continue } referencedTransports[tag] = true transportQueue = append(transportQueue, transport.Dependencies()...) transportReferrer, isTransportReferrer := transport.(adapter.Referrer) if isTransportReferrer { outboundQueue = append(outboundQueue, transportReferrer.References()...) } } referencedOutbounds := make(map[string]bool) for len(outboundQueue) > 0 { tag := outboundQueue[0] outboundQueue = outboundQueue[1:] if tag == "" || referencedOutbounds[tag] { continue } outbound, loaded := outboundManager.Outbound(tag) if !loaded { continue } referencedOutbounds[tag] = true outboundReferrer, isOutboundReferrer := outbound.(adapter.Referrer) if isOutboundReferrer { outboundQueue = append(outboundQueue, outboundReferrer.References()...) } else { outboundQueue = append(outboundQueue, outbound.Dependencies()...) } } keepIdle := make(map[any]bool) for _, outbound := range outboundManager.Outbounds() { keeper, isKeeper := outbound.(adapter.IdleConnectionKeeper) if !isKeeper { continue } m.applyKeepIdle(keepIdle, idleTarget{ value: outbound, keeper: keeper, kind: "outbound/", action: "closing idle connections", typeName: outbound.Type(), tag: outbound.Tag(), keep: referencedOutbounds[outbound.Tag()], }) } unreferencedTransports := make(map[string]bool) for _, transport := range transportManager.Transports() { tag := transport.Tag() referenced := referencedTransports[tag] keeper, isKeeper := transport.(adapter.IdleConnectionKeeper) if isKeeper { m.applyKeepIdle(keepIdle, idleTarget{ value: transport, keeper: keeper, kind: "dns/", action: "closing idle connections", typeName: transport.Type(), tag: tag, keep: referenced, }) continue } if referenced { continue } unreferencedTransports[tag] = true if m.unreferencedTransports != nil && !m.unreferencedTransports[tag] { m.logger.Debug("dns/", transport.Type(), "[", tag, "] is unreferenced, resetting") transport.Reset() } } m.keepIdle = keepIdle m.unreferencedTransports = unreferencedTransports } type idleKeeper interface { SetKeepIdleConnections(keep bool) } type idleTarget struct { value any keeper idleKeeper kind string action string typeName string tag string keep bool } func (m *ReferenceManager) applyKeepIdle(keepIdle map[any]bool, target idleTarget) { keepIdle[target.value] = target.keep previous, tracked := m.keepIdle[target.value] if tracked && previous == target.keep { return } if !target.keep { m.logger.Debug(target.kind, target.typeName, "[", target.tag, "] is unreferenced, ", target.action) } target.keeper.SetKeepIdleConnections(target.keep) } func (m *ReferenceManager) CloseIdleConnections() { outboundManager := service.FromContext[adapter.OutboundManager](m.ctx) transportManager := service.FromContext[adapter.DNSTransportManager](m.ctx) for _, outbound := range outboundManager.Outbounds() { keeper, isKeeper := outbound.(adapter.IdleConnectionKeeper) if isKeeper { keeper.CloseIdleConnections() } } for _, transport := range transportManager.Transports() { keeper, isKeeper := transport.(adapter.IdleConnectionKeeper) if isKeeper { keeper.CloseIdleConnections() } } }