Skip to content

Service Registry ​

sing-box uses Go's context.Context as a service container for dependency injection. This eliminates global singletons and makes the dependency graph explicit.

Source: github.com/sagernet/sing/service, box.go, include/

How It Works ​

The sing/service package provides typed service registration:

go
// Register a service in context
func ContextWith[T any](ctx context.Context, service T) context.Context

// Retrieve a service from context
func FromContext[T any](ctx context.Context) T

// Register with panic on duplicate
func MustRegister[T any](ctx context.Context, service T)

Services are keyed by their interface type, not concrete type. This means:

go
// Register NetworkManager
service.MustRegister[adapter.NetworkManager](ctx, networkManager)

// Any code with the context can retrieve it
nm := service.FromContext[adapter.NetworkManager](ctx)

Registration in Box.New() ​

During Box.New(), all managers are registered:

go
// Create managers
endpointManager := endpoint.NewManager(...)
inboundManager := inbound.NewManager(...)
outboundManager := outbound.NewManager(...)
dnsTransportManager := dns.NewTransportManager(...)
serviceManager := boxService.NewManager(...)

// Register in context
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)

// Also register router, network manager, DNS router, connection manager
service.MustRegister[adapter.Router](ctx, router)
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)

Registry Pattern ​

Protocol types are registered via typed registries:

go
type InboundRegistry interface {
    option.InboundOptionsRegistry
    Create(ctx, router, logger, tag, inboundType string, options any) (Inbound, error)
}

type OutboundRegistry interface {
    option.OutboundOptionsRegistry
    CreateOutbound(ctx, router, logger, tag, outboundType string, options any) (Outbound, error)
}

How Registries Are Populated ​

The include/ package uses build tags to register protocol types:

go
// include/inbound.go
func InboundRegistry() *inbound.Registry {
    registry := inbound.NewRegistry()
    tun.RegisterInbound(registry)
    socks.RegisterInbound(registry)
    http.RegisterInbound(registry)
    mixed.RegisterInbound(registry)
    direct.RegisterInbound(registry)
    // ... all protocol types
    return registry
}

Each protocol registers itself:

go
// protocol/vless/inbound.go
func RegisterInbound(registry adapter.InboundRegistry) {
    inbound.Register[option.VLESSInboundOptions](registry, C.TypeVLESS, NewInbound)
}

The generic Register function maps: type string → options type → factory function.

Context Initialization ​

The box.Context() function prepares the context with registries:

go
func Context(
    ctx context.Context,
    inboundRegistry adapter.InboundRegistry,
    outboundRegistry adapter.OutboundRegistry,
    endpointRegistry adapter.EndpointRegistry,
    dnsTransportRegistry adapter.DNSTransportRegistry,
    serviceRegistry adapter.ServiceRegistry,
) context.Context {
    ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
    ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
    // ... etc.
    return ctx
}

This is called before Box.New(), typically in cmd/sing-box/main.go:

go
ctx = box.Context(ctx,
    include.InboundRegistry(),
    include.OutboundRegistry(),
    include.EndpointRegistry(),
    include.DNSTransportRegistry(),
    include.ServiceRegistry(),
)
instance, err := box.New(box.Options{
    Context: ctx,
    Options: options,
})

Dual Registration ​

Both the options registry and the adapter registry are registered:

go
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)

The options registry is used during JSON parsing to determine the correct options struct type. The adapter registry is used during Box.New() to create instances from options.

Usage in Components ​

Any component with access to the context can retrieve services:

go
// In Router constructor
func NewRouter(ctx context.Context, ...) *Router {
    return &Router{
        inbound:  service.FromContext[adapter.InboundManager](ctx),
        outbound: service.FromContext[adapter.OutboundManager](ctx),
        dns:      service.FromContext[adapter.DNSRouter](ctx),
        network:  service.FromContext[adapter.NetworkManager](ctx),
        // ...
    }
}

Comparison with Xray-core ​

AspectXray-coresing-box
DI patternRequireFeatures + reflectionContext-based typed lookup
RegistrationGlobal feature registry on InstancePer-context service registry
ResolutionLazy (resolved when all deps available)Eager (resolved at creation time)
Type safetyRuntime type assertionCompile-time generics
LifecycleFeature.Start() called by InstanceMulti-phase Start(stage)