1use crate::upper::hosts::{HostMap, HostMapReloader};
14use crate::{NodeAddr, PeerIdentity};
15use simple_dns::rdata::{AAAA, RData};
16use simple_dns::{CLASS, Name, Packet, PacketFlag, QTYPE, RCODE, ResourceRecord, TYPE};
17use std::net::Ipv6Addr;
18use tracing::{debug, trace, warn};
19
20pub struct DnsResolvedIdentity {
22 pub node_addr: NodeAddr,
23 pub pubkey: secp256k1::PublicKey,
24}
25
26pub type DnsIdentityTx = tokio::sync::mpsc::Sender<DnsResolvedIdentity>;
28
29pub type DnsIdentityRx = tokio::sync::mpsc::Receiver<DnsResolvedIdentity>;
31
32fn extract_fips_label(name: &str) -> Option<&str> {
36 let name = name.strip_suffix('.').unwrap_or(name);
37 name.strip_suffix(".fips")
38 .or_else(|| name.strip_suffix(".FIPS"))
39 .or_else(|| {
40 let lower = name.to_ascii_lowercase();
41 if lower.ends_with(".fips") {
42 Some(&name[..name.len() - 5])
43 } else {
44 None
45 }
46 })
47}
48
49pub fn resolve_fips_query(name: &str) -> Option<(Ipv6Addr, NodeAddr, secp256k1::PublicKey)> {
54 let npub = extract_fips_label(name)?;
55 let peer = PeerIdentity::from_npub(npub).ok()?;
56 let ipv6 = peer.address().to_ipv6();
57 let node_addr = *peer.node_addr();
58 let pubkey = peer.pubkey_full();
59
60 Some((ipv6, node_addr, pubkey))
61}
62
63pub fn resolve_fips_query_with_hosts(
71 name: &str,
72 hosts: &HostMap,
73) -> Option<(Ipv6Addr, NodeAddr, secp256k1::PublicKey)> {
74 let label = extract_fips_label(name)?;
75
76 let npub_owned;
78 let npub = if let Some(mapped) = hosts.lookup_npub(label) {
79 npub_owned = mapped.to_string();
80 &npub_owned
81 } else {
82 label
83 };
84
85 let peer = PeerIdentity::from_npub(npub).ok()?;
86 let ipv6 = peer.address().to_ipv6();
87 let node_addr = *peer.node_addr();
88 let pubkey = peer.pubkey_full();
89
90 Some((ipv6, node_addr, pubkey))
91}
92
93pub fn handle_dns_packet(
99 query_bytes: &[u8],
100 ttl: u32,
101 hosts: &HostMap,
102) -> Option<(Vec<u8>, Option<DnsResolvedIdentity>)> {
103 let query = Packet::parse(query_bytes).ok()?;
104 let question = query.questions.first()?;
105
106 let qname = question.qname.to_string();
107 let is_aaaa = matches!(question.qtype, QTYPE::TYPE(TYPE::AAAA));
108
109 let mut response = query.into_reply();
110 response.set_flags(PacketFlag::AUTHORITATIVE_ANSWER);
111
112 if is_aaaa && let Some((ipv6, node_addr, pubkey)) = resolve_fips_query_with_hosts(&qname, hosts)
113 {
114 let name = Name::new_unchecked(&qname).into_owned();
115 let record = ResourceRecord::new(name, CLASS::IN, ttl, RData::AAAA(AAAA::from(ipv6)));
116 response.answers.push(record);
117
118 let identity = DnsResolvedIdentity { node_addr, pubkey };
119 let bytes = response.build_bytes_vec_compressed().ok()?;
120 return Some((bytes, Some(identity)));
121 }
122
123 if !is_aaaa && resolve_fips_query_with_hosts(&qname, hosts).is_some() {
127 let bytes = response.build_bytes_vec_compressed().ok()?;
128 return Some((bytes, None));
129 }
130
131 *response.rcode_mut() = RCODE::NameError;
133 let bytes = response.build_bytes_vec_compressed().ok()?;
134 Some((bytes, None))
135}
136
137fn is_mesh_interface_query(arrival_ifindex: Option<u32>, mesh_ifindex: Option<u32>) -> bool {
149 match (arrival_ifindex, mesh_ifindex) {
150 (Some(arrival), Some(mesh)) => arrival == mesh,
151 _ => false,
152 }
153}
154
155pub async fn run_dns_responder(
170 socket: tokio::net::UdpSocket,
171 identity_tx: DnsIdentityTx,
172 ttl: u32,
173 mut reloader: HostMapReloader,
174 mesh_ifindex: Option<u32>,
175) {
176 let mut buf = [0u8; 512]; loop {
179 let (len, src, arrival_ifindex) = match recv_with_pktinfo(&socket, &mut buf).await {
180 Ok(result) => result,
181 Err(e) => {
182 warn!(error = %e, "DNS socket recv error");
183 continue;
184 }
185 };
186
187 if is_mesh_interface_query(arrival_ifindex, mesh_ifindex) {
188 trace!(
189 src = %src,
190 ifindex = ?arrival_ifindex,
191 "DNS query arrived on mesh interface, dropping"
192 );
193 continue;
194 }
195
196 let query_bytes = &buf[..len];
197
198 reloader.check_reload();
200
201 match handle_dns_packet(query_bytes, ttl, reloader.hosts()) {
202 Some((response_bytes, identity)) => {
203 if let Some(id) = identity {
204 debug!(
205 node_addr = %id.node_addr,
206 "DNS resolved .fips name, registering identity"
207 );
208 let _ = identity_tx.send(id).await;
209 }
210
211 if let Err(e) = socket.send_to(&response_bytes, src).await {
212 debug!(error = %e, "DNS send error");
213 }
214 }
215 None => {
216 debug!(len, "Failed to parse DNS query, dropping");
217 }
218 }
219 }
220}
221
222#[cfg(unix)]
230async fn recv_with_pktinfo(
231 socket: &tokio::net::UdpSocket,
232 buf: &mut [u8],
233) -> std::io::Result<(usize, std::net::SocketAddr, Option<u32>)> {
234 use std::os::fd::AsRawFd;
235 loop {
236 socket.readable().await?;
237 let fd = socket.as_raw_fd();
238 match socket.try_io(tokio::io::Interest::READABLE, || {
239 recvmsg_with_pktinfo(fd, buf)
240 }) {
241 Ok(result) => return Ok(result),
242 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
243 Err(e) => return Err(e),
244 }
245 }
246}
247
248#[cfg(not(unix))]
249async fn recv_with_pktinfo(
250 socket: &tokio::net::UdpSocket,
251 buf: &mut [u8],
252) -> std::io::Result<(usize, std::net::SocketAddr, Option<u32>)> {
253 let (len, src) = socket.recv_from(buf).await?;
254 Ok((len, src, None))
255}
256
257#[cfg(unix)]
262fn recvmsg_with_pktinfo(
263 fd: std::os::fd::RawFd,
264 buf: &mut [u8],
265) -> std::io::Result<(usize, std::net::SocketAddr, Option<u32>)> {
266 let mut iov = libc::iovec {
267 iov_base: buf.as_mut_ptr() as *mut _,
268 iov_len: buf.len(),
269 };
270
271 let mut src_store: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
272 let mut cmsg_buf = [0u8; 128];
274
275 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
276 msg.msg_name = &mut src_store as *mut _ as *mut _;
277 msg.msg_namelen = std::mem::size_of::<libc::sockaddr_storage>() as u32;
278 msg.msg_iov = &mut iov;
279 msg.msg_iovlen = 1;
280 msg.msg_control = cmsg_buf.as_mut_ptr() as *mut _;
281 msg.msg_controllen = cmsg_buf.len() as _;
282
283 let n = unsafe { libc::recvmsg(fd, &mut msg, libc::MSG_DONTWAIT) };
284 if n < 0 {
285 return Err(std::io::Error::last_os_error());
286 }
287 let n = n as usize;
288
289 let src = sockaddr_storage_to_socket_addr(&src_store, msg.msg_namelen)?;
290 let ifindex = extract_pktinfo_ifindex(&msg);
291
292 Ok((n, src, ifindex))
293}
294
295#[cfg(unix)]
297fn extract_pktinfo_ifindex(msg: &libc::msghdr) -> Option<u32> {
298 let mut cmsg_ptr = unsafe { libc::CMSG_FIRSTHDR(msg) };
299 while !cmsg_ptr.is_null() {
300 let cmsg = unsafe { &*cmsg_ptr };
301 if cmsg.cmsg_level == libc::IPPROTO_IPV6 && cmsg.cmsg_type == libc::IPV6_PKTINFO {
302 let data_ptr = unsafe { libc::CMSG_DATA(cmsg_ptr) } as *const libc::in6_pktinfo;
303 let pktinfo: libc::in6_pktinfo = unsafe { std::ptr::read_unaligned(data_ptr) };
304 return Some(pktinfo.ipi6_ifindex as u32);
305 }
306 cmsg_ptr = unsafe { libc::CMSG_NXTHDR(msg, cmsg_ptr) };
307 }
308 None
309}
310
311#[cfg(unix)]
313fn sockaddr_storage_to_socket_addr(
314 storage: &libc::sockaddr_storage,
315 len: libc::socklen_t,
316) -> std::io::Result<std::net::SocketAddr> {
317 match storage.ss_family as i32 {
318 libc::AF_INET => {
319 if (len as usize) < std::mem::size_of::<libc::sockaddr_in>() {
320 return Err(std::io::Error::new(
321 std::io::ErrorKind::InvalidData,
322 "sockaddr_in too small",
323 ));
324 }
325 let sin = unsafe { &*(storage as *const _ as *const libc::sockaddr_in) };
326 let ip = std::net::Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr));
327 let port = u16::from_be(sin.sin_port);
328 Ok(std::net::SocketAddr::V4(std::net::SocketAddrV4::new(
329 ip, port,
330 )))
331 }
332 libc::AF_INET6 => {
333 if (len as usize) < std::mem::size_of::<libc::sockaddr_in6>() {
334 return Err(std::io::Error::new(
335 std::io::ErrorKind::InvalidData,
336 "sockaddr_in6 too small",
337 ));
338 }
339 let sin6 = unsafe { &*(storage as *const _ as *const libc::sockaddr_in6) };
340 let ip = std::net::Ipv6Addr::from(sin6.sin6_addr.s6_addr);
341 let port = u16::from_be(sin6.sin6_port);
342 Ok(std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
343 ip,
344 port,
345 sin6.sin6_flowinfo,
346 sin6.sin6_scope_id,
347 )))
348 }
349 af => Err(std::io::Error::new(
350 std::io::ErrorKind::InvalidData,
351 format!("unexpected address family: {}", af),
352 )),
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::Identity;
360
361 #[test]
362 fn test_resolve_valid_npub() {
363 let identity = Identity::generate();
364 let npub = identity.npub();
365 let expected_ipv6 = identity.address().to_ipv6();
366
367 let query = format!("{}.fips", npub);
368 let result = resolve_fips_query(&query);
369
370 assert!(result.is_some(), "should resolve valid npub.fips");
371 let (ipv6, node_addr, _pubkey) = result.unwrap();
372 assert_eq!(ipv6, expected_ipv6);
373 assert_eq!(node_addr, *identity.node_addr());
374 }
375
376 #[test]
377 fn test_resolve_trailing_dot() {
378 let identity = Identity::generate();
379 let npub = identity.npub();
380 let expected_ipv6 = identity.address().to_ipv6();
381
382 let query = format!("{}.fips.", npub);
383 let result = resolve_fips_query(&query);
384
385 assert!(result.is_some(), "should handle trailing dot");
386 let (ipv6, _, _) = result.unwrap();
387 assert_eq!(ipv6, expected_ipv6);
388 }
389
390 #[test]
391 fn test_resolve_case_insensitive() {
392 let identity = Identity::generate();
393 let npub = identity.npub();
394
395 let result = resolve_fips_query(&format!("{}.FIPS", npub));
397 assert!(result.is_some(), "should handle .FIPS");
398
399 let result = resolve_fips_query(&format!("{}.Fips", npub));
401 assert!(result.is_some(), "should handle .Fips");
402 }
403
404 #[test]
405 fn test_resolve_invalid_npub() {
406 let result = resolve_fips_query("not-a-valid-npub.fips");
407 assert!(result.is_none());
408 }
409
410 #[test]
411 fn test_resolve_wrong_suffix() {
412 let identity = Identity::generate();
413 let npub = identity.npub();
414
415 let result = resolve_fips_query(&format!("{}.com", npub));
416 assert!(result.is_none());
417 }
418
419 #[test]
420 fn test_resolve_empty_name() {
421 assert!(resolve_fips_query("").is_none());
422 assert!(resolve_fips_query(".fips").is_none());
423 assert!(resolve_fips_query("fips").is_none());
424 }
425
426 #[test]
429 fn test_resolve_hostname_via_hosts() {
430 let identity = Identity::generate();
431 let expected_ipv6 = identity.address().to_ipv6();
432
433 let mut hosts = HostMap::new();
434 hosts.insert("gateway", &identity.npub()).unwrap();
435
436 let result = resolve_fips_query_with_hosts("gateway.fips", &hosts);
437 assert!(result.is_some(), "should resolve hostname via host map");
438 let (ipv6, node_addr, _) = result.unwrap();
439 assert_eq!(ipv6, expected_ipv6);
440 assert_eq!(node_addr, *identity.node_addr());
441 }
442
443 #[test]
444 fn test_resolve_hostname_case_insensitive() {
445 let identity = Identity::generate();
446
447 let mut hosts = HostMap::new();
448 hosts.insert("gateway", &identity.npub()).unwrap();
449
450 assert!(resolve_fips_query_with_hosts("Gateway.FIPS", &hosts).is_some());
451 assert!(resolve_fips_query_with_hosts("GATEWAY.fips", &hosts).is_some());
452 }
453
454 #[test]
455 fn test_resolve_hostname_trailing_dot() {
456 let identity = Identity::generate();
457
458 let mut hosts = HostMap::new();
459 hosts.insert("gateway", &identity.npub()).unwrap();
460
461 assert!(resolve_fips_query_with_hosts("gateway.fips.", &hosts).is_some());
462 }
463
464 #[test]
465 fn test_resolve_npub_with_empty_hosts() {
466 let identity = Identity::generate();
467 let expected_ipv6 = identity.address().to_ipv6();
468 let hosts = HostMap::new();
469
470 let query = format!("{}.fips", identity.npub());
471 let result = resolve_fips_query_with_hosts(&query, &hosts);
472 assert!(result.is_some(), "should fall through to npub resolution");
473 let (ipv6, _, _) = result.unwrap();
474 assert_eq!(ipv6, expected_ipv6);
475 }
476
477 #[test]
478 fn test_resolve_unknown_hostname_returns_none() {
479 let hosts = HostMap::new();
480 assert!(resolve_fips_query_with_hosts("unknown.fips", &hosts).is_none());
481 }
482
483 #[test]
486 fn test_handle_aaaa_query() {
487 let identity = Identity::generate();
488 let npub = identity.npub();
489 let expected_ipv6 = identity.address().to_ipv6();
490 let hosts = HostMap::new();
491
492 let query_name = format!("{}.fips", npub);
493 let query_packet = build_test_query(&query_name, TYPE::AAAA);
494
495 let result = handle_dns_packet(&query_packet, 300, &hosts);
496 assert!(result.is_some(), "should handle AAAA query");
497
498 let (response_bytes, identity_opt) = result.unwrap();
499 assert!(identity_opt.is_some(), "should produce identity");
500
501 let response = Packet::parse(&response_bytes).unwrap();
502 assert_eq!(response.answers.len(), 1);
503
504 if let RData::AAAA(aaaa) = &response.answers[0].rdata {
505 let addr = Ipv6Addr::from(aaaa.address);
506 assert_eq!(addr, expected_ipv6);
507 } else {
508 panic!("expected AAAA record");
509 }
510 }
511
512 #[test]
513 fn test_handle_aaaa_query_hostname() {
514 let identity = Identity::generate();
515 let expected_ipv6 = identity.address().to_ipv6();
516
517 let mut hosts = HostMap::new();
518 hosts.insert("gateway", &identity.npub()).unwrap();
519
520 let query_packet = build_test_query("gateway.fips", TYPE::AAAA);
521
522 let result = handle_dns_packet(&query_packet, 300, &hosts);
523 assert!(result.is_some(), "should handle hostname AAAA query");
524
525 let (response_bytes, identity_opt) = result.unwrap();
526 assert!(
527 identity_opt.is_some(),
528 "should produce identity for hostname"
529 );
530
531 let response = Packet::parse(&response_bytes).unwrap();
532 assert_eq!(response.answers.len(), 1);
533
534 if let RData::AAAA(aaaa) = &response.answers[0].rdata {
535 assert_eq!(Ipv6Addr::from(aaaa.address), expected_ipv6);
536 } else {
537 panic!("expected AAAA record");
538 }
539 }
540
541 #[test]
542 fn test_handle_nxdomain_for_unknown() {
543 let hosts = HostMap::new();
544 let query_packet = build_test_query("unknown.fips", TYPE::AAAA);
545
546 let result = handle_dns_packet(&query_packet, 300, &hosts);
547 assert!(result.is_some());
548
549 let (response_bytes, identity_opt) = result.unwrap();
550 assert!(
551 identity_opt.is_none(),
552 "should not produce identity for unknown"
553 );
554
555 let response = Packet::parse(&response_bytes).unwrap();
556 assert_eq!(response.rcode(), RCODE::NameError);
557 assert!(response.answers.is_empty());
558 }
559
560 #[test]
561 fn test_handle_non_aaaa_query() {
562 let identity = Identity::generate();
563 let hosts = HostMap::new();
564 let query_name = format!("{}.fips", identity.npub());
565 let query_packet = build_test_query(&query_name, TYPE::A);
566
567 let result = handle_dns_packet(&query_packet, 300, &hosts);
568 assert!(result.is_some());
569
570 let (response_bytes, identity_opt) = result.unwrap();
571 assert!(identity_opt.is_none(), "A query should not resolve .fips");
572
573 let response = Packet::parse(&response_bytes).unwrap();
576 assert_eq!(response.rcode(), RCODE::NoError);
577 assert!(response.answers.is_empty());
578 }
579
580 #[tokio::test]
581 async fn test_dns_responder_udp() {
582 let identity = Identity::generate();
583 let npub = identity.npub();
584 let expected_ipv6 = identity.address().to_ipv6();
585
586 let reloader = HostMapReloader::new(
588 HostMap::new(),
589 std::path::PathBuf::from("/nonexistent/hosts"),
590 );
591
592 let server_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
594 let server_addr = server_socket.local_addr().unwrap();
595
596 let (identity_tx, mut identity_rx) = tokio::sync::mpsc::channel(16);
597
598 let responder_handle = tokio::spawn(run_dns_responder(
600 server_socket,
601 identity_tx,
602 300,
603 reloader,
604 None,
605 ));
606
607 let client_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
609 let query = build_test_query(&format!("{}.fips", npub), TYPE::AAAA);
610 client_socket.send_to(&query, server_addr).await.unwrap();
611
612 let mut buf = [0u8; 512];
614 let (len, _) = tokio::time::timeout(
615 std::time::Duration::from_secs(2),
616 client_socket.recv_from(&mut buf),
617 )
618 .await
619 .unwrap()
620 .unwrap();
621
622 let response = Packet::parse(&buf[..len]).unwrap();
623 assert_eq!(response.answers.len(), 1);
624 if let RData::AAAA(aaaa) = &response.answers[0].rdata {
625 assert_eq!(Ipv6Addr::from(aaaa.address), expected_ipv6);
626 } else {
627 panic!("expected AAAA record");
628 }
629
630 let resolved = tokio::time::timeout(std::time::Duration::from_secs(1), identity_rx.recv())
632 .await
633 .unwrap()
634 .unwrap();
635 assert_eq!(resolved.node_addr, *identity.node_addr());
636
637 responder_handle.abort();
638 }
639
640 #[tokio::test]
641 async fn test_dns_responder_with_hosts() {
642 let identity = Identity::generate();
643 let expected_ipv6 = identity.address().to_ipv6();
644
645 let dir = tempfile::tempdir().unwrap();
647 let hosts_path = dir.path().join("hosts");
648 std::fs::write(&hosts_path, format!("gateway {}\n", identity.npub())).unwrap();
649
650 let reloader = HostMapReloader::new(HostMap::new(), hosts_path);
651
652 let server_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
653 let server_addr = server_socket.local_addr().unwrap();
654
655 let (identity_tx, mut identity_rx) = tokio::sync::mpsc::channel(16);
656
657 let responder_handle = tokio::spawn(run_dns_responder(
658 server_socket,
659 identity_tx,
660 300,
661 reloader,
662 None,
663 ));
664
665 let client_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
667 let query = build_test_query("gateway.fips", TYPE::AAAA);
668 client_socket.send_to(&query, server_addr).await.unwrap();
669
670 let mut buf = [0u8; 512];
671 let (len, _) = tokio::time::timeout(
672 std::time::Duration::from_secs(2),
673 client_socket.recv_from(&mut buf),
674 )
675 .await
676 .unwrap()
677 .unwrap();
678
679 let response = Packet::parse(&buf[..len]).unwrap();
680 assert_eq!(response.answers.len(), 1);
681 if let RData::AAAA(aaaa) = &response.answers[0].rdata {
682 assert_eq!(Ipv6Addr::from(aaaa.address), expected_ipv6);
683 } else {
684 panic!("expected AAAA record");
685 }
686
687 let resolved = tokio::time::timeout(std::time::Duration::from_secs(1), identity_rx.recv())
689 .await
690 .unwrap()
691 .unwrap();
692 assert_eq!(resolved.node_addr, *identity.node_addr());
693
694 responder_handle.abort();
695 }
696
697 #[tokio::test]
698 async fn test_dns_responder_auto_reload() {
699 let id1 = Identity::generate();
700 let id2 = Identity::generate();
701 let expected_ipv6_2 = id2.address().to_ipv6();
702
703 let dir = tempfile::tempdir().unwrap();
705 let hosts_path = dir.path().join("hosts");
706 std::fs::write(&hosts_path, format!("gateway {}\n", id1.npub())).unwrap();
707
708 let reloader = HostMapReloader::new(HostMap::new(), hosts_path.clone());
709
710 let server_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
711 let server_addr = server_socket.local_addr().unwrap();
712 let (identity_tx, _identity_rx) = tokio::sync::mpsc::channel(16);
713
714 let responder_handle = tokio::spawn(run_dns_responder(
715 server_socket,
716 identity_tx,
717 300,
718 reloader,
719 None,
720 ));
721
722 let client_socket = tokio::net::UdpSocket::bind("127.0.0.1:0").await.unwrap();
723
724 let query = build_test_query("server2.fips", TYPE::AAAA);
726 client_socket.send_to(&query, server_addr).await.unwrap();
727 let mut buf = [0u8; 512];
728 let (len, _) = tokio::time::timeout(
729 std::time::Duration::from_secs(2),
730 client_socket.recv_from(&mut buf),
731 )
732 .await
733 .unwrap()
734 .unwrap();
735 let response = Packet::parse(&buf[..len]).unwrap();
736 assert!(
737 response.answers.is_empty(),
738 "server2 should not resolve before reload"
739 );
740
741 std::thread::sleep(std::time::Duration::from_millis(50));
743 std::fs::write(
744 &hosts_path,
745 format!("gateway {}\nserver2 {}\n", id1.npub(), id2.npub()),
746 )
747 .unwrap();
748
749 let query = build_test_query("server2.fips", TYPE::AAAA);
751 client_socket.send_to(&query, server_addr).await.unwrap();
752 let (len, _) = tokio::time::timeout(
753 std::time::Duration::from_secs(2),
754 client_socket.recv_from(&mut buf),
755 )
756 .await
757 .unwrap()
758 .unwrap();
759 let response = Packet::parse(&buf[..len]).unwrap();
760 assert_eq!(
761 response.answers.len(),
762 1,
763 "server2 should resolve after reload"
764 );
765 if let RData::AAAA(aaaa) = &response.answers[0].rdata {
766 assert_eq!(Ipv6Addr::from(aaaa.address), expected_ipv6_2);
767 } else {
768 panic!("expected AAAA record");
769 }
770
771 responder_handle.abort();
772 }
773
774 #[test]
777 fn test_is_mesh_interface_query_matching() {
778 assert!(
779 is_mesh_interface_query(Some(7), Some(7)),
780 "arrival == mesh ifindex should drop"
781 );
782 }
783
784 #[test]
785 fn test_is_mesh_interface_query_non_matching() {
786 assert!(
787 !is_mesh_interface_query(Some(1), Some(7)),
788 "lo arrival should pass when mesh is fips0"
789 );
790 }
791
792 #[test]
793 fn test_is_mesh_interface_query_no_arrival() {
794 assert!(
795 !is_mesh_interface_query(None, Some(7)),
796 "unknown arrival (no PKTINFO cmsg) should fail-open"
797 );
798 }
799
800 #[test]
801 fn test_is_mesh_interface_query_no_filter() {
802 assert!(
803 !is_mesh_interface_query(Some(7), None),
804 "unconfigured mesh ifindex disables the filter"
805 );
806 }
807
808 #[cfg(unix)]
811 fn loopback_ifindex_for_test() -> u32 {
812 let name = if cfg!(target_os = "macos") {
813 "lo0"
814 } else {
815 "lo"
816 };
817 let c = std::ffi::CString::new(name).unwrap();
818 unsafe { libc::if_nametoindex(c.as_ptr()) }
819 }
820
821 #[cfg(unix)]
824 fn bind_loopback_v6_with_pktinfo() -> tokio::net::UdpSocket {
825 use socket2::{Domain, Protocol, Socket, Type};
826 use std::os::fd::AsRawFd;
827 let sock = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::UDP)).unwrap();
828 sock.set_only_v6(false).unwrap();
829 let enable: libc::c_int = 1;
830 let ret = unsafe {
831 libc::setsockopt(
832 sock.as_raw_fd(),
833 libc::IPPROTO_IPV6,
834 libc::IPV6_RECVPKTINFO,
835 &enable as *const _ as *const libc::c_void,
836 std::mem::size_of::<libc::c_int>() as libc::socklen_t,
837 )
838 };
839 assert_eq!(ret, 0, "setsockopt IPV6_RECVPKTINFO failed");
840 sock.set_nonblocking(true).unwrap();
841 let addr: std::net::SocketAddr = "[::1]:0".parse().unwrap();
842 sock.bind(&addr.into()).unwrap();
843 tokio::net::UdpSocket::from_std(sock.into()).unwrap()
844 }
845
846 #[cfg(unix)]
847 #[tokio::test]
848 async fn test_recv_with_pktinfo_returns_loopback_ifindex() {
849 let lo = loopback_ifindex_for_test();
850 if lo == 0 {
851 return;
854 }
855
856 let server = bind_loopback_v6_with_pktinfo();
857 let server_addr = server.local_addr().unwrap();
858
859 let client = tokio::net::UdpSocket::bind("[::1]:0").await.unwrap();
860 client.send_to(b"hello", server_addr).await.unwrap();
861
862 let mut buf = [0u8; 32];
863 let (len, src, ifindex) = tokio::time::timeout(
864 std::time::Duration::from_secs(2),
865 recv_with_pktinfo(&server, &mut buf),
866 )
867 .await
868 .unwrap()
869 .unwrap();
870
871 assert_eq!(&buf[..len], b"hello");
872 assert!(src.ip().is_loopback(), "source should be loopback");
873 assert_eq!(
874 ifindex,
875 Some(lo),
876 "IPV6_PKTINFO should report loopback ifindex"
877 );
878 }
879
880 #[cfg(unix)]
881 #[tokio::test]
882 async fn test_dns_responder_drops_mesh_interface_query() {
883 let lo = loopback_ifindex_for_test();
884 if lo == 0 {
885 return;
886 }
887
888 let server_socket = bind_loopback_v6_with_pktinfo();
889 let server_addr = server_socket.local_addr().unwrap();
890
891 let reloader = HostMapReloader::new(
892 HostMap::new(),
893 std::path::PathBuf::from("/nonexistent/hosts"),
894 );
895 let (identity_tx, _identity_rx) = tokio::sync::mpsc::channel(16);
896
897 let responder_handle = tokio::spawn(run_dns_responder(
901 server_socket,
902 identity_tx,
903 300,
904 reloader,
905 Some(lo),
906 ));
907
908 let identity = Identity::generate();
909 let query = build_test_query(&format!("{}.fips", identity.npub()), TYPE::AAAA);
910 let client = tokio::net::UdpSocket::bind("[::1]:0").await.unwrap();
911 client.send_to(&query, server_addr).await.unwrap();
912
913 let mut buf = [0u8; 512];
914 let result = tokio::time::timeout(
915 std::time::Duration::from_millis(300),
916 client.recv_from(&mut buf),
917 )
918 .await;
919
920 assert!(
921 result.is_err(),
922 "response arrived from server ({:?}) — filter did not drop mesh-interface query",
923 result
924 );
925
926 responder_handle.abort();
927 }
928
929 fn build_test_query(name: &str, rtype: TYPE) -> Vec<u8> {
931 use simple_dns::Question;
932
933 let mut packet = Packet::new_query(0x1234);
934 let question = Question::new(
935 Name::new_unchecked(name).into_owned(),
936 QTYPE::TYPE(rtype),
937 simple_dns::QCLASS::CLASS(CLASS::IN),
938 false,
939 );
940 packet.questions.push(question);
941 packet.build_bytes_vec().unwrap()
942 }
943}