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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
threads | integer | no | 1 | Default 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_port | boolean | no | false | When 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. |
rcvbuf | integer | no | 0 (OS default) | When > 0, sets the UDP socket receive buffer size (bytes) before bind. 0 leaves the OS default. Applies to UDP listeners only. |
sndbuf | integer | no | 0 | Reserved — accepted in YAML but not applied to sockets. |
listeners | list | yes (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
Related topics
- Minimal configuration — smallest
listeners+poolsexample - First query — test with
digafter bind - Architecture and packet path — Receive phase
- Runtime and concurrency — ingress workers, runtime models, slot pool
- Configuration model — snapshot updates and restart-required changes
- Built-in metrics —
listenerandprotocollabels - Security — dataplane listeners vs control-plane gRPC
- Config schema overview