浏览代码

Add service component type

世界 6 月之前
父节点
当前提交
7a1eee78df

+ 20 - 0
adapter/dns.go

@@ -7,7 +7,9 @@ import (
 	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"
 	"github.com/sagernet/sing/common/logger"
+	"github.com/sagernet/sing/service"
 
 	"github.com/miekg/dns"
 )
@@ -38,6 +40,24 @@ type DNSQueryOptions struct {
 	ClientSubnet netip.Prefix
 }
 
+func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
+	if options == nil {
+		return &DNSQueryOptions{}, nil
+	}
+	transportManager := service.FromContext[DNSTransportManager](ctx)
+	transport, loaded := transportManager.Transport(options.Server)
+	if !loaded {
+		return nil, E.New("domain resolver not found: " + options.Server)
+	}
+	return &DNSQueryOptions{
+		Transport:    transport,
+		Strategy:     C.DomainStrategy(options.Strategy),
+		DisableCache: options.DisableCache,
+		RewriteTTL:   options.RewriteTTL,
+		ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
+	}, nil
+}
+
 type RDRCStore interface {
 	LoadRDRC(transportName string, qName string, qType uint16) (rejected bool)
 	SaveRDRC(transportName string, qName string, qType uint16) error

+ 1 - 1
adapter/fakeip.go

@@ -7,7 +7,7 @@ import (
 )
 
 type FakeIPStore interface {
-	Service
+	SimpleLifecycle
 	Contains(address netip.Addr) bool
 	Create(domain string, isIPv6 bool) (netip.Addr, error)
 	Lookup(address netip.Addr) (string, bool)

+ 5 - 0
adapter/lifecycle.go

@@ -2,6 +2,11 @@ package adapter
 
 import E "github.com/sagernet/sing/common/exceptions"
 
+type SimpleLifecycle interface {
+	Start() error
+	Close() error
+}
+
 type StartStage uint8
 
 const (

+ 6 - 6
adapter/lifecycle_legacy.go

@@ -28,14 +28,14 @@ func LegacyStart(starter any, stage StartStage) error {
 }
 
 type lifecycleServiceWrapper struct {
-	Service
+	SimpleLifecycle
 	name string
 }
 
-func NewLifecycleService(service Service, name string) LifecycleService {
+func NewLifecycleService(service SimpleLifecycle, name string) LifecycleService {
 	return &lifecycleServiceWrapper{
-		Service: service,
-		name:    name,
+		SimpleLifecycle: service,
+		name:            name,
 	}
 }
 
@@ -44,9 +44,9 @@ func (l *lifecycleServiceWrapper) Name() string {
 }
 
 func (l *lifecycleServiceWrapper) Start(stage StartStage) error {
-	return LegacyStart(l.Service, stage)
+	return LegacyStart(l.SimpleLifecycle, stage)
 }
 
 func (l *lifecycleServiceWrapper) Close() error {
-	return l.Service.Close()
+	return l.SimpleLifecycle.Close()
 }

+ 1 - 1
adapter/rule.go

@@ -11,7 +11,7 @@ type HeadlessRule interface {
 
 type Rule interface {
 	HeadlessRule
-	Service
+	SimpleLifecycle
 	Type() string
 	Action() RuleAction
 }

+ 23 - 2
adapter/service.go

@@ -1,6 +1,27 @@
 package adapter
 
+import (
+	"context"
+
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+)
+
 type Service interface {
-	Start() error
-	Close() error
+	Lifecycle
+	Type() string
+	Tag() string
+}
+
+type ServiceRegistry interface {
+	option.ServiceOptionsRegistry
+	Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) (Service, error)
+}
+
+type ServiceManager interface {
+	Lifecycle
+	Services() []Service
+	Get(tag string) (Service, bool)
+	Remove(tag string) error
+	Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error
 }

+ 21 - 0
adapter/service/adapter.go

@@ -0,0 +1,21 @@
+package service
+
+type Adapter struct {
+	serviceType string
+	serviceTag  string
+}
+
+func NewAdapter(serviceType string, serviceTag string) Adapter {
+	return Adapter{
+		serviceType: serviceType,
+		serviceTag:  serviceTag,
+	}
+}
+
+func (a *Adapter) Type() string {
+	return a.serviceType
+}
+
+func (a *Adapter) Tag() string {
+	return a.serviceTag
+}

+ 143 - 0
adapter/service/manager.go

@@ -0,0 +1,143 @@
+package service
+
+import (
+	"context"
+	"os"
+	"sync"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/common/taskmonitor"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+var _ adapter.ServiceManager = (*Manager)(nil)
+
+type Manager struct {
+	logger       log.ContextLogger
+	registry     adapter.ServiceRegistry
+	access       sync.Mutex
+	started      bool
+	stage        adapter.StartStage
+	services     []adapter.Service
+	serviceByTag map[string]adapter.Service
+}
+
+func NewManager(logger log.ContextLogger, registry adapter.ServiceRegistry) *Manager {
+	return &Manager{
+		logger:       logger,
+		registry:     registry,
+		serviceByTag: make(map[string]adapter.Service),
+	}
+}
+
+func (m *Manager) Start(stage adapter.StartStage) error {
+	m.access.Lock()
+	defer m.access.Unlock()
+	if m.started && m.stage >= stage {
+		panic("already started")
+	}
+	m.started = true
+	m.stage = stage
+	for _, service := range m.services {
+		err := adapter.LegacyStart(service, stage)
+		if err != nil {
+			return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
+		}
+	}
+	return nil
+}
+
+func (m *Manager) Close() error {
+	m.access.Lock()
+	defer m.access.Unlock()
+	if !m.started {
+		return nil
+	}
+	m.started = false
+	services := m.services
+	m.services = nil
+	monitor := taskmonitor.New(m.logger, C.StopTimeout)
+	var err error
+	for _, service := range services {
+		monitor.Start("close service/", service.Type(), "[", service.Tag(), "]")
+		err = E.Append(err, service.Close(), func(err error) error {
+			return E.Cause(err, "close service/", service.Type(), "[", service.Tag(), "]")
+		})
+		monitor.Finish()
+	}
+	return nil
+}
+
+func (m *Manager) Services() []adapter.Service {
+	m.access.Lock()
+	defer m.access.Unlock()
+	return m.services
+}
+
+func (m *Manager) Get(tag string) (adapter.Service, bool) {
+	m.access.Lock()
+	service, found := m.serviceByTag[tag]
+	m.access.Unlock()
+	return service, found
+}
+
+func (m *Manager) Remove(tag string) error {
+	m.access.Lock()
+	service, found := m.serviceByTag[tag]
+	if !found {
+		m.access.Unlock()
+		return os.ErrInvalid
+	}
+	delete(m.serviceByTag, tag)
+	index := common.Index(m.services, func(it adapter.Service) bool {
+		return it == service
+	})
+	if index == -1 {
+		panic("invalid service index")
+	}
+	m.services = append(m.services[:index], m.services[index+1:]...)
+	started := m.started
+	m.access.Unlock()
+	if started {
+		return service.Close()
+	}
+	return nil
+}
+
+func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, serviceType string, options any) error {
+	service, err := m.registry.Create(ctx, logger, tag, serviceType, options)
+	if err != nil {
+		return err
+	}
+	m.access.Lock()
+	defer m.access.Unlock()
+	if m.started {
+		for _, stage := range adapter.ListStartStages {
+			err = adapter.LegacyStart(service, stage)
+			if err != nil {
+				return E.Cause(err, stage, " service/", service.Type(), "[", service.Tag(), "]")
+			}
+		}
+	}
+	if existsService, loaded := m.serviceByTag[tag]; loaded {
+		if m.started {
+			err = existsService.Close()
+			if err != nil {
+				return E.Cause(err, "close service/", existsService.Type(), "[", existsService.Tag(), "]")
+			}
+		}
+		existsIndex := common.Index(m.services, func(it adapter.Service) bool {
+			return it == existsService
+		})
+		if existsIndex == -1 {
+			panic("invalid service index")
+		}
+		m.services = append(m.services[:existsIndex], m.services[existsIndex+1:]...)
+	}
+	m.services = append(m.services, service)
+	m.serviceByTag[tag] = service
+	return nil
+}

+ 72 - 0
adapter/service/registry.go

@@ -0,0 +1,72 @@
+package service
+
+import (
+	"context"
+	"sync"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.Service, error)
+
+func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) {
+	registry.register(outboundType, func() any {
+		return new(Options)
+	}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.Service, error) {
+		var options *Options
+		if rawOptions != nil {
+			options = rawOptions.(*Options)
+		}
+		return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
+	})
+}
+
+var _ adapter.ServiceRegistry = (*Registry)(nil)
+
+type (
+	optionsConstructorFunc func() any
+	constructorFunc        func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.Service, error)
+)
+
+type Registry struct {
+	access      sync.Mutex
+	optionsType map[string]optionsConstructorFunc
+	constructor map[string]constructorFunc
+}
+
+func NewRegistry() *Registry {
+	return &Registry{
+		optionsType: make(map[string]optionsConstructorFunc),
+		constructor: make(map[string]constructorFunc),
+	}
+}
+
+func (m *Registry) CreateOptions(outboundType string) (any, bool) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	optionsConstructor, loaded := m.optionsType[outboundType]
+	if !loaded {
+		return nil, false
+	}
+	return optionsConstructor(), true
+}
+
+func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Service, error) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	constructor, loaded := m.constructor[outboundType]
+	if !loaded {
+		return nil, E.New("outbound type not found: " + outboundType)
+	}
+	return constructor(ctx, logger, tag, options)
+}
+
+func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	m.optionsType[outboundType] = optionsConstructor
+	m.constructor[outboundType] = constructor
+}

+ 1 - 1
adapter/time.go

@@ -3,6 +3,6 @@ package adapter
 import "time"
 
 type TimeService interface {
-	Service
+	SimpleLifecycle
 	TimeFunc() func() time.Time
 }

+ 76 - 41
box.go

@@ -12,6 +12,7 @@ import (
 	"github.com/sagernet/sing-box/adapter/endpoint"
 	"github.com/sagernet/sing-box/adapter/inbound"
 	"github.com/sagernet/sing-box/adapter/outbound"
+	boxService "github.com/sagernet/sing-box/adapter/service"
 	"github.com/sagernet/sing-box/common/certificate"
 	"github.com/sagernet/sing-box/common/dialer"
 	"github.com/sagernet/sing-box/common/taskmonitor"
@@ -34,22 +35,23 @@ import (
 	"github.com/sagernet/sing/service/pause"
 )
 
-var _ adapter.Service = (*Box)(nil)
+var _ adapter.SimpleLifecycle = (*Box)(nil)
 
 type Box struct {
-	createdAt    time.Time
-	logFactory   log.Factory
-	logger       log.ContextLogger
-	network      *route.NetworkManager
-	endpoint     *endpoint.Manager
-	inbound      *inbound.Manager
-	outbound     *outbound.Manager
-	dnsTransport *dns.TransportManager
-	dnsRouter    *dns.Router
-	connection   *route.ConnectionManager
-	router       *route.Router
-	services     []adapter.LifecycleService
-	done         chan struct{}
+	createdAt       time.Time
+	logFactory      log.Factory
+	logger          log.ContextLogger
+	network         *route.NetworkManager
+	endpoint        *endpoint.Manager
+	inbound         *inbound.Manager
+	outbound        *outbound.Manager
+	service         *boxService.Manager
+	dnsTransport    *dns.TransportManager
+	dnsRouter       *dns.Router
+	connection      *route.ConnectionManager
+	router          *route.Router
+	internalService []adapter.LifecycleService
+	done            chan struct{}
 }
 
 type Options struct {
@@ -64,6 +66,7 @@ func Context(
 	outboundRegistry adapter.OutboundRegistry,
 	endpointRegistry adapter.EndpointRegistry,
 	dnsTransportRegistry adapter.DNSTransportRegistry,
+	serviceRegistry adapter.ServiceRegistry,
 ) context.Context {
 	if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
 		service.FromContext[adapter.InboundRegistry](ctx) == nil {
@@ -84,6 +87,10 @@ func Context(
 		ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
 		ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
 	}
+	if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
+		ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
+		ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
+	}
 	return ctx
 }
 
@@ -99,6 +106,7 @@ func New(options Options) (*Box, error) {
 	inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
 	outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
 	dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
+	serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
 
 	if endpointRegistry == nil {
 		return nil, E.New("missing endpoint registry in context")
@@ -109,6 +117,12 @@ func New(options Options) (*Box, error) {
 	if outboundRegistry == nil {
 		return nil, E.New("missing outbound registry in context")
 	}
+	if dnsTransportRegistry == nil {
+		return nil, E.New("missing DNS transport registry in context")
+	}
+	if serviceRegistry == nil {
+		return nil, E.New("missing service registry in context")
+	}
 
 	ctx = pause.WithDefaultManager(ctx)
 	experimentalOptions := common.PtrValueOrDefault(options.Experimental)
@@ -142,7 +156,7 @@ func New(options Options) (*Box, error) {
 		return nil, E.Cause(err, "create log factory")
 	}
 
-	var services []adapter.LifecycleService
+	var internalServices []adapter.LifecycleService
 	certificateOptions := common.PtrValueOrDefault(options.Certificate)
 	if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
 		len(certificateOptions.Certificate) > 0 ||
@@ -153,7 +167,7 @@ func New(options Options) (*Box, error) {
 			return nil, err
 		}
 		service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
-		services = append(services, certificateStore)
+		internalServices = append(internalServices, certificateStore)
 	}
 
 	routeOptions := common.PtrValueOrDefault(options.Route)
@@ -162,10 +176,12 @@ func New(options Options) (*Box, error) {
 	inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
 	outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
 	dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
+	serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
 	service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
 	service.MustRegister[adapter.InboundManager](ctx, inboundManager)
 	service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
 	service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
+	service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
 	dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
 	service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
 	networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
@@ -280,6 +296,24 @@ func New(options Options) (*Box, error) {
 			return nil, E.Cause(err, "initialize outbound[", i, "]")
 		}
 	}
+	for i, serviceOptions := range options.Services {
+		var tag string
+		if serviceOptions.Tag != "" {
+			tag = serviceOptions.Tag
+		} else {
+			tag = F.ToString(i)
+		}
+		err = serviceManager.Create(
+			ctx,
+			logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
+			tag,
+			serviceOptions.Type,
+			serviceOptions.Options,
+		)
+		if err != nil {
+			return nil, E.Cause(err, "initialize service[", i, "]")
+		}
+	}
 	outboundManager.Initialize(common.Must1(
 		direct.NewOutbound(
 			ctx,
@@ -305,7 +339,7 @@ func New(options Options) (*Box, error) {
 	if needCacheFile {
 		cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
 		service.MustRegister[adapter.CacheFile](ctx, cacheFile)
-		services = append(services, cacheFile)
+		internalServices = append(internalServices, cacheFile)
 	}
 	if needClashAPI {
 		clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI)
@@ -316,7 +350,7 @@ func New(options Options) (*Box, error) {
 		}
 		router.AppendTracker(clashServer)
 		service.MustRegister[adapter.ClashServer](ctx, clashServer)
-		services = append(services, clashServer)
+		internalServices = append(internalServices, clashServer)
 	}
 	if needV2RayAPI {
 		v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI))
@@ -325,7 +359,7 @@ func New(options Options) (*Box, error) {
 		}
 		if v2rayServer.StatsService() != nil {
 			router.AppendTracker(v2rayServer.StatsService())
-			services = append(services, v2rayServer)
+			internalServices = append(internalServices, v2rayServer)
 			service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
 		}
 	}
@@ -343,22 +377,23 @@ func New(options Options) (*Box, error) {
 			WriteToSystem: ntpOptions.WriteToSystem,
 		})
 		timeService.TimeService = ntpService
-		services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
+		internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
 	}
 	return &Box{
-		network:      networkManager,
-		endpoint:     endpointManager,
-		inbound:      inboundManager,
-		outbound:     outboundManager,
-		dnsTransport: dnsTransportManager,
-		dnsRouter:    dnsRouter,
-		connection:   connectionManager,
-		router:       router,
-		createdAt:    createdAt,
-		logFactory:   logFactory,
-		logger:       logFactory.Logger(),
-		services:     services,
-		done:         make(chan struct{}),
+		network:         networkManager,
+		endpoint:        endpointManager,
+		inbound:         inboundManager,
+		outbound:        outboundManager,
+		dnsTransport:    dnsTransportManager,
+		service:         serviceManager,
+		dnsRouter:       dnsRouter,
+		connection:      connectionManager,
+		router:          router,
+		createdAt:       createdAt,
+		logFactory:      logFactory,
+		logger:          logFactory.Logger(),
+		internalService: internalServices,
+		done:            make(chan struct{}),
 	}, nil
 }
 
@@ -408,11 +443,11 @@ func (s *Box) preStart() error {
 	if err != nil {
 		return E.Cause(err, "start logger")
 	}
-	err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api
+	err = adapter.StartNamed(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
 	if err != nil {
 		return err
 	}
-	err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
+	err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
 	if err != nil {
 		return err
 	}
@@ -428,7 +463,7 @@ func (s *Box) start() error {
 	if err != nil {
 		return err
 	}
-	err = adapter.StartNamed(adapter.StartStateStart, s.services)
+	err = adapter.StartNamed(adapter.StartStateStart, s.internalService)
 	if err != nil {
 		return err
 	}
@@ -440,19 +475,19 @@ func (s *Box) start() error {
 	if err != nil {
 		return err
 	}
-	err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
+	err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
 	if err != nil {
 		return err
 	}
-	err = adapter.StartNamed(adapter.StartStatePostStart, s.services)
+	err = adapter.StartNamed(adapter.StartStatePostStart, s.internalService)
 	if err != nil {
 		return err
 	}
-	err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
+	err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
 	if err != nil {
 		return err
 	}
-	err = adapter.StartNamed(adapter.StartStateStarted, s.services)
+	err = adapter.StartNamed(adapter.StartStateStarted, s.internalService)
 	if err != nil {
 		return err
 	}
@@ -469,7 +504,7 @@ func (s *Box) Close() error {
 	err := common.Close(
 		s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
 	)
-	for _, lifecycleService := range s.services {
+	for _, lifecycleService := range s.internalService {
 		err = E.Append(err, lifecycleService.Close(), func(err error) error {
 			return E.Cause(err, "close ", lifecycleService.Name())
 		})

+ 1 - 1
cmd/sing-box/cmd.go

@@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) {
 		configPaths = append(configPaths, "config.json")
 	}
 	globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger()))
-	globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry())
+	globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.DNSTransportRegistry(), include.ServiceRegistry())
 }

+ 1 - 1
common/tls/acme.go

@@ -68,7 +68,7 @@ func encoderConfig() zapcore.EncoderConfig {
 	return config
 }
 
-func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
+func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
 	var acmeServer string
 	switch options.Provider {
 	case "", "letsencrypt":

+ 1 - 1
common/tls/acme_stub.go

@@ -12,6 +12,6 @@ import (
 	"github.com/sagernet/sing/common/logger"
 )
 
-func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.Service, error) {
+func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
 	return nil, nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
 }

+ 2 - 2
common/tls/std_server.go

@@ -22,7 +22,7 @@ var errInsecureUnused = E.New("tls: insecure unused")
 type STDServerConfig struct {
 	config          *tls.Config
 	logger          log.Logger
-	acmeService     adapter.Service
+	acmeService     adapter.SimpleLifecycle
 	certificate     []byte
 	key             []byte
 	certificatePath string
@@ -165,7 +165,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
 		return nil, nil
 	}
 	var tlsConfig *tls.Config
-	var acmeService adapter.Service
+	var acmeService adapter.SimpleLifecycle
 	var err error
 	if options.ACME != nil && len(options.ACME.Domain) > 0 {
 		//nolint:staticcheck

+ 1 - 1
experimental/libbox/config.go

@@ -33,7 +33,7 @@ func BaseContext(platformInterface PlatformInterface) context.Context {
 			})
 		}
 	}
-	return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry)
+	return box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
 }
 
 func parseConfig(ctx context.Context, configContent string) (option.Options, error) {

+ 5 - 0
include/registry.go

@@ -118,6 +118,11 @@ func DNSTransportRegistry() *dns.TransportRegistry {
 	return registry
 }
 
+func ServiceRegistry() *service.Registry {
+	registry := service.NewRegistry()
+	return registry
+}
+
 func registerStubForRemovedInbounds(registry *inbound.Registry) {
 	inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) {
 		return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0")

+ 0 - 1
option/dns.go

@@ -121,7 +121,6 @@ type LegacyDNSFakeIPOptions struct {
 type DNSTransportOptionsRegistry interface {
 	CreateOptions(transportType string) (any, bool)
 }
-
 type _DNSServerOptions struct {
 	Type    string `json:"type,omitempty"`
 	Tag     string `json:"tag,omitempty"`

+ 2 - 2
option/endpoint.go

@@ -32,11 +32,11 @@ func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) err
 	}
 	registry := service.FromContext[EndpointOptionsRegistry](ctx)
 	if registry == nil {
-		return E.New("missing Endpoint fields registry in context")
+		return E.New("missing endpoint fields registry in context")
 	}
 	options, loaded := registry.CreateOptions(h.Type)
 	if !loaded {
-		return E.New("unknown inbound type: ", h.Type)
+		return E.New("unknown endpoint type: ", h.Type)
 	}
 	err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options)
 	if err != nil {

+ 1 - 1
option/inbound.go

@@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro
 	}
 	registry := service.FromContext[InboundOptionsRegistry](ctx)
 	if registry == nil {
-		return E.New("missing Inbound fields registry in context")
+		return E.New("missing inbound fields registry in context")
 	}
 	options, loaded := registry.CreateOptions(h.Type)
 	if !loaded {

+ 1 - 0
option/options.go

@@ -19,6 +19,7 @@ type _Options struct {
 	Inbounds     []Inbound            `json:"inbounds,omitempty"`
 	Outbounds    []Outbound           `json:"outbounds,omitempty"`
 	Route        *RouteOptions        `json:"route,omitempty"`
+	Services     []Service            `json:"services,omitempty"`
 	Experimental *ExperimentalOptions `json:"experimental,omitempty"`
 }
 

+ 47 - 0
option/service.go

@@ -0,0 +1,47 @@
+package option
+
+import (
+	"context"
+
+	E "github.com/sagernet/sing/common/exceptions"
+	"github.com/sagernet/sing/common/json"
+	"github.com/sagernet/sing/common/json/badjson"
+	"github.com/sagernet/sing/service"
+)
+
+type ServiceOptionsRegistry interface {
+	CreateOptions(serviceType string) (any, bool)
+}
+
+type _Service struct {
+	Type    string `json:"type"`
+	Tag     string `json:"tag,omitempty"`
+	Options any    `json:"-"`
+}
+
+type Service _Service
+
+func (h *Service) MarshalJSONContext(ctx context.Context) ([]byte, error) {
+	return badjson.MarshallObjectsContext(ctx, (*_Service)(h), h.Options)
+}
+
+func (h *Service) UnmarshalJSONContext(ctx context.Context, content []byte) error {
+	err := json.UnmarshalContext(ctx, content, (*_Service)(h))
+	if err != nil {
+		return err
+	}
+	registry := service.FromContext[ServiceOptionsRegistry](ctx)
+	if registry == nil {
+		return E.New("missing service fields registry in context")
+	}
+	options, loaded := registry.CreateOptions(h.Type)
+	if !loaded {
+		return E.New("unknown inbound type: ", h.Type)
+	}
+	err = badjson.UnmarshallExcludedContext(ctx, content, (*_Service)(h), options)
+	if err != nil {
+		return err
+	}
+	h.Options = options
+	return nil
+}