Files
sing-box/protocol/group/selector.go
T
世界 0298b2f33b Fix group status updates broken by API service
The URL test history update hook and the Clash mode update hook were
single-slot: the API service's attached service overwrote the hook set
by the daemon, so clients stopped receiving group updates. Replace both
with multicast hook lists.

Also share a single URL test history storage via context: Clash API
looked it up under a key nobody registered and fell back to its own
empty storage, so dashboards showed no delay once an API service was
configured. Selector changes now notify through the shared storage,
covering selections made from any API surface.
2026-06-13 09:30:11 +08:00

199 lines
6.2 KiB
Go

package group
import (
"context"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
"github.com/sagernet/sing-box/common/interrupt"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
)
func RegisterSelector(registry *outbound.Registry) {
outbound.Register[option.SelectorOutboundOptions](registry, C.TypeSelector, NewSelector)
}
var (
_ adapter.OutboundGroup = (*Selector)(nil)
_ adapter.ConnectionHandler = (*Selector)(nil)
_ adapter.PacketConnectionHandler = (*Selector)(nil)
)
type Selector struct {
outbound.Adapter
ctx context.Context
outbound adapter.OutboundManager
connection adapter.ConnectionManager
logger logger.ContextLogger
tags []string
defaultTag string
outbounds map[string]adapter.Outbound
selected common.TypedValue[adapter.Outbound]
history *urltest.HistoryStorage
interruptGroup *interrupt.Group
interruptExternalConnections bool
}
func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (adapter.Outbound, error) {
outbound := &Selector{
Adapter: outbound.NewAdapter(C.TypeSelector, tag, nil, options.Outbounds),
ctx: ctx,
outbound: service.FromContext[adapter.OutboundManager](ctx),
connection: service.FromContext[adapter.ConnectionManager](ctx),
logger: logger,
tags: options.Outbounds,
defaultTag: options.Default,
outbounds: make(map[string]adapter.Outbound),
history: service.PtrFromContext[urltest.HistoryStorage](ctx),
interruptGroup: interrupt.NewGroup(),
interruptExternalConnections: options.InterruptExistConnections,
}
if len(outbound.tags) == 0 {
return nil, E.New("missing tags")
}
return outbound, nil
}
func (s *Selector) Network() []string {
selected := s.selected.Load()
if selected == nil {
return []string{N.NetworkTCP, N.NetworkUDP}
}
return selected.Network()
}
func (s *Selector) Start() error {
for i, tag := range s.tags {
detour, loaded := s.outbound.Outbound(tag)
if !loaded {
return E.New("outbound ", i, " not found: ", tag)
}
s.outbounds[tag] = detour
}
if s.Tag() != "" {
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
selected := cacheFile.LoadSelected(s.Tag())
if selected != "" {
detour, loaded := s.outbounds[selected]
if loaded {
s.selected.Store(detour)
return nil
}
}
}
}
if s.defaultTag != "" {
detour, loaded := s.outbounds[s.defaultTag]
if !loaded {
return E.New("default outbound not found: ", s.defaultTag)
}
s.selected.Store(detour)
return nil
}
s.selected.Store(s.outbounds[s.tags[0]])
return nil
}
func (s *Selector) Now() string {
selected := s.selected.Load()
if selected == nil {
return s.tags[0]
}
return selected.Tag()
}
func (s *Selector) All() []string {
return s.tags
}
func (s *Selector) SelectOutbound(tag string) bool {
detour, loaded := s.outbounds[tag]
if !loaded {
return false
}
if s.selected.Swap(detour) == detour {
return true
}
if s.Tag() != "" {
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)
if cacheFile != nil {
err := cacheFile.StoreSelected(s.Tag(), tag)
if err != nil {
s.logger.Error("store selected: ", err)
}
}
}
s.interruptGroup.Interrupt(s.interruptExternalConnections)
if s.history != nil {
s.history.NotifyUpdated()
}
return true
}
func (s *Selector) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
conn, err := s.selected.Load().DialContext(ctx, network, destination)
if err != nil {
return nil, err
}
return s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
}
func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
conn, err := s.selected.Load().ListenPacket(ctx, destination)
if err != nil {
return nil, err
}
return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil
}
func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load()
if outboundHandler, isHandler := selected.(adapter.ConnectionHandler); isHandler {
outboundHandler.NewConnection(ctx, conn, metadata, onClose)
} else {
s.connection.NewConnection(ctx, selected, conn, metadata, onClose)
}
}
func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
ctx = interrupt.ContextWithIsExternalConnection(ctx)
selected := s.selected.Load()
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandler); isHandler {
outboundHandler.NewPacketConnection(ctx, conn, metadata, onClose)
} else {
s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose)
}
}
func (s *Selector) NewDirectRouteConnection(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) {
selected := s.selected.Load()
if !common.Contains(selected.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", selected.Tag())
}
return selected.(adapter.DirectRouteOutbound).NewDirectRouteConnection(metadata, routeContext, timeout)
}
func RealTag(detour adapter.Outbound) string {
if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
return group.Now()
}
return detour.Tag()
}