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 Insecure(String),
216 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 Zones(Vec<ZoneSpec>),
229 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}