Skip to main content

modo/dns/
config.rs

1//! Configuration for the DNS verification module.
2
3use std::net::SocketAddr;
4
5use serde::Deserialize;
6
7use crate::error::{Error, Result};
8
9fn default_txt_prefix() -> String {
10    "_modo-verify".into()
11}
12
13fn default_timeout_ms() -> u64 {
14    5000
15}
16
17/// Configuration for [`super::DomainVerifier`].
18///
19/// Deserializes from YAML via `serde`. The `txt_prefix` and `timeout_ms`
20/// fields have defaults and can be omitted.
21///
22/// # Example (YAML)
23///
24/// ```yaml
25/// dns:
26///   nameserver: "8.8.8.8:53"
27///   txt_prefix: "_myapp-verify"   # default: _modo-verify
28///   timeout_ms: 5000              # default: 5000
29/// ```
30#[non_exhaustive]
31#[derive(Debug, Clone, Deserialize)]
32#[serde(default)]
33pub struct DnsConfig {
34    /// Nameserver address, with or without port. Port 53 is appended when omitted.
35    ///
36    /// Examples: `"8.8.8.8:53"`, `"1.1.1.1"`.
37    pub nameserver: String,
38    /// Prefix prepended to the domain when looking up TXT records.
39    ///
40    /// The resolved TXT lookup name is `{txt_prefix}.{domain}`.
41    /// Defaults to `"_modo-verify"`.
42    #[serde(default = "default_txt_prefix")]
43    pub txt_prefix: String,
44    /// UDP receive timeout in milliseconds. Defaults to `5000`.
45    #[serde(default = "default_timeout_ms")]
46    pub timeout_ms: u64,
47}
48
49impl Default for DnsConfig {
50    fn default() -> Self {
51        Self {
52            nameserver: "8.8.8.8".into(),
53            txt_prefix: "_modo-verify".into(),
54            timeout_ms: 5000,
55        }
56    }
57}
58
59impl DnsConfig {
60    /// Create a DNS configuration with the given nameserver address.
61    ///
62    /// Defaults: `txt_prefix = "_modo-verify"`, `timeout_ms = 5000`.
63    pub fn new(nameserver: impl Into<String>) -> Self {
64        Self {
65            nameserver: nameserver.into(),
66            txt_prefix: "_modo-verify".into(),
67            timeout_ms: 5000,
68        }
69    }
70
71    /// Parse `nameserver` into a [`SocketAddr`].
72    ///
73    /// If the address already contains a port it is used as-is; otherwise port
74    /// `53` is appended.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`crate::Error`] with status 500 when the address is not a
79    /// valid IP or hostname+port.
80    pub fn parse_nameserver(&self) -> Result<SocketAddr> {
81        if let Ok(addr) = self.nameserver.parse::<SocketAddr>() {
82            return Ok(addr);
83        }
84        let with_port = format!("{}:53", self.nameserver);
85        with_port.parse::<SocketAddr>().map_err(|_| {
86            Error::internal(format!(
87                "invalid dns nameserver address: {}",
88                self.nameserver
89            ))
90        })
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn parse_full_config() {
100        let yaml = r#"
101nameserver: "8.8.8.8:53"
102txt_prefix: "_myapp-verify"
103timeout_ms: 3000
104"#;
105        let config: DnsConfig = serde_yaml_ng::from_str(yaml).unwrap();
106        assert_eq!(config.nameserver, "8.8.8.8:53");
107        assert_eq!(config.txt_prefix, "_myapp-verify");
108        assert_eq!(config.timeout_ms, 3000);
109    }
110
111    #[test]
112    fn defaults_applied_when_fields_omitted() {
113        let yaml = r#"
114nameserver: "8.8.8.8"
115"#;
116        let config: DnsConfig = serde_yaml_ng::from_str(yaml).unwrap();
117        assert_eq!(config.nameserver, "8.8.8.8");
118        assert_eq!(config.txt_prefix, "_modo-verify");
119        assert_eq!(config.timeout_ms, 5000);
120    }
121
122    #[test]
123    fn parse_nameserver_with_port() {
124        let config = DnsConfig {
125            nameserver: "1.1.1.1:53".into(),
126            txt_prefix: "_modo-verify".into(),
127            timeout_ms: 5000,
128        };
129        let addr = config.parse_nameserver().unwrap();
130        assert_eq!(addr.to_string(), "1.1.1.1:53");
131    }
132
133    #[test]
134    fn parse_nameserver_without_port_appends_53() {
135        let config = DnsConfig {
136            nameserver: "8.8.8.8".into(),
137            txt_prefix: "_modo-verify".into(),
138            timeout_ms: 5000,
139        };
140        let addr = config.parse_nameserver().unwrap();
141        assert_eq!(addr.to_string(), "8.8.8.8:53");
142    }
143
144    #[test]
145    fn parse_nameserver_invalid_address_fails() {
146        let config = DnsConfig {
147            nameserver: "not-an-address".into(),
148            txt_prefix: "_modo-verify".into(),
149            timeout_ms: 5000,
150        };
151        let err = config.parse_nameserver().unwrap_err();
152        assert_eq!(err.status(), http::StatusCode::INTERNAL_SERVER_ERROR);
153    }
154}