How S-UI Implements DNS Transport and Resolution: A Technical Deep Dive
S-UI delegates DNS handling to the sing-box core by registering transport factories (TCP, UDP, TLS, HTTPS, QUIC, etc.) in a TransportRegistry, instantiating a TransportManager to create DNS servers from user configuration, and wiring a Router that applies domain-specific resolution rules.
S-UI is a web-based user interface for the sing-box proxy platform that simplifies complex proxy configurations. Understanding how S-UI handles DNS transport and resolution requires examining its integration with sing-box's DNS subsystem rather than a custom implementation. The architecture relies on a registry pattern for pluggable transports, a manager for lifecycle control, and a rule-based router for intelligent query distribution.
Overview of the DNS Architecture
S-UI does not implement its own DNS stack from scratch. Instead, it orchestrates sing-box's DNS subsystem through three primary components: the Transport Registry, the Transport Manager, and the Router. During startup, S-UI registers available DNS transports, builds a manager that creates concrete DNS servers defined in the user configuration, and wires a router that applies resolution rules.
The execution flow follows four distinct stages:
- Register DNS transport factories –
core/DNSTransportRegistry()registers handlers for TCP, UDP, TLS, HTTPS, QUIC, DHCP, hosts-file, fake-IP, local, and optional Tailscale transports. - Create a Transport Manager –
dns.NewTransportManager(instantiated incore/Box.NewBox) receives the registry and DNS options from the configuration. - Instantiate each transport – The manager iterates over
options.DNS.Serversand calls the corresponding factory to build a concrete transport (e.g., a DoH client). - Create a Router –
dns.NewRouterbuilds the rule engine that decides which server handles a particular query, supporting domain-specific rules, fallback, and default resolvers.
Registering DNS Transport Factories
The foundation of S-UI's DNS flexibility lies in the transport registry defined in core/register.go. The DNSTransportRegistry() function initializes a new registry and registers factory functions for each supported protocol:
// core/register.go – DNSTransportRegistry
func DNSTransportRegistry() *dns.TransportRegistry {
registry := dns.NewTransportRegistry()
transport.RegisterTCP(registry) // TCP transport
transport.RegisterUDP(registry) // UDP transport
transport.RegisterTLS(registry) // TLS over TCP
transport.RegisterHTTPS(registry) // DoH (HTTPS)
hosts.RegisterTransport(registry) // hosts-file lookup
local.RegisterTransport(registry) // local stub resolver
fakeip.RegisterTransport(registry) // fake-IP generator
quic.RegisterTransport(registry) // QUIC transport
quic.RegisterHTTP3Transport(registry) // HTTP/3 (DoH over QUIC)
dhcp.RegisterTransport(registry) // DHCP resolver
registerTailscaleTransport(registry) // optional Tailscale DNS
return registry
}
The registry stores factory functions keyed by transport type strings ("tcp", "udp", "https", "quic"). When the manager needs a server of a given type, it looks up the factory and executes it with the provided configuration options.
Configuration Schema
The user-visible configuration that drives the registry lives in the SingBoxConfig struct within service/config.go. The Dns field contains the full DNS section, including servers, rules, and hosts, passed directly to sing-box:
// service/config.go
type SingBoxConfig struct {
Log json.RawMessage `json:"log"`
Dns json.RawMessage `json:"dns"` // <-- DNS block
Ntp json.RawMessage `json:"ntp"`
// …
}
This raw JSON configuration is parsed into structured options that the DNS manager consumes during initialization.
Building the Transport Manager and Router
Inside core/box.go, the NewBox function orchestrates the instantiation of the DNS subsystem. It creates both the transport manager and the router, registering them in the service container for dependency injection:
// core/box.go – NewBox (excerpt)
dnsTransportManager := dns.NewTransportManager(
logFactory.NewLogger("dns/transport"),
dnsTransportRegistry,
outboundManager,
dnsOptions.Final,
)
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
// DNS router – applies rules, default resolver, domain-specific routing
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
The dns.NewTransportManager creates a manager that instantiates each server defined under dns.servers. The dns.NewRouter consumes the same dnsOptions to build a rule-based engine that determines which transport handles specific queries. Both objects are stored as adapter.DNSTransportManager and adapter.DNSRouter in the service container.
Instantiating DNS Servers
The Box.NewBox function iterates through the user-provided server configurations to create concrete transport instances:
for i, transportOptions := range dnsOptions.Servers {
var tag string
if transportOptions.Tag != "" {
tag = transportOptions.Tag
} else {
tag = F.ToString(i)
}
err = dnsTransportManager.Create(
ctx,
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
tag,
transportOptions.Type, // e.g. "udp", "https", "quic"
transportOptions.Options,
)
// error handling omitted for brevity
}
For every server entry, the manager looks up the factory for transportOptions.Type in the registry, calls the factory with the logger, tag, and type-specific options (address, bootstrap, TLS settings), and stores the resulting adapter.DNSTransport instance. Because these factories belong to the sing-box library, all protocol implementations (DoH client, TLS handshake, QUIC, fake-IP cache) are provided out-of-the-box.
The DNS Resolution Flow
When a request reaches sing-box (e.g., from an outbound proxy needing to resolve a hostname), the resolution process follows this path:
- The Router (
dns.Router) receives the query. - It evaluates its rule set defined in
dns.rules(domain-based actions,default_domain_resolver, etc.). - The router selects the appropriate transport (server) from the manager by tag.
- The selected transport performs the actual lookup (UDP, DoH, QUIC, etc.) and returns the answer to the core.
If no rule matches, the router falls back to the default server defined under dns.final.
Optional Tailscale Transport
S-UI supports conditional compilation for Tailscale DNS. When built with the with_tailscale build tag, the stub in core/register_tailscale_transport.go is replaced by a real implementation:
func registerTailscaleTransport(registry *dns.TransportRegistry) {
dns.RegisterTransport[option.TailscaleDNSServerOptions](
registry,
C.DNSTypeTailscale,
func(ctx context.Context, logger log.ContextLogger, tag string,
options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) {
// real Tailscale resolver implementation …
})
}
This design makes the DNS subsystem pluggable; new transports can be added by registering additional factories without modifying core logic.
Example Configuration
Consider this user configuration that creates two DNS servers with routing rules:
{
"dns": {
"servers": [
{
"tag": "cloudflare",
"type": "https",
"server": "https://1.1.1.1/dns-query",
"bootstrap": ["8.8.8.8", "8.8.4.4"]
},
{
"tag": "local",
"type": "local",
"address": "127.0.0.1",
"port": 53
}
],
"rules": [
{
"domain": [
"internal.local",
"corp.example.com"
],
"outbound": "local"
}
],
"final": "cloudflare"
}
}
During startup, the registry supplies factories for "https" and "local", the transport manager builds both instances, and the router applies the rule set above—routing internal domains to the local resolver and all other queries to Cloudflare.
Summary
- S-UI delegates all DNS functionality to sing-box rather than implementing custom resolution logic.
- The
DNSTransportRegistry()incore/register.goregisters factory functions for TCP, UDP, TLS, HTTPS, QUIC, DHCP, hosts-file, fake-IP, local, and optional Tailscale transports. dns.NewTransportManageranddns.NewRouterincore/box.goinstantiate the management layer and rule engine using the user-provided configuration.- The transport manager iterates through
dnsOptions.Serversto create concreteadapter.DNSTransportinstances via registered factories. - The router evaluates
dns.rulesto determine which transport handles each query, falling back todns.finalfor unmatched requests. - DNS configuration is defined in
service/config.gowithin theSingBoxConfigstruct and processed as raw JSON.
Frequently Asked Questions
Which DNS transports does S-UI support?
S-UI supports TCP, UDP, TLS (DoT), HTTPS (DoH), QUIC (DoQ), HTTP/3, DHCP, local system resolvers, hosts-file lookups, and fake-IP generation. Optional Tailscale DNS support is available when building with the with_tailscale tag. These transports are registered in core/register.go via their respective factory functions.
How does S-UI decide which DNS server to use for a query?
The Router (dns.NewRouter) evaluates rules defined in the configuration's dns.rules array. It matches queries based on domain patterns, geolocation, or other criteria, then routes to the tagged server. If no rule matches, it uses the default server specified in dns.final.
Where is the DNS configuration stored in S-UI?
The DNS configuration resides in the SingBoxConfig struct defined in service/config.go. The Dns field contains raw JSON that matches sing-box's schema, including server definitions, routing rules, and fallback settings. This configuration is passed directly to the sing-box core during initialization.
Can I add custom DNS transports to S-UI?
Yes, the registry pattern in core/register.go makes the DNS subsystem extensible. You can register new transport factories using dns.RegisterTransport with a unique type string and factory function. The optional Tailscale implementation demonstrates this pattern using build tags to conditionally compile additional transports.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →