Skip to content

Config schema: listeners

This page lists the fields for the top-level listeners: block — dataplane ingress where clients send DNS queries. For the query path after a packet arrives, see Architecture and packet path — Receive. For a minimal runnable example, see Minimal configuration.

DNS listeners are separate from the optional gRPC control: listener — see Security and Reference: control.

listeners

Property Value
Type Mapping (object)
Required Yes for a runnable installation (at least one entry under listeners.listeners)
Location Top-level key in the config file

When the block is omitted entirely, Conduit applies defaults at parse time (threads: 1, reuse_port: false, empty socket buffer overrides, no bound addresses). An empty listeners.listeners list may pass validation but Conduit will not accept client DNS — see What makes a config runnable.

Block fields

FieldTypeRequiredDefaultDescription
threadsintegerno1Default number of ingress worker threads per listener entry below. Must be ≥ 1. Each thread is an ingress worker: under sync it runs the whole query pipeline on its thread; under split_io it accepts and hands off (see Runtime and concurrency). A listener entry may override this — see Per-listener overrides.
reuse_portbooleannofalseWhen true, UDP sockets use SO_REUSEPORT (Unix only) so multiple workers can bind the same address. Use true when threads > 1 on UDP. Ignored on non-Unix platforms and for TCP listeners.
rcvbufintegerno0 (OS default)When > 0, sets the UDP socket receive buffer size (bytes) before bind. 0 leaves the OS default. Applies to UDP listeners only.
sndbufintegerno0Reserved — accepted in YAML but not applied to sockets.
listenerslistyes (for DNS service)[]One or more listener objects. Conduit binds each entry at process start.

Worker count

Total ingress workers = the sum of each entry's resolved threads — the block threads for entries that don't override it (see Per-listener overrides). With no per-listener overrides this is simply threads × the number of entries in listeners.listeners.

Example — two UDP sockets and threads: 2 (no overrides) starts four worker threads (two per address). Under the sync runtime, each worker handles one query at a time through upstream wait; see Runtime and concurrency — Sync runtime.

reuse_port and threads

Client protocol threads reuse_port
UDP 1 false (default)
UDP > 1 true — required (config validate rejects otherwise)
TCP any N/A — TCP uses SO_REUSEADDR internally; reuse_port is ignored

Without reuse_port: true, a second UDP worker binding the same address cannot start. Conduit rejects that combination at config validate (and at load/apply) instead of failing later with a bind error.

Listener object

Each list entry under listeners.listeners is one bind address and protocol.

Field Type Required Default Description
address string yes — Client-facing socket address as host:port. IPv6 literals use bracket notation, for example [::1]:53. Must parse as a socket address; must not be empty.
protocol string yes — udp or tcp. Comparison is case-insensitive. Values other than tcp are treated as UDP.
threads integer no inherits block threads Per-entry ingress worker override for this listener only. Must be ≥ 1 when set.
reuse_port boolean no inherits block reuse_port Per-entry SO_REUSEPORT override (UDP, Unix only).
name string no derived from address Stable identity for this listener. When set, it becomes the listener metric label instead of the address. Must be unique across entries when set.
acls object no inherit top-level acls: Optional client ACL policy for this listener only. When set, fully replaces global ACL (same shape as Config schema: acls). When omitted, inherits top-level acls: (or admit-all).
rcvbuf integer no inherits block rcvbuf Per-entry UDP receive-buffer override in bytes; 0 keeps the OS default. UDP only.

sndbuf is a block-level reserved field only and is not applied to sockets (see Block fields).

Per-listener overrides and inheritance

threads, reuse_port, and rcvbuf on a listener entry are optional overrides of the block-level values. When a field is omitted, the entry inherits the block value; when it is set, the entry value wins for that listener only. This lets one block default cover most listeners while a specific entry tunes its own ingress.

Per-entry field When omitted When set
threads Inherits block threads Overrides for this entry; resolved value is forced to ≥ 1
reuse_port Inherits block reuse_port Overrides for this entry (UDP, Unix)
rcvbuf Inherits block rcvbuf Overrides for this entry (UDP); 0 = OS default
name listener metric label is the address name becomes the listener metric label; must be unique

Use a per-entry name to keep dashboards and logs stable when a listener's address changes, and per-entry threads to give a high-volume address more ingress workers than low-traffic listeners that share the block default.

Address format

Form Example Notes
IPv4 loopback "127.0.0.1:15353" Common in lab configs (high port avoids mDNS on 5353)
All interfaces "0.0.0.0:53" Requires privilege to bind port 53 on many systems
IPv6 "[::1]:15353" Brackets required around the literal

By default the address string is the listener label on built-in metrics such as conduit_queries_total (together with protocol: udp or tcp). Setting a per-listener name replaces the address as that label.

UDP vs TCP

UDP TCP
Wire format One datagram per query RFC 1035 length-prefixed messages per connection
Typical use Resolver traffic, high volume Clients that require TCP (large responses, +tcp in dig)
Socket tuning reuse_port, rcvbuf apply reuse_port and rcvbuf ignored
Upstream transport Controlled by forward.upstream_transport When forward.client_tcp_uses_upstream_tcp is true, TCP client queries can use upstream TCP

Conduit does not terminate DNS-over-TLS or DNS-over-HTTPS on these listeners — only plain UDP and TCP DNS.

Reload and restart

Listener sockets are opened at process start from the config present when conduit starts.

Change Stored in new snapshot? On-the-wire effect
pools, rules, orchestrator, … Yes — hot for new queries N/A (not listener fields)
listeners (addresses, threads, reuse_port, rcvbuf, name, per-listener overrides, entries) Yes — stored in new snapshot Restart required — existing sockets keep serving until restart

After a successful reload that changes listeners, Conduit logs pending (restart required) and continues on the previous bind until you restart the process. See Configuration model — Pending reconcile and Reload and export.

conduitctl validate does not bind sockets — bind failures (address in use, permission denied) appear at startup or after restart, not during validate.

Validation summary

Rule Error or outcome if violated
listeners.threads ≥ 1 listeners.threads must be >= 1
Per-listener threads ≥ 1 when set listener '<address>' threads must be >= 1
Per-listener name unique when set duplicate listener name '<name>'
Listener address non-empty listener address must not be empty
UDP resolved threads > 1 requires reuse_port: true listener '<name>' (UDP): threads is N but reuse_port is false; …
address parses as socket address Bind error at startup (not caught by validate)
Duplicate UDP entries on one address without reuse_port Bind error at startup (not caught by validate)

Validate with conduitctl validate --file … or load via the running process; see Config file.

Example configuration

Lab UDP listener with two workers (matches common test fixtures):

listeners:
  threads: 2
  reuse_port: true
  listeners:
    - address: "127.0.0.1:15353"
      protocol: udp

Dual-stack ingress — separate entries per address family:

listeners:
  threads: 1
  listeners:
    - address: "0.0.0.0:53"
      protocol: udp
    - address: "[::]:53"
      protocol: udp
    - address: "0.0.0.0:53"
      protocol: tcp

Optional receive buffer tuning for high-volume UDP:

listeners:
  threads: 4
  reuse_port: true
  rcvbuf: 4194304   # 4 MiB — only applied when > 0
  listeners:
    - address: "10.0.0.5:53"
      protocol: udp

Per-listener overrides — a named public listener with extra ingress threads alongside a low-traffic internal one that keeps the block default:

listeners:
  threads: 2          # block default for entries that don't override
  reuse_port: true
  listeners:
    - address: "0.0.0.0:53"
      protocol: udp
      name: public-udp
      threads: 8        # this entry overrides the block default
    - address: "127.0.0.1:53"
      protocol: udp
      name: internal-udp