mod_dns_resolver/
lib.rs

1use ::config::{any_err, get_or_create_sub_module, serialize_options, SerdeWrappedValue};
2use dns_resolver::{
3    get_resolver, ptr_host, resolve_a_or_aaaa, reverse_ip, AggregateResolver, HickoryResolver,
4    IpLookupStrategy, Resolver, TestResolver,
5};
6use kumo_address::host_or_socket::HostOrSocketAddress;
7use mailexchanger::{
8    set_mx_concurrency_limit, set_mx_negative_cache_ttl, set_mx_timeout,
9    set_mx_transient_negative_cache_ttl, MailExchanger,
10};
11use mlua::{Lua, LuaSerdeExt, Value};
12use parking_lot::Mutex;
13use std::collections::HashMap;
14use std::net::IpAddr;
15use std::str::FromStr;
16use std::sync::{Arc, LazyLock};
17use std::time::Duration;
18
19mod config;
20mod hickory_backend;
21mod resolv_conf_loader;
22mod unbound_backend;
23
24use crate::config::DnsResolverConfig;
25
26static RESOLVERS: LazyLock<Mutex<HashMap<String, Arc<Box<dyn Resolver>>>>> =
27    LazyLock::new(|| Mutex::new(HashMap::new()));
28
29pub fn get_resolver_instance(
30    opt_resolver_name: &Option<String>,
31) -> anyhow::Result<Arc<Box<dyn Resolver>>> {
32    if let Some(name) = opt_resolver_name {
33        return RESOLVERS
34            .lock()
35            .get(name)
36            .cloned()
37            .ok_or_else(|| anyhow::anyhow!("resolver {name} is not defined"));
38    }
39
40    Ok(get_resolver())
41}
42
43pub fn get_opt_resolver(
44    opt_resolver_name: &Option<String>,
45) -> anyhow::Result<Option<Arc<Box<dyn Resolver>>>> {
46    if let Some(name) = opt_resolver_name {
47        let r = RESOLVERS
48            .lock()
49            .get(name)
50            .cloned()
51            .ok_or_else(|| anyhow::anyhow!("resolver {name} is not defined"))?;
52        Ok(Some(r))
53    } else {
54        Ok(None)
55    }
56}
57
58pub fn register(lua: &Lua) -> anyhow::Result<()> {
59    let dns_mod = get_or_create_sub_module(lua, "dns")?;
60
61    dns_mod.set(
62        "lookup_mx",
63        lua.create_async_function(
64            |lua, (domain, opt_resolver_name): (String, Option<String>)| async move {
65                let opt_resolver = get_opt_resolver(&opt_resolver_name).map_err(any_err)?;
66                let mx = MailExchanger::resolve_via(&domain, opt_resolver.as_ref().map(|r| &***r))
67                    .await
68                    .map_err(any_err)?;
69                Ok(lua.to_value_with(&*mx, serialize_options()))
70            },
71        )?,
72    )?;
73
74    dns_mod.set(
75        "set_mx_concurrency_limit",
76        lua.create_function(move |_lua, limit: usize| {
77            set_mx_concurrency_limit(limit);
78            Ok(())
79        })?,
80    )?;
81
82    dns_mod.set(
83        "set_mx_timeout",
84        lua.create_function(move |lua, duration: Value| {
85            let duration: duration_serde::Wrap<Duration> = lua.from_value(duration)?;
86            set_mx_timeout(duration.into_inner()).map_err(any_err)
87        })?,
88    )?;
89
90    dns_mod.set(
91        "set_mx_negative_cache_ttl",
92        lua.create_function(move |lua, duration: Value| {
93            let duration: duration_serde::Wrap<Duration> = lua.from_value(duration)?;
94            set_mx_negative_cache_ttl(duration.into_inner()).map_err(any_err)
95        })?,
96    )?;
97
98    dns_mod.set(
99        "set_mx_transient_negative_cache_ttl",
100        lua.create_function(move |lua, duration: Value| {
101            let duration: duration_serde::Wrap<Duration> = lua.from_value(duration)?;
102            set_mx_transient_negative_cache_ttl(duration.into_inner()).map_err(any_err)
103        })?,
104    )?;
105
106    dns_mod.set(
107        "set_mta_sts_enabled",
108        lua.create_function(move |_lua, enabled: bool| {
109            mailexchanger::set_mta_sts_enabled(enabled);
110            Ok(())
111        })?,
112    )?;
113
114    dns_mod.set(
115        "set_dane_enabled",
116        lua.create_function(move |_lua, enabled: bool| {
117            mailexchanger::set_dane_enabled(enabled);
118            Ok(())
119        })?,
120    )?;
121
122    dns_mod.set(
123        "ptr_host",
124        lua.create_function(move |_lua, ip: String| {
125            let ip: IpAddr = ip.parse().map_err(any_err)?;
126            Ok(ptr_host(ip))
127        })?,
128    )?;
129
130    dns_mod.set(
131        "reverse_ip",
132        lua.create_function(move |_lua, ip: String| {
133            let ip: IpAddr = ip.parse().map_err(any_err)?;
134            Ok(reverse_ip(ip))
135        })?,
136    )?;
137
138    dns_mod.set(
139        "rbl_lookup",
140        lua.create_async_function(
141            |_lua, (ip_str, bl_domain, opt_resolver_name): (String, String, Option<String>)| async move {
142                let resolver = get_resolver_instance(&opt_resolver_name).map_err(any_err)?;
143
144                let address: HostOrSocketAddress = ip_str.parse().map_err(any_err)?;
145                let reversed_ip = reverse_ip(address.ip().ok_or_else(||mlua::Error::external(format!("{ip_str} is not a valid IpAddr or SocketAddr")))?);
146                let name = format!("{reversed_ip}.{bl_domain}.");
147
148                let answers = resolver.resolve_ip(&name).await.map_err(any_err)?;
149                match answers.first() {
150                    Some(ip) => {
151                        let txt = resolver.resolve_txt(&name).await.map(|a| a.as_txt().join("")).ok();
152                        Ok((Some(ip.to_string()), txt))
153                    }
154                    None => {
155                        Ok((None, None))
156                    }
157                }
158            },
159        )?,
160    )?;
161
162    dns_mod.set(
163        "lookup_ptr",
164        lua.create_async_function(
165            |lua, (ip_str, opt_resolver_name): (String, Option<String>)| async move {
166                let resolver = get_resolver_instance(&opt_resolver_name).map_err(any_err)?;
167                let addr = std::net::IpAddr::from_str(&ip_str).map_err(any_err)?;
168                let answer = resolver.resolve_ptr(addr).await.map_err(any_err)?;
169                Ok(lua.to_value_with(&*answer, serialize_options()))
170            },
171        )?,
172    )?;
173
174    dns_mod.set(
175        "lookup_txt",
176        lua.create_async_function(
177            |_lua, (domain, opt_resolver_name): (String, Option<String>)| async move {
178                let resolver = get_resolver_instance(&opt_resolver_name).map_err(any_err)?;
179                let answer = resolver.resolve_txt(&domain).await.map_err(any_err)?;
180                Ok(answer.as_txt())
181            },
182        )?,
183    )?;
184
185    dns_mod.set(
186        "lookup_addr",
187        lua.create_async_function(
188            |_lua,
189             (domain, opt_resolver_name, strategy): (
190                String,
191                Option<String>,
192                Option<SerdeWrappedValue<IpLookupStrategy>>,
193            )| async move {
194                let opt_resolver = get_opt_resolver(&opt_resolver_name).map_err(any_err)?;
195                let result = resolve_a_or_aaaa(
196                    &domain,
197                    opt_resolver.as_ref().map(|r| &***r),
198                    strategy.map(|v| v.0).unwrap_or_default(),
199                )
200                .await
201                .map_err(any_err)?;
202                let result: Vec<String> = result
203                    .into_iter()
204                    .map(|item| item.addr.to_string())
205                    .collect();
206                Ok(result)
207            },
208        )?,
209    )?;
210
211    #[derive(serde::Deserialize, Debug)]
212    #[serde(untagged)]
213    enum ZoneSpec {
214        /// An insecure (non-DNSSEC) zone.
215        Insecure(String),
216        /// A zone with an explicit DNSSEC secure flag.
217        Detailed {
218            zone: String,
219            #[serde(default)]
220            secure: bool,
221        },
222    }
223
224    #[derive(serde::Deserialize, Debug)]
225    #[serde(untagged)]
226    enum TestResolverConfig {
227        /// A bare list of zones (each an insecure string or `{zone, secure}`).
228        Zones(Vec<ZoneSpec>),
229        /// The full form, which additionally supports forcing SERVFAIL for a
230        /// set of owner names.
231        Detailed {
232            zones: Vec<ZoneSpec>,
233            #[serde(default)]
234            servfail: Vec<String>,
235        },
236    }
237
238    impl TestResolverConfig {
239        fn make_resolver(&self) -> anyhow::Result<TestResolver> {
240            let mut resolver = TestResolver::default();
241
242            let (zones, servfail): (&[ZoneSpec], &[String]) = match self {
243                Self::Zones(zones) => (zones, &[]),
244                Self::Detailed { zones, servfail } => (zones, servfail),
245            };
246
247            for zone in zones {
248                resolver = match zone {
249                    ZoneSpec::Insecure(zone) => resolver.with_zone(zone),
250                    ZoneSpec::Detailed { zone, secure: true } => resolver.with_secure_zone(zone),
251                    ZoneSpec::Detailed {
252                        zone,
253                        secure: false,
254                    } => resolver.with_zone(zone),
255                }
256                .map_err(|err| anyhow::anyhow!("{err}"))?;
257            }
258
259            for name in servfail {
260                resolver = resolver.with_servfail(name);
261            }
262
263            Ok(resolver)
264        }
265    }
266
267    #[derive(serde::Deserialize, Debug)]
268    enum KumoResolverConfig {
269        Hickory(DnsResolverConfig),
270        HickorySystemConfig,
271        Unbound(DnsResolverConfig),
272        Test(TestResolverConfig),
273        Aggregate(Vec<KumoResolverConfig>),
274    }
275
276    impl KumoResolverConfig {
277        fn make_resolver(&self, path: &str) -> anyhow::Result<Box<dyn Resolver>> {
278            match self {
279                Self::Hickory(config) => Ok(Box::new(
280                    hickory_backend::build_hickory_resolver(config)
281                        .map_err(|e| anyhow::anyhow!("{path}: {e}"))?,
282                )),
283                Self::HickorySystemConfig => Ok(Box::new(HickoryResolver::new()?)),
284                Self::Unbound(config) => Ok(Box::new(
285                    unbound_backend::build_unbound_resolver(config)
286                        .map_err(|e| anyhow::anyhow!("{path}: {e}"))?,
287                )),
288                Self::Test(config) => Ok(Box::new(config.make_resolver()?)),
289                Self::Aggregate(children) => {
290                    let mut resolver = AggregateResolver::new();
291                    for (idx, child) in children.iter().enumerate() {
292                        let child_path = format!("{path}.Aggregate[{idx}]");
293                        resolver.push_resolver(child.make_resolver(&child_path)?);
294                    }
295                    Ok(Box::new(resolver))
296                }
297            }
298        }
299    }
300
301    dns_mod.set(
302        "configure_resolver",
303        lua.create_function(move |lua, config: mlua::Value| {
304            match lua.from_value::<KumoResolverConfig>(config.clone()) {
305                Ok(config) => {
306                    let resolver = config
307                        .make_resolver("configure_resolver")
308                        .map_err(any_err)?;
309                    dns_resolver::reconfigure_resolver(resolver);
310                    Ok(())
311                }
312                Err(err1) => match lua.from_value::<DnsResolverConfig>(config) {
313                    Ok(config) => {
314                        let resolver = hickory_backend::build_hickory_resolver(&config)
315                            .map_err(any_err)?;
316                        dns_resolver::reconfigure_resolver(resolver);
317                        Ok(())
318                    }
319                    Err(err2) => Err(mlua::Error::external(format!(
320                        "failed to parse config as either KumoResolverConfig ({err1:#}) or DnsResolverConfig ({err2:#})"
321                    ))),
322                },
323            }
324        })?,
325    )?;
326
327    dns_mod.set(
328        "define_resolver",
329        lua.create_function(move |lua, (name, config): (String, mlua::Value)| {
330            let config = lua
331                .from_value::<KumoResolverConfig>(config.clone())
332                .map_err(any_err)?;
333            let path = format!("define_resolver({name:?})");
334            let resolver = config.make_resolver(&path).map_err(any_err)?;
335
336            RESOLVERS.lock().insert(name, resolver.into());
337
338            Ok(())
339        })?,
340    )?;
341
342    dns_mod.set(
343        "configure_unbound_resolver",
344        lua.create_function(move |lua, config: mlua::Value| {
345            tracing::error!(
346                "kumo.dns.configure_unbound_resolver is deprecated and will be \
347                 removed in a future release; use kumo.dns.configure_resolver instead"
348            );
349            let config: DnsResolverConfig = lua.from_value(config)?;
350            let resolver = unbound_backend::build_unbound_resolver(&config).map_err(any_err)?;
351            dns_resolver::reconfigure_resolver(resolver);
352            Ok(())
353        })?,
354    )?;
355
356    dns_mod.set(
357        "configure_test_resolver",
358        lua.create_function(move |lua, config: mlua::Value| {
359            let config = lua
360                .from_value::<TestResolverConfig>(config)
361                .map_err(any_err)?;
362            let resolver = config.make_resolver().map_err(any_err)?;
363            dns_resolver::reconfigure_resolver(resolver);
364            Ok(())
365        })?,
366    )?;
367
368    dns_mod.set(
369        "configure_test_mta_sts",
370        lua.create_function(
371            move |_lua, policies: std::collections::BTreeMap<String, String>| {
372                let parsed = policies
373                    .into_iter()
374                    .map(|(domain, text)| {
375                        let policy =
376                            mta_sts::policy::MtaStsPolicy::parse(&text).map_err(any_err)?;
377                        Ok((domain, policy))
378                    })
379                    .collect::<mlua::Result<std::collections::BTreeMap<_, _>>>()?;
380                mta_sts::set_test_policies(parsed);
381                Ok(())
382            },
383        )?,
384    )?;
385
386    dns_mod.set(
387        "load_resolv_conf",
388        lua.create_function(move |lua, path: Option<String>| {
389            let config = resolv_conf_loader::load_resolv_conf(path.as_deref()).map_err(any_err)?;
390            lua.to_value_with(&config, serialize_options())
391        })?,
392    )?;
393
394    Ok(())
395}