Skip to main content

microsandbox_network/model/dns/nameserver/
parse.rs

1//! User-facing nameserver type and parsing.
2//!
3//! A nameserver can be configured by IP or hostname, with an optional port.
4//! Hostnames are resolved at interceptor startup using the host's own
5//! resolver — never the interceptor itself — so there is no bootstrap loop.
6
7use std::fmt;
8use std::net::{IpAddr, SocketAddr};
9use std::str::FromStr;
10
11use serde::{Deserialize, Serialize};
12
13/// Default DNS port (used when a spec omits `:PORT`).
14const DEFAULT_DNS_PORT: u16 = 53;
15
16/// A nameserver — either a literal address or a hostname to resolve later.
17///
18/// Serializes as a single string (`"1.1.1.1"`, `"1.1.1.1:53"`,
19/// `"dns.google"`, `"dns.google:53"`).
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Nameserver {
22    /// A literal socket address, ready to use.
23    Addr(SocketAddr),
24    /// A hostname + port to be resolved at startup via the host's resolver.
25    Host {
26        /// DNS name to resolve.
27        host: String,
28        /// UDP/TCP port to connect on.
29        port: u16,
30    },
31}
32
33impl fmt::Display for Nameserver {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Addr(sa) => write!(f, "{sa}"),
37            Self::Host { host, port } => write!(f, "{host}:{port}"),
38        }
39    }
40}
41
42/// Error returned when a user-supplied nameserver string can't be parsed.
43#[derive(Debug, thiserror::Error)]
44#[error("invalid nameserver {0:?}; expected IP, IP:PORT, HOST, or HOST:PORT")]
45pub struct ParseNameserverError(pub String);
46
47/// Parse a user-supplied nameserver string.
48///
49/// Accepted forms:
50/// - `1.1.1.1` — IPv4, port defaults to 53
51/// - `1.1.1.1:5353` — IPv4 with explicit port
52/// - `2606:4700:4700::1111` — IPv6 (bare)
53/// - `[2606:4700:4700::1111]:53` — IPv6 with port (brackets required)
54/// - `dns.google` — hostname, port defaults to 53
55/// - `dns.google:53` — hostname with port
56impl FromStr for Nameserver {
57    type Err = ParseNameserverError;
58
59    fn from_str(input: &str) -> Result<Self, Self::Err> {
60        let s = input.trim();
61        if s.is_empty() {
62            return Err(ParseNameserverError(input.to_owned()));
63        }
64
65        // IP:PORT or [IPv6]:PORT.
66        if let Ok(sa) = s.parse::<SocketAddr>() {
67            return Ok(Self::Addr(sa));
68        }
69
70        // Bare IPv4 / IPv6.
71        if let Ok(ip) = s.parse::<IpAddr>() {
72            return Ok(Self::Addr(SocketAddr::new(ip, DEFAULT_DNS_PORT)));
73        }
74
75        // HOST:PORT. `rsplit_once` so we don't get confused by port-less IPv6
76        // forms (those are handled above). Reject when the host segment would
77        // itself parse as an IPv6 address — that means the user wrote a bare
78        // v6 literal without brackets and the `:` is an IPv6 separator.
79        if let Some((host, port)) = s.rsplit_once(':')
80            && !host.is_empty()
81            && !host.contains(':')
82            && host.parse::<IpAddr>().is_err()
83            && let Ok(port) = port.parse::<u16>()
84        {
85            return Ok(Self::Host {
86                host: host.to_owned(),
87                port,
88            });
89        }
90
91        // Bare hostname. Reject anything with whitespace or characters that
92        // couldn't form a DNS label.
93        if !s.contains(char::is_whitespace) && !s.contains(':') {
94            return Ok(Self::Host {
95                host: s.to_owned(),
96                port: DEFAULT_DNS_PORT,
97            });
98        }
99
100        Err(ParseNameserverError(input.to_owned()))
101    }
102}
103
104// Serialize as a single string ("1.1.1.1:53" or "dns.google:53") so
105// config files stay flat and readable.
106impl Serialize for Nameserver {
107    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
108        s.collect_str(self)
109    }
110}
111
112impl<'de> Deserialize<'de> for Nameserver {
113    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
114        let s = String::deserialize(d)?;
115        s.parse().map_err(serde::de::Error::custom)
116    }
117}
118
119// Ergonomic conversions for Rust builder callers.
120impl From<SocketAddr> for Nameserver {
121    fn from(sa: SocketAddr) -> Self {
122        Self::Addr(sa)
123    }
124}
125
126impl From<IpAddr> for Nameserver {
127    fn from(ip: IpAddr) -> Self {
128        Self::Addr(SocketAddr::new(ip, DEFAULT_DNS_PORT))
129    }
130}
131
132//--------------------------------------------------------------------------------------------------
133// Tests
134//--------------------------------------------------------------------------------------------------
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    fn addr(s: &str) -> Nameserver {
141        Nameserver::Addr(s.parse().unwrap())
142    }
143
144    fn host(host: &str, port: u16) -> Nameserver {
145        Nameserver::Host {
146            host: host.to_owned(),
147            port,
148        }
149    }
150
151    #[test]
152    fn parses_ipv4_bare() {
153        assert_eq!("1.1.1.1".parse::<Nameserver>().unwrap(), addr("1.1.1.1:53"));
154    }
155
156    #[test]
157    fn parses_ipv4_with_port() {
158        assert_eq!(
159            "8.8.8.8:5353".parse::<Nameserver>().unwrap(),
160            addr("8.8.8.8:5353")
161        );
162    }
163
164    #[test]
165    fn parses_ipv6_bare() {
166        assert_eq!(
167            "2606:4700:4700::1111".parse::<Nameserver>().unwrap(),
168            addr("[2606:4700:4700::1111]:53")
169        );
170    }
171
172    #[test]
173    fn parses_ipv6_bracketed_with_port() {
174        assert_eq!(
175            "[2606:4700:4700::1111]:53".parse::<Nameserver>().unwrap(),
176            addr("[2606:4700:4700::1111]:53")
177        );
178    }
179
180    #[test]
181    fn parses_hostname_bare() {
182        assert_eq!(
183            "dns.google".parse::<Nameserver>().unwrap(),
184            host("dns.google", 53)
185        );
186    }
187
188    #[test]
189    fn parses_hostname_with_port() {
190        assert_eq!(
191            "dns.google:53".parse::<Nameserver>().unwrap(),
192            host("dns.google", 53)
193        );
194        assert_eq!(
195            "my-dns.corp.internal:5353".parse::<Nameserver>().unwrap(),
196            host("my-dns.corp.internal", 5353)
197        );
198    }
199
200    #[test]
201    fn trims_whitespace() {
202        assert_eq!(
203            "  1.1.1.1  ".parse::<Nameserver>().unwrap(),
204            addr("1.1.1.1:53")
205        );
206    }
207
208    #[test]
209    fn rejects_empty() {
210        assert!("".parse::<Nameserver>().is_err());
211        assert!("   ".parse::<Nameserver>().is_err());
212    }
213
214    #[test]
215    fn rejects_embedded_whitespace() {
216        assert!("dns google".parse::<Nameserver>().is_err());
217    }
218
219    #[test]
220    fn rejects_bad_port() {
221        assert!("dns.google:notaport".parse::<Nameserver>().is_err());
222        assert!("1.1.1.1:99999".parse::<Nameserver>().is_err());
223    }
224
225    #[test]
226    fn display_roundtrip() {
227        for s in ["1.1.1.1:53", "[2606:4700:4700::1111]:53", "dns.google:53"] {
228            let ns: Nameserver = s.parse().unwrap();
229            assert_eq!(ns.to_string(), s);
230        }
231    }
232
233    #[test]
234    fn display_feeds_back_into_parse() {
235        for s in ["1.1.1.1", "dns.google", "dns.google:53"] {
236            let ns: Nameserver = s.parse().unwrap();
237            // Display output round-trips to the same value via parse.
238            let reparsed: Nameserver = ns.to_string().parse().unwrap();
239            assert_eq!(ns, reparsed);
240        }
241    }
242}