f0f2617b79
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.
199 lines
6.2 KiB
Go
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()
|
|
}
|