stalwartlabs/stalwart · critical

Failed to build DNS resolver

Error message

Failed to build DNS resolver

What it means

This panic occurs when the standard (non-DNSSEC) hickory resolver wrapped in MessageAuthenticator cannot be built during resolver initialization. MessageAuthenticator::new(config, opts) uses .expect("Failed to build DNS resolver"), so any invalid resolver configuration aborts instead of returning an error. It indicates the ResolverConfig or ResolverOpts supplied for plain DNS are invalid.

Source

Thrown at crates/common/src/config/smtp/resolver.rs:348

    }
}

impl Default for Resolvers {
    fn default() -> Self {
        let (config, opts) = match read_system_conf() {
            Ok(conf) => conf,
            Err(_) => (
                ResolverConfig::udp_and_tcp(&CLOUDFLARE),
                ResolverOpts::default(),
            ),
        };

        let config_dnssec = config.clone();
        let mut opts_dnssec = opts.clone();
        opts_dnssec.validate = true;

        Self {
            dns: MessageAuthenticator::new(config, opts).expect("Failed to build DNS resolver"),
            dnssec: DnssecResolver {
                resolver: TokioResolver::builder_with_config(
                    config_dnssec,
                    TokioRuntimeProvider::default(),
                )
                .with_options(opts_dnssec)
                .build()
                .expect("Failed to build DNSSEC resolver"),
            },
            dnssec_available: true,
        }
    }
}

impl Display for Policy {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("version: STSv1\r\n")?;
        f.write_str("mode: ")?;

View on GitHub (pinned to e962003857)

Solutions

  1. Validate the nameserver IPs/ports and resolver options in the smtp.resolver config section
  2. Revert to default resolver options (remove custom timeout/cache/protocol overrides) to isolate the offending value
  3. Test resolution with a minimal config using system resolvers
  4. Check hickory-resolver changelogs after upgrades for option semantics changes
  5. Replace .expect() with proper error propagation so startup reports the underlying cause

Example fix

// before
MessageAuthenticator::new(config, opts).expect("Failed to build DNS resolver"),
// after
MessageAuthenticator::new(config, opts)
    .map_err(|e| anyhow!("Failed to build DNS resolver: {e}"))?,
Defensive patterns

Strategy: validation

Validate before calling

// sanity-check the plain DNS config before constructing the resolver
fn validate_dns_options(opts: &ResolverOpts) -> Result<(), String> {
    if opts.timeout.is_zero() {
        return Err("resolver timeout must be > 0".into());
    }
    Ok(())
}

Try / catch

// The constructor panics via expect; guard at startup:
let resolvers = std::panic::catch_unwind(|| build_resolvers(&config, &opts))
    .map_err(|_| anyhow!("DNS resolver init panicked — check resolver config"))?;

Prevention

When it happens

Trigger: Constructing the resolver struct where MessageAuthenticator::new(config, opts) fails — a ResolverConfig with malformed nameserver addresses/ports, or ResolverOpts values hickory rejects at build time (invalid timeouts, cache sizes, or protocol settings).

Common situations: Typo'd or malformed nameserver IP in the smtp.resolver config section; invalid resolver options (protocol, timeout, cache) in configuration; hickory version upgrades changing accepted option semantics; corrupted config values loaded from TOML.

Related errors


AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06). Data as JSON: /api/errors/5992f8b1eee2e87d. Report an issue: GitHub.