Files
Lennon Day-Reynolds 4568c9b390 WIP: Update/clean up dependencies, move API crates into repo, replace Docker w/Nix
This does a number of things which _could_ be separated, so I consider this more or
less experimental.

Implied in the removal of the various dockerfiles is the expectation that production
deployments of ZeroNSD will simply use a Docker container, rather than distro-native
packages.

TODO: automate publishing of containers from Nix-generated image
2024-10-14 09:30:07 -07:00

773 lines
23 KiB
Rust

use zeronsd::utils::init_logger;
#[cfg(feature = "integration-tests")]
mod service;
#[ctor::ctor]
fn init() {
init_logger(Some(tracing::Level::ERROR));
}
#[cfg(feature = "integration-tests")]
mod sixplane {
use std::{net::IpAddr, path::Path, str::FromStr, time::Duration};
use rand::prelude::SliceRandom;
use tracing::info;
use trust_dns_resolver::{IntoName, Name};
use crate::service::{
resolver::Lookup, to_ip::ToIPv6Vec, utils::HostsType, Service, ServiceConfig,
};
use zeronsd::{addresses::Calculator, hosts::parse_hosts};
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_single_domain() {
let service = Service::new(ServiceConfig::default().network_filename("6plane-only")).await;
let record = service.member_record();
info!("Looking up {}", record);
let mut listen_ips = service.listen_ips.clone();
listen_ips.sort();
for _ in 0..1000 {
let mut ips = service.lookup_aaaa(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv6_vec());
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_single_domain_named() {
let update_interval = Duration::new(2, 0);
let service = Service::new(
ServiceConfig::default()
.update_interval(Some(update_interval))
.network_filename("6plane-only"),
)
.await;
let member_record = service.member_record();
service.change_name("islay").await;
let named_record = "islay.home.arpa.".to_string();
for record in vec![member_record, named_record.clone()] {
info!("Looking up {}", record);
let mut listen_ips = service.listen_ips.clone();
listen_ips.sort();
for _ in 0..1000 {
let mut ips = service.lookup_aaaa(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv6_vec());
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_multi_domain_hosts_file() {
let service = Service::new(
ServiceConfig::default()
.hosts(HostsType::Fixture("basic-ipv6"))
.network_filename("6plane-only"),
)
.await;
let record = service.member_record();
info!("Looking up random domains");
let mut hosts_map = parse_hosts(
Some(
Path::new(&format!("{}/basic-ipv6", zeronsd::utils::TEST_HOSTS_DIR)).to_path_buf(),
),
"home.arpa.".into_name().unwrap(),
)
.unwrap();
let ip = service.test_network().member().sixplane().unwrap().ip();
hosts_map.insert(ip, vec![record.clone().into_name().unwrap()]);
let mut hosts = hosts_map.values().flatten().collect::<Vec<&Name>>();
for _ in 0..1000 {
hosts.shuffle(&mut rand::thread_rng());
let host = *hosts.first().unwrap();
let ip = service.lookup_aaaa(host.to_string()).await;
assert!(hosts_map
.get(&IpAddr::V6(*ip.first().unwrap()))
.unwrap()
.contains(host));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_wildcard_central() {
let service = Service::new(
ServiceConfig::default()
.update_interval(Some(Duration::new(5, 0)))
.network_filename("6plane-only")
.wildcard_everything(true),
)
.await;
let member_record = service.member_record();
let named_record = Name::from_str("islay.home.arpa.").unwrap();
service.change_name("islay").await;
assert_eq!(
service
.lookup_aaaa(named_record.to_string())
.await
.first()
.unwrap(),
&service.clone().any_listen_ip()
);
assert_eq!(
service
.lookup_aaaa(member_record.to_string())
.await
.first()
.unwrap(),
&service.clone().any_listen_ip()
);
for host in vec!["one", "ten", "zt-foo", "another-record"] {
for rec in vec![named_record.to_string(), member_record.clone()] {
let lookup = Name::from_str(&host)
.unwrap()
.append_domain(&Name::from_str(&rec).unwrap())
.unwrap()
.to_string();
assert_eq!(
service.lookup_aaaa(lookup).await.first().unwrap(),
&service.clone().any_listen_ip()
);
}
}
}
}
#[cfg(feature = "integration-tests")]
mod rfc4193 {
use std::{net::IpAddr, path::Path, str::FromStr, time::Duration};
use rand::{prelude::SliceRandom, thread_rng};
use tracing::info;
use trust_dns_resolver::{IntoName, Name};
use zeronsd::{addresses::Calculator, hosts::parse_hosts};
use crate::service::{
resolver::Lookup,
to_ip::{ToIPv6Vec, ToPTRVec},
utils::HostsType,
Service, ServiceConfig,
};
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_single_domain() {
let service = Service::new(ServiceConfig::default().network_filename("rfc4193-only")).await;
let record = service.member_record();
info!("Looking up {}", record);
let mut listen_ips = service.listen_ips.clone();
listen_ips.sort();
for _ in 0..1000 {
let mut ips = service.lookup_aaaa(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv6_vec());
}
let ptr_records: Vec<String> = service
.listen_ips
.clone()
.into_iter()
.map(|ip| ip.ip().to_string())
.collect();
for ptr_record in ptr_records.clone() {
info!("Looking up {}", ptr_record);
for _ in 0..1000 {
let service = service.clone();
assert_eq!(
service
.lookup_ptr(ptr_record.clone())
.await
.first()
.unwrap(),
&record.to_string()
);
}
}
info!("Interleaved lookups of PTR and AAAA records");
for _ in 0..1000 {
// randomly switch order
if rand::random::<bool>() {
let mut ips = service.lookup_aaaa(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv6_vec(),);
assert_eq!(
service
.clone()
.lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string())
.await
.first()
.unwrap(),
&record.to_string()
);
} else {
assert_eq!(
service
.clone()
.lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string())
.await
.first()
.unwrap(),
&record.to_string()
);
let mut ips = service.lookup_aaaa(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv6_vec());
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_single_domain_named() {
let update_interval = Duration::new(2, 0);
let service = Service::new(
ServiceConfig::default()
.update_interval(Some(update_interval))
.network_filename("rfc4193-only"),
)
.await;
let member_record = service.member_record();
service.change_name("islay").await;
let named_record = "islay.home.arpa.".to_string();
for record in vec![member_record, named_record.clone()] {
info!("Looking up {}", record);
let mut listen_ips = service.listen_ips.clone();
listen_ips.sort();
for _ in 0..1000 {
let mut ips = service.lookup_aaaa(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv6_vec());
}
}
let ptr_records: Vec<String> = service.listen_ips.clone().to_ptr_vec();
for ptr_record in ptr_records {
info!("Looking up {}", ptr_record);
for _ in 0..1000 {
let service = service.clone();
assert_eq!(
service
.lookup_ptr(ptr_record.clone())
.await
.first()
.unwrap(),
&named_record.to_string()
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_multi_domain_hosts_file() {
let service = Service::new(
ServiceConfig::default()
.hosts(HostsType::Fixture("basic-ipv6"))
.network_filename("rfc4193-only"),
)
.await;
let record = service.member_record();
info!("Looking up random domains");
let mut hosts_map = parse_hosts(
Some(
Path::new(&format!("{}/basic-ipv6", zeronsd::utils::TEST_HOSTS_DIR)).to_path_buf(),
),
"home.arpa.".into_name().unwrap(),
)
.unwrap();
let ip = service.test_network().member().rfc4193().unwrap().ip();
hosts_map.insert(ip, vec![record.clone().into_name().unwrap()]);
let mut hosts = hosts_map.values().flatten().collect::<Vec<&Name>>();
for _ in 0..1000 {
hosts.shuffle(&mut rand::thread_rng());
let host = *hosts.first().unwrap();
let ip = service.lookup_aaaa(host.to_string()).await;
assert!(hosts_map
.get(&IpAddr::V6(*ip.first().unwrap()))
.unwrap()
.contains(host));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_wildcard_central() {
let service = Service::new(
ServiceConfig::default()
.update_interval(Some(Duration::new(5, 0)))
.network_filename("rfc4193-only")
.wildcard_everything(true),
)
.await;
let member_record = service.member_record();
let named_record = Name::from_str("islay.home.arpa.").unwrap();
service.change_name("islay").await;
assert_eq!(
service
.lookup_aaaa(named_record.to_string())
.await
.first()
.unwrap(),
&service.clone().any_listen_ip()
);
assert_eq!(
service
.lookup_aaaa(member_record.to_string())
.await
.first()
.unwrap(),
&service.clone().any_listen_ip()
);
for host in vec!["one", "ten", "zt-foo", "another-record"] {
for rec in vec![named_record.to_string(), member_record.clone()] {
let lookup = Name::from_str(&host)
.unwrap()
.append_domain(&Name::from_str(&rec).unwrap())
.unwrap()
.to_string();
assert_eq!(
service.lookup_aaaa(lookup).await.first().unwrap(),
&service.clone().any_listen_ip()
);
}
}
}
}
#[cfg(feature = "integration-tests")]
mod ipv4 {
use std::time::Duration;
use std::str::FromStr;
use tracing::info;
use trust_dns_resolver::Name;
use crate::service::{
resolver::Lookup,
to_ip::{ToIPv4Vec, ToPTRVec},
Service, ServiceConfig,
};
#[tokio::test(flavor = "multi_thread")]
async fn test_wildcard_central() {
let service = Service::new(
ServiceConfig::default()
.update_interval(Some(Duration::new(5, 0)))
.wildcard_everything(true),
)
.await;
let member_record = service.member_record();
let named_record = Name::from_str("islay.home.arpa.").unwrap();
service.change_name("islay").await;
assert_eq!(
service
.lookup_a(named_record.to_string())
.await
.first()
.unwrap(),
&service.clone().any_listen_ip()
);
assert_eq!(
service
.lookup_a(member_record.to_string())
.await
.first()
.unwrap(),
&service.clone().any_listen_ip()
);
for host in vec!["one", "ten", "zt-foo", "another-record"] {
for rec in vec![named_record.to_string(), member_record.clone()] {
let lookup = Name::from_str(&host)
.unwrap()
.append_domain(&Name::from_str(&rec).unwrap())
.unwrap()
.to_string();
assert_eq!(
service.lookup_a(lookup).await.first().unwrap(),
&service.clone().any_listen_ip()
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_single_domain() {
use rand::{seq::SliceRandom, thread_rng};
let service = Service::new(ServiceConfig::default().ips(Some(vec![
"172.16.240.2",
"172.16.240.3",
"172.16.240.4",
])))
.await;
let record = service.member_record();
info!("Looking up {}", record);
let mut listen_ips = service.listen_ips.clone();
listen_ips.sort();
for _ in 0..1000 {
let mut ips = service.lookup_a(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv4_vec());
}
let ptr_records: Vec<String> = service
.listen_ips
.clone()
.into_iter()
.map(|ip| ip.ip().to_string())
.collect();
for ptr_record in ptr_records.clone() {
info!("Looking up {}", ptr_record);
for _ in 0..1000 {
let service = service.clone();
assert_eq!(
service
.lookup_ptr(ptr_record.clone())
.await
.first()
.unwrap(),
&record.to_string()
);
}
}
info!("Interleaved lookups of PTR and A records");
for _ in 0..1000 {
// randomly switch order
if rand::random::<bool>() {
let mut ips = service.lookup_a(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv4_vec());
assert_eq!(
service
.clone()
.lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string())
.await
.first()
.unwrap(),
&record.to_string()
);
} else {
assert_eq!(
service
.clone()
.lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string())
.await
.first()
.unwrap(),
&record.to_string()
);
let mut ips = service.lookup_a(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv4_vec());
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_single_domain_named() {
let update_interval = Duration::new(2, 0);
let service = Service::new(
ServiceConfig::default()
.update_interval(Some(update_interval))
.ips(Some(vec!["172.16.240.2", "172.16.240.3", "172.16.240.4"])),
)
.await;
let member_record = service.member_record();
service.change_name("islay").await;
let named_record = "islay.home.arpa.".to_string();
for record in vec![member_record, named_record.clone()] {
info!("Looking up {}", record);
let mut listen_ips = service.listen_ips.clone();
listen_ips.sort();
for _ in 0..1000 {
let mut ips = service.lookup_a(record.clone()).await;
ips.sort();
assert_eq!(ips, listen_ips.clone().to_ipv4_vec());
}
}
let ptr_records: Vec<String> = service.listen_ips.clone().to_ptr_vec();
for ptr_record in ptr_records {
info!("Looking up {}", ptr_record);
for _ in 0..1000 {
let service = service.clone();
assert_eq!(
service
.lookup_ptr(ptr_record.clone())
.await
.first()
.unwrap(),
&named_record.to_string()
);
}
}
}
}
#[cfg(feature = "integration-tests")]
mod all {
use rand::prelude::SliceRandom;
use tracing::info;
use trust_dns_resolver::{IntoName, Name};
use zeronsd::{addresses::Calculator, hosts::parse_hosts, utils::TEST_HOSTS_DIR};
use crate::service::{resolver::Lookup, utils::HostsType, Service, ServiceConfig};
use std::{
net::{IpAddr, Ipv4Addr, Ipv6Addr},
path::Path,
str::FromStr,
thread::sleep,
time::Duration,
};
#[tokio::test(flavor = "multi_thread")]
async fn test_battery_multi_domain_hosts_file() {
let ips = vec!["172.16.240.2", "172.16.240.3", "172.16.240.4"];
let service = Service::new(
ServiceConfig::default()
.hosts(HostsType::Fixture("basic"))
.ips(Some(ips.clone())),
)
.await;
let record = service.member_record();
info!("Looking up random domains");
let mut hosts_map = parse_hosts(
Some(Path::new(&format!("{}/basic", TEST_HOSTS_DIR)).to_path_buf()),
"home.arpa.".into_name().unwrap(),
)
.unwrap();
for ip in ips {
hosts_map.insert(
IpAddr::from_str(&ip).unwrap(),
vec![record.clone().into_name().unwrap()],
);
}
let mut hosts = hosts_map.values().flatten().collect::<Vec<&Name>>();
for _ in 0..1000 {
hosts.shuffle(&mut rand::thread_rng());
let host = *hosts.first().unwrap();
let ips = service.lookup_a(host.to_string()).await;
assert!(hosts_map
.get(&IpAddr::from(*ips.first().unwrap()))
.unwrap()
.contains(host));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_hosts_file_reloading() {
let hosts_path = "/tmp/zeronsd-test-hosts";
std::fs::write(hosts_path, "127.0.0.2 islay\n::2 islay\n").unwrap();
let service = Service::new(
ServiceConfig::default()
.hosts(HostsType::Path(hosts_path))
.update_interval(Some(Duration::new(2, 0))),
)
.await;
assert_eq!(
service
.lookup_a("islay.home.arpa.".to_string())
.await
.first()
.unwrap(),
&Ipv4Addr::from_str("127.0.0.2").unwrap()
);
assert_eq!(
service
.lookup_aaaa("islay.home.arpa.".to_string())
.await
.first()
.unwrap(),
&Ipv6Addr::from_str("::2").unwrap()
);
std::fs::write(hosts_path, "127.0.0.3 islay\n::3 islay\n").unwrap();
sleep(Duration::new(10, 0)); // wait for bg update
assert_eq!(
service
.lookup_a("islay.home.arpa.".to_string())
.await
.first()
.unwrap(),
&Ipv4Addr::from_str("127.0.0.3").unwrap()
);
assert_eq!(
service
.lookup_aaaa("islay.home.arpa.".to_string())
.await
.first()
.unwrap(),
&Ipv6Addr::from_str("::3").unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_listen_ip() -> Result<(), anyhow::Error> {
use crate::service::{context::TestContext, network::TestNetwork};
use zeronsd::utils::*;
let tn = TestNetwork::new("basic-ipv4", &mut TestContext::default().await)
.await
.unwrap();
let listen_ips = get_listen_ips(
&authtoken_path(None),
&tn.network.clone().id.unwrap(),
ZEROTIER_LOCAL_URL.into(),
)
.await?;
eprintln!("My listen IP is {}", listen_ips.first().unwrap());
assert_ne!(*listen_ips.first().unwrap(), String::from(""));
drop(tn);
// see testdata/networks/basic-ipv4.json
let mut ips = vec!["172.16.240.2", "172.16.240.3", "172.16.240.4"];
let tn =
TestNetwork::new_multi_ip("basic-ipv4", &mut TestContext::default().await, ips.clone())
.await
.unwrap();
ips.sort();
let mut listen_ips: Vec<String> = get_listen_ips(
&authtoken_path(None),
&tn.network.clone().id.unwrap(),
ZEROTIER_LOCAL_URL.into(),
)
.await?
.iter()
.map(|x| parse_ip_from_cidr(x.clone()).to_string())
.collect();
listen_ips.sort();
assert_eq!(listen_ips, ips);
eprintln!("My listen IPs are {}", listen_ips.join(", "));
let tn = TestNetwork::new("rfc4193-only", &mut TestContext::default().await)
.await
.unwrap();
let mut listen_ips: Vec<String> = get_listen_ips(
&authtoken_path(None),
&tn.network.clone().id.unwrap(),
ZEROTIER_LOCAL_URL.into(),
)
.await?
.iter()
.map(|x| parse_ip_from_cidr(x.clone()).to_string())
.collect();
listen_ips.sort();
let mut ips = vec![tn.member().clone().rfc4193()?.ip().to_string()];
ips.sort();
assert_eq!(listen_ips, ips);
eprintln!("My listen IPs are {}", listen_ips.join(", "));
drop(tn);
let tn = TestNetwork::new("6plane-only", &mut TestContext::default().await)
.await
.unwrap();
let mut listen_ips: Vec<String> = get_listen_ips(
&authtoken_path(None),
&tn.network.clone().id.unwrap(),
ZEROTIER_LOCAL_URL.into(),
)
.await?
.iter()
.map(|x| parse_ip_from_cidr(x.clone()).to_string())
.collect();
listen_ips.sort();
let mut ips = vec![tn.member().clone().sixplane()?.ip().to_string()];
ips.sort();
assert_eq!(listen_ips, ips);
eprintln!("My listen IPs are {}", listen_ips.join(", "));
Ok(())
}
}