# How S-UI Implements DNS Transport and Resolution: A Technical Deep Dive

> Discover how S-UI uses sing-box to implement DNS transport and resolution. Learn about transport factories, TransportManager, and domain-specific routing for efficient DNS handling.

- Repository: [Alireza Ahmadi/s-ui](https://github.com/alireza0/s-ui)
- Tags: deep-dive
- Published: 2026-05-22

---

**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:

1. **Register DNS transport factories** – `core/DNSTransportRegistry()` registers handlers for TCP, UDP, TLS, HTTPS, QUIC, DHCP, hosts-file, fake-IP, local, and optional Tailscale transports.
2. **Create a Transport Manager** – `dns.NewTransportManager` (instantiated in `core/Box.NewBox`) receives the registry and DNS options from the configuration.
3. **Instantiate each transport** – The manager iterates over `options.DNS.Servers` and calls the corresponding factory to build a concrete transport (e.g., a DoH client).
4. **Create a Router** – `dns.NewRouter` builds 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`](https://github.com/alireza0/s-ui/blob/main/core/register.go). The `DNSTransportRegistry()` function initializes a new registry and registers factory functions for each supported protocol:

```go
// 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`](https://github.com/alireza0/s-ui/blob/main/service/config.go). The `Dns` field contains the full DNS section, including servers, rules, and hosts, passed directly to sing-box:

```go
// 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`](https://github.com/alireza0/s-ui/blob/main/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:

```go
// 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:

```go
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:

1. The **Router** (`dns.Router`) receives the query.
2. It evaluates its rule set defined in `dns.rules` (domain-based actions, `default_domain_resolver`, etc.).
3. The router selects the appropriate **transport** (server) from the manager by tag.
4. 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`](https://github.com/alireza0/s-ui/blob/main/core/register_tailscale_transport.go) is replaced by a real implementation:

```go
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:

```json
{
  "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()` in [`core/register.go`](https://github.com/alireza0/s-ui/blob/main/core/register.go) registers factory functions for TCP, UDP, TLS, HTTPS, QUIC, DHCP, hosts-file, fake-IP, local, and optional Tailscale transports.
- `dns.NewTransportManager` and `dns.NewRouter` in [`core/box.go`](https://github.com/alireza0/s-ui/blob/main/core/box.go) instantiate the management layer and rule engine using the user-provided configuration.
- The transport manager iterates through `dnsOptions.Servers` to create concrete `adapter.DNSTransport` instances via registered factories.
- The router evaluates `dns.rules` to determine which transport handles each query, falling back to `dns.final` for unmatched requests.
- DNS configuration is defined in [`service/config.go`](https://github.com/alireza0/s-ui/blob/main/service/config.go) within the `SingBoxConfig` struct 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`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/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`](https://github.com/alireza0/s-ui/blob/main/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.