diff --git a/Cargo.lock b/Cargo.lock index 8d81952..863465f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,19 +81,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "time 0.1.43", - "winapi", -] - [[package]] name = "clap" version = "3.1.0" @@ -186,6 +173,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -646,25 +646,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - [[package]] name = "num_cpus" version = "1.13.1" @@ -1110,19 +1091,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "stderrlog" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a53e2eff3e94a019afa6265e8ee04cb05b9d33fe9f5078b14e4e391d155a38" -dependencies = [ - "atty", - "chrono", - "log", - "termcolor", - "thread_local", -] - [[package]] name = "strsim" version = "0.10.0" @@ -1189,25 +1157,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "time" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "time" version = "0.3.7" @@ -1348,7 +1297,7 @@ dependencies = [ "radix_trie", "rand", "thiserror", - "time 0.3.7", + "time", "tokio", "trust-dns-proto", ] @@ -1410,13 +1359,13 @@ dependencies = [ "bytes", "cfg-if", "enum-as-inner", - "env_logger", + "env_logger 0.8.4", "futures-executor", "futures-util", "log", "serde", "thiserror", - "time 0.3.7", + "time", "tokio", "toml", "trust-dns-client", @@ -1682,19 +1631,21 @@ dependencies = [ [[package]] name = "zeronsd" -version = "0.2.6" +version = "0.2.7" dependencies = [ "anyhow", + "async-trait", "clap", + "env_logger 0.9.0", "hex", "ipnetwork", + "lazy_static", "log", "num_cpus", "rand", "regex", "serde", "serde_json", - "stderrlog", "tinytemplate", "tokio", "trust-dns-resolver", @@ -1705,9 +1656,7 @@ dependencies = [ [[package]] name = "zerotier-central-api" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7648fc784340f8992902d91c36e0cbceb71176926c53e5bc84b918069db79643" +version = "1.0.3" dependencies = [ "reqwest", "serde", @@ -1718,9 +1667,7 @@ dependencies = [ [[package]] name = "zerotier-one-api" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e82c6f3322540f6c7808d446849baf2dc027a1784dc30626be2a665b548153" +version = "1.0.5" dependencies = [ "reqwest", "serde", diff --git a/Cargo.toml b/Cargo.toml index d27dbbf..acdca1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,22 +18,23 @@ regex = ">= 0" anyhow = ">= 0" clap = { version = "^3", features = ["derive"] } ipnetwork = ">= 0" -trust-dns-resolver = "0.20.2" -trust-dns-server = { version = "0.20.2", features = ["trust-dns-resolver"] } +trust-dns-resolver = { version = "^0.20.4", features = ["tokio-runtime"] } +trust-dns-server = { version = "^0.20.4", features = ["trust-dns-resolver"] } tokio = { version = "1", features = ["full"] } -zerotier-central-api = "= 1.0.2" -zerotier-one-api = "= 1.0.4" +zerotier-central-api = { version = "= 1.0.3", path = "../zerotier-rust-api/zerotier-central-api" } +zerotier-one-api = { version = "= 1.0.5", path = "../zerotier-rust-api/zerotier-one-api" } serde = ">= 0" serde_json = ">= 0" tinytemplate = ">= 0" rand = ">= 0" num_cpus = ">=0" log = ">=0" -stderrlog = ">=0" +env_logger = ">=0" hex = ">=0" -[features] -integration-tests = [] +[dev-dependencies] +async-trait = ">=0" +lazy_static = ">=0" [package.metadata.deb] copyright = "ZeroTier, Inc" diff --git a/Makefile b/Makefile index 32a7f2b..598cc57 100644 --- a/Makefile +++ b/Makefile @@ -9,9 +9,9 @@ test: test-integration: ifneq (${SKIP},) - TOKEN=$$(cat test-token.txt) sudo -E bash -c "$$(which cargo) test ${RUN_TEST} --features integration-tests -- --skip '${SKIP}' --nocapture --test-threads 1" + TOKEN=$$(cat test-token.txt) sudo -E bash -c "$$(which cargo) test ${RUN_TEST} -- --skip '${SKIP}' --nocapture --test-threads 1" else - TOKEN=$$(cat test-token.txt) sudo -E bash -c "$$(which cargo) test ${RUN_TEST} --features integration-tests -- --nocapture --test-threads 1" + TOKEN=$$(cat test-token.txt) sudo -E bash -c "$$(which cargo) test ${RUN_TEST} -- --nocapture --test-threads 1" endif generate: central service diff --git a/src/addresses.rs b/src/addresses.rs index dc38f84..ea1dabb 100644 --- a/src/addresses.rs +++ b/src/addresses.rs @@ -18,7 +18,7 @@ fn get_parts(member: Member) -> Result<(u64, u64), anyhow::Error> { )) } -pub(crate) trait Calculator { +pub trait Calculator { fn sixplane(self) -> Result; fn rfc4193(self) -> Result; } diff --git a/src/authority.rs b/src/authority.rs index 3d14ef9..a46a06d 100644 --- a/src/authority.rs +++ b/src/authority.rs @@ -36,7 +36,7 @@ use crate::{ utils::{parse_member_name, ToHostname}, }; -pub(crate) trait ToPointerSOA { +pub trait ToPointerSOA { fn to_ptr_soa_name(self) -> Result; } @@ -56,7 +56,7 @@ impl ToPointerSOA for IpNetwork { } } -pub(crate) trait ToWildcard { +pub trait ToWildcard { fn to_wildcard(self, count: u8) -> Name; } @@ -71,12 +71,12 @@ impl ToWildcard for Name { } } -pub(crate) type TokioZTAuthority = Arc>; +pub type TokioZTAuthority = Arc>; // Authority is lock managed, and kept on the heap. Be mindful when modifying through the Arc. -pub(crate) type Authority = Box>>; -pub(crate) type PtrAuthorityMap = HashMap; +pub type Authority = Box>>; +pub type PtrAuthorityMap = HashMap; -pub(crate) fn new_ptr_authority(ip: IpNetwork) -> Result { +pub fn new_ptr_authority(ip: IpNetwork) -> Result { let domain_name = ip.to_ptr_soa_name()?; let mut authority = InMemoryAuthority::empty( @@ -93,7 +93,7 @@ pub(crate) fn new_ptr_authority(ip: IpNetwork) -> Result Authority { +pub fn init_trust_dns_authority(domain_name: Name) -> Authority { let mut authority = InMemoryAuthority::empty( domain_name.clone(), trust_dns_server::authority::ZoneType::Primary, @@ -313,7 +313,7 @@ pub(crate) fn init_trust_dns_authority(domain_name: Name) -> Authority { // init_catalog: also a really ugly constructor, but in this case initializes the whole trust-dns // subsystem. -pub(crate) async fn init_catalog(zt: TokioZTAuthority) -> Result { +pub async fn init_catalog(zt: TokioZTAuthority) -> Result { let read = zt.read().await; let mut catalog = Catalog::default(); @@ -355,7 +355,7 @@ pub(crate) async fn init_catalog(zt: TokioZTAuthority) -> Result, ptr_name: Name, @@ -365,7 +365,7 @@ pub(crate) struct ZTRecord { } impl ZTRecord { - pub(crate) fn new( + pub fn new( member: &Member, sixplane: Option, rfc4193: Option, @@ -421,7 +421,7 @@ impl ZTRecord { } // insert_records is hopefully well-named. - pub(crate) fn insert_records(&self, records: &mut Vec) { + pub fn insert_records(&self, records: &mut Vec) { records.push(self.fqdn.clone()); for ip in self.ips.clone() { @@ -441,7 +441,7 @@ impl ZTRecord { } // get_canonical_wildcard is a function to combine canonical_name (named members) and wildcard functionality. - pub(crate) fn get_canonical_wildcard(&self) -> Option { + pub fn get_canonical_wildcard(&self) -> Option { if self.canonical_name.is_none() { return None; } @@ -451,7 +451,7 @@ impl ZTRecord { // insert_authority is not very well named, but performs the function of inserting a ZTRecord // into a ZTAuthority. - pub(crate) fn insert_authority(&self, authority: &ZTAuthority) -> Result<(), anyhow::Error> { + pub fn insert_authority(&self, authority: &ZTAuthority) -> Result<(), anyhow::Error> { authority.match_or_insert(self.fqdn.clone(), &self.ips); if self.wildcard_everything { @@ -469,7 +469,7 @@ impl ZTRecord { } // insert_member_ptr is a lot like insert_authority, but for PTRs. - pub(crate) fn insert_member_ptr( + pub fn insert_member_ptr( &self, authority_map: PtrAuthorityMap, _sixplane: Option, @@ -502,7 +502,7 @@ impl ZTRecord { // ZTAuthority is the customized trust-dns authority. #[derive(Clone)] -pub(crate) struct ZTAuthority { +pub struct ZTAuthority { ptr_authority_map: PtrAuthorityMap, authority: Authority, domain_name: Name, @@ -515,7 +515,7 @@ pub(crate) struct ZTAuthority { } impl ZTAuthority { - pub(crate) fn new( + pub fn new( domain_name: Name, network: String, config: Configuration, @@ -537,7 +537,7 @@ impl ZTAuthority { } } - pub(crate) fn wildcard_everything(&mut self) { + pub fn wildcard_everything(&mut self) { self.wildcard_everything = true; } @@ -559,9 +559,7 @@ impl ZTAuthority { .expect("Could not get authority read lock"); // for the record type, fetch the named record. - let rs = lock - .records() - .get(&RrKey::new(name.clone().into(), rt)); + let rs = lock.records().get(&RrKey::new(name.clone().into(), rt)); // gather all the ips (v6 too) for the record. let ips: Vec = newips @@ -746,8 +744,3 @@ impl ZTAuthority { } } } - -#[cfg(all(feature = "integration-tests", test))] -mod service; -#[cfg(all(feature = "integration-tests", test))] -mod tests; diff --git a/src/authority/service.rs b/src/authority/service.rs deleted file mode 100644 index 1b17ff6..0000000 --- a/src/authority/service.rs +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Service abstraction provides a way to automatically generate services that are attached to - * TestNetworks for testing against. Each Service is composed of a DNS service attached to a - * TestNetwork. The service can then be resolved against, for example. Several parameters for - * managing the underlying TestNetwork, and the Service are available via the ServiceConfig struct. - */ - -use std::{ - collections::HashMap, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - path::Path, - str::FromStr, - sync::{Arc, Mutex}, - thread::sleep, - time::Duration, -}; - -use ipnetwork::IpNetwork; -use log::info; -use rand::prelude::{IteratorRandom, SliceRandom}; -use tokio::{runtime::Runtime, sync::RwLock}; -use trust_dns_resolver::{ - config::{NameServerConfig, ResolverConfig, ResolverOpts}, - proto::rr::RecordType, - Resolver, -}; - -use crate::{ - addresses::Calculator, - authority::{find_members, init_trust_dns_authority, new_ptr_authority, ZTAuthority}, - integration_tests::{init_test_runtime, TestContext, TestNetwork}, - tests::HOSTS_DIR, - utils::{authtoken_path, domain_or_default, get_listen_ips, parse_ip_from_cidr}, -}; - -#[derive(Clone)] -pub(crate) struct Service { - runtime: Arc>, - tn: Arc, - resolvers: Arc>>, - update_interval: Option, - pub listen_ips: Vec, -} - -pub(crate) trait Lookup { - fn lookup_a(&self, record: String) -> Vec; - fn lookup_aaaa(&self, record: String) -> Vec; - fn lookup_ptr(&self, record: String) -> Vec; -} - -impl Lookup for Resolver { - fn lookup_a(&self, record: String) -> Vec { - self.lookup(record, RecordType::A) - .unwrap() - .record_iter() - .map(|r| r.rdata().clone().into_a().unwrap()) - .collect() - } - - fn lookup_aaaa(&self, record: String) -> Vec { - self.ipv6_lookup(record) - .unwrap() - .as_lookup() - .record_iter() - .map(|r| r.rdata().clone().into_aaaa().unwrap()) - .collect() - } - - fn lookup_ptr(&self, record: String) -> Vec { - self.lookup(record, RecordType::PTR) - .unwrap() - .record_iter() - .map(|r| r.rdata().clone().into_ptr().unwrap().to_string()) - .collect() - } -} - -pub(crate) enum HostsType { - Path(&'static str), - Fixture(&'static str), - None, -} - -fn create_listeners( - runtime: Arc>, - tn: &TestNetwork, - hosts: HostsType, - update_interval: Option, - wildcard_everything: bool, -) -> Vec { - let listen_cidrs = runtime - .lock() - .unwrap() - .block_on(get_listen_ips( - &authtoken_path(None), - &tn.network.clone().id.unwrap(), - )) - .unwrap(); - - let mut listen_ips = Vec::new(); - - let mut ipmap = HashMap::new(); - let mut authority_map = HashMap::new(); - let authority = init_trust_dns_authority(domain_or_default(None).unwrap()); - - for cidr in listen_cidrs.clone() { - let listen_ip = parse_ip_from_cidr(cidr.clone()); - let socket_addr = SocketAddr::new(listen_ip.clone(), 53); - listen_ips.push(socket_addr); - let cidr = IpNetwork::from_str(&cidr.clone()).unwrap(); - if !ipmap.contains_key(&listen_ip) { - ipmap.insert(listen_ip, cidr.network()); - } - - if !authority_map.contains_key(&cidr) { - let ptr_authority = new_ptr_authority(cidr).unwrap(); - authority_map.insert(cidr, ptr_authority.clone()); - } - } - - if let Some(v6assign) = tn.network.config.clone().unwrap().v6_assign_mode { - if v6assign.rfc4193.unwrap_or(false) { - let cidr = tn.network.clone().rfc4193().unwrap(); - if !authority_map.contains_key(&cidr) { - let ptr_authority = new_ptr_authority(cidr).unwrap(); - authority_map.insert(cidr, ptr_authority); - } - } - } - - let update_interval = update_interval.unwrap_or(Duration::new(10, 0)); - - let mut ztauthority = ZTAuthority::new( - domain_or_default(None).unwrap(), - tn.network.clone().id.unwrap(), - tn.central(), - match hosts { - HostsType::Fixture(hosts) => { - Some(Path::new(&format!("{}/{}", HOSTS_DIR, hosts)).to_path_buf()) - } - HostsType::Path(hosts) => Some(Path::new(hosts).to_path_buf()), - HostsType::None => None, - }, - authority_map, - update_interval, - authority.clone(), - ); - - if wildcard_everything { - ztauthority.wildcard_everything(); - } - - let arc_authority = Arc::new(RwLock::new(ztauthority)); - let lock = runtime.lock().unwrap(); - lock.spawn(find_members(arc_authority.clone())); - - lock.block_on(async { tokio::time::sleep(Duration::new(5, 0)).await }); - - for ip in listen_ips.clone() { - let server = crate::server::Server::new(arc_authority.to_owned()); - info!("Serving {}", ip.clone()); - lock.spawn(server.listen(ip, Duration::new(0, 1000))); - } - - listen_ips -} - -fn create_resolvers(sockets: Vec) -> Vec> { - let mut resolvers = Vec::new(); - - for socket in sockets { - let mut resolver_config = ResolverConfig::new(); - resolver_config.add_search(domain_or_default(None).unwrap()); - resolver_config.add_name_server(NameServerConfig { - socket_addr: socket, - protocol: trust_dns_resolver::config::Protocol::Udp, - tls_dns_name: None, - trust_nx_responses: true, - }); - - let mut opts = ResolverOpts::default(); - opts.cache_size = 0; - opts.rotate = true; - opts.use_hosts_file = false; - opts.positive_min_ttl = Some(Duration::new(0, 0)); - opts.positive_max_ttl = Some(Duration::new(0, 0)); - opts.negative_min_ttl = Some(Duration::new(0, 0)); - opts.negative_max_ttl = Some(Duration::new(0, 0)); - - resolvers.push(Arc::new( - trust_dns_resolver::Resolver::new(resolver_config, opts).unwrap(), - )); - } - - resolvers -} - -pub(crate) struct ServiceConfig { - hosts: HostsType, - update_interval: Option, - ips: Option>, - wildcard_everything: bool, - network_filename: Option<&'static str>, -} - -impl Default for ServiceConfig { - fn default() -> Self { - Self { - network_filename: None, - hosts: HostsType::None, - update_interval: None, - ips: None, - wildcard_everything: false, - } - } -} - -impl ServiceConfig { - pub(crate) fn network_filename(mut self, n: &'static str) -> Self { - self.network_filename = Some(n); - self - } - - pub(crate) fn hosts(mut self, h: HostsType) -> Self { - self.hosts = h; - self - } - - pub(crate) fn update_interval(mut self, u: Option) -> Self { - self.update_interval = u; - self - } - - pub(crate) fn ips(mut self, ips: Option>) -> Self { - self.ips = ips; - self - } - - pub(crate) fn wildcard_everything(mut self, w: bool) -> Self { - self.wildcard_everything = w; - self - } -} - -impl Service { - pub(crate) fn new(sc: ServiceConfig) -> Self { - let runtime = init_test_runtime(); - - let network_filename = sc.network_filename.unwrap_or("basic-ipv4"); - let tn = if let Some(ips) = sc.ips { - TestNetwork::new_multi_ip( - runtime.clone(), - network_filename, - &mut TestContext::default(), - ips, - ) - .unwrap() - } else { - TestNetwork::new( - runtime.clone(), - network_filename, - &mut TestContext::default(), - ) - .unwrap() - }; - - let listen_ips = create_listeners( - runtime.clone(), - &tn, - sc.hosts, - sc.update_interval, - sc.wildcard_everything, - ); - - Self { - runtime, - tn: Arc::new(tn), - resolvers: Arc::new(create_resolvers(listen_ips.clone())), - listen_ips, - update_interval: sc.update_interval, - } - } - - pub fn any_listen_ip(self) -> IpAddr { - self.listen_ips - .clone() - .into_iter() - .choose(&mut rand::thread_rng()) - .unwrap() - .clone() - .ip() - } - - pub fn runtime(&self) -> Arc> { - self.runtime.clone() - } - - pub fn network(&self) -> Arc { - self.tn.clone() - } - - pub fn resolvers(&self) -> Arc>> { - self.resolvers.clone() - } - - pub fn any_resolver(&self) -> Arc { - self.resolvers() - .choose(&mut rand::thread_rng()) - .to_owned() - .unwrap() - .clone() - } - - pub fn member_record(&self) -> String { - format!("zt-{}.domain.", self.network().identity().clone()) - } - - pub fn change_name(&self, name: &'static str) { - let mut member = self - .runtime() - .lock() - .unwrap() - .block_on( - zerotier_central_api::apis::network_member_api::get_network_member( - &self.network().central(), - &self.network().network.clone().id.unwrap(), - &self.network().identity(), - ), - ) - .unwrap(); - - member.name = Some(name.to_string()); - - self.runtime() - .lock() - .unwrap() - .block_on( - zerotier_central_api::apis::network_member_api::update_network_member( - &self.network().central(), - &self.network().network.clone().id.unwrap(), - &self.network().identity(), - member, - ), - ) - .unwrap(); - - if self.update_interval.is_some() { - sleep(self.update_interval.unwrap()); // wait for it to update - } - } - - pub fn test_network(&self) -> Arc { - self.tn.clone() - } -} - -impl Lookup for Service { - fn lookup_a(&self, record: String) -> Vec { - self.any_resolver().lookup_a(record) - } - - fn lookup_aaaa(&self, record: String) -> Vec { - self.any_resolver().lookup_aaaa(record) - } - - fn lookup_ptr(&self, record: String) -> Vec { - self.any_resolver().lookup_ptr(record) - } -} diff --git a/src/bin/zeronsd.rs b/src/bin/zeronsd.rs new file mode 100644 index 0000000..14dc9ff --- /dev/null +++ b/src/bin/zeronsd.rs @@ -0,0 +1,5 @@ +use zeronsd::init::init; + +fn main() -> Result<(), anyhow::Error> { + init() +} diff --git a/src/cli.rs b/src/cli.rs index 80e5a53..ecf056a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use clap::{Args, Subcommand, Parser}; +use clap::{Args, Parser, Subcommand}; /// zerotier central nameserver #[derive(Parser)] diff --git a/src/hosts.rs b/src/hosts.rs index d6839a4..8b09fec 100644 --- a/src/hosts.rs +++ b/src/hosts.rs @@ -5,14 +5,14 @@ use trust_dns_server::client::rr::Name; use crate::utils::ToHostname; -pub(crate) type HostsFile = HashMap>; +pub type HostsFile = HashMap>; const WHITESPACE_SPLIT: &str = r"\s+"; const COMMENT_MATCH: &str = r"^\s*#"; /// Parses an /etc/hosts-formatted file into a mapping of ip -> [name]. Used to populate the /// authority. -pub(crate) fn parse_hosts( +pub fn parse_hosts( hosts_file: Option, domain_name: Name, ) -> Result { diff --git a/src/main.rs b/src/init.rs similarity index 72% rename from src/main.rs rename to src/init.rs index a3c7223..b2db869 100644 --- a/src/main.rs +++ b/src/init.rs @@ -8,43 +8,24 @@ use ipnetwork::IpNetwork; use log::{error, info, warn}; use tokio::sync::RwLock; -use crate::{ - addresses::Calculator, - authority::{find_members, init_trust_dns_authority, new_ptr_authority, ZTAuthority}, - cli::{Cli, Command, StartArgs, SuperviseArgs, UnsuperviseArgs}, - utils::{central_config, central_token, update_central_dns}, -}; - -mod addresses; -mod authority; -mod cli; -mod hosts; -mod server; -mod supervise; -mod utils; - -// integration tests are setup a little weird; basically `cargo test --feature integration-tests` -#[cfg(all(feature = "integration-tests", test))] -mod integration_tests; -#[cfg(test)] -mod tests; +use crate::{addresses::*, authority::*, cli::*, server::*, supervise::*, utils::*}; fn unsupervise(args: UnsuperviseArgs) -> Result<(), anyhow::Error> { - supervise::Properties::from(args).uninstall_supervisor() + Properties::from(args).uninstall_supervisor() } fn supervise(args: SuperviseArgs) -> Result<(), anyhow::Error> { - supervise::Properties::from(args).install_supervisor() + Properties::from(args).install_supervisor() } fn start(args: StartArgs) -> Result<(), anyhow::Error> { - let domain_name = utils::domain_or_default(args.domain.as_deref())?; - let authtoken = utils::authtoken_path(args.secret.as_deref()); - let runtime = &mut utils::init_runtime(); + let domain_name = domain_or_default(args.domain.as_deref())?; + let authtoken = authtoken_path(args.secret.as_deref()); + let runtime = &mut init_runtime(); let token = central_config(central_token(args.token.as_deref())?); info!("Welcome to ZeroNS!"); - let ips = runtime.block_on(utils::get_listen_ips(&authtoken, &args.network_id))?; + let ips = runtime.block_on(get_listen_ips(&authtoken, &args.network_id))?; // more or less the setup for the "main loop" if ips.len() > 0 { @@ -52,7 +33,7 @@ fn start(args: StartArgs) -> Result<(), anyhow::Error> { runtime, domain_name.clone(), ips.iter() - .map(|i| utils::parse_ip_from_cidr(i.clone()).to_string()) + .map(|i| parse_ip_from_cidr(i.clone()).to_string()) .collect(), token.clone(), args.network_id.clone(), @@ -64,7 +45,7 @@ fn start(args: StartArgs) -> Result<(), anyhow::Error> { let authority = init_trust_dns_authority(domain_name.clone()); for cidr in ips.clone() { - let listen_ip = utils::parse_ip_from_cidr(cidr.clone()); + let listen_ip = parse_ip_from_cidr(cidr.clone()); listen_ips.push(listen_ip.clone()); let cidr = IpNetwork::from_str(&cidr.clone())?; if !ipmap.contains_key(&listen_ip) { @@ -120,7 +101,7 @@ fn start(args: StartArgs) -> Result<(), anyhow::Error> { for ip in listen_ips { info!("Your IP for this network: {}", ip); - let server = crate::server::Server::new(arc_authority.to_owned()); + let server = Server::new(arc_authority.to_owned()); runtime.spawn(server.listen(SocketAddr::new(ip, 53), Duration::new(0, 1000))); } @@ -138,15 +119,10 @@ fn start(args: StartArgs) -> Result<(), anyhow::Error> { )); } -fn main() -> Result<(), anyhow::Error> { - let cli = Cli::parse(); +pub fn init() -> Result<(), anyhow::Error> { + init_logger(); - stderrlog::new() - .module(String::from("zeronsd")) - .verbosity(cli.verbose + 2) - .timestamp(stderrlog::Timestamp::Off) - .init() - .unwrap(); + let cli = Cli::parse(); let result = match cli.command { Command::Start(args) => start(args), diff --git a/src/integration_tests.rs b/src/integration_tests.rs deleted file mode 100644 index f04013c..0000000 --- a/src/integration_tests.rs +++ /dev/null @@ -1,404 +0,0 @@ -/// testing stuff; gated by feature flags. Does full round-trip processing of DNS results. -use crate::{ - addresses::Calculator, - utils::{authtoken_path, central_config, get_listen_ips, init_runtime}, -}; -use log::warn; -use std::{ - path::Path, - sync::{Arc, Mutex}, - thread::sleep, - time::Duration, -}; -use tokio::runtime::Runtime; -use zerotier_central_api::{ - apis::configuration::Configuration, - models::{Member, MemberConfig, Network}, -}; - -fn randstring(len: u8) -> String { - (0..len) - .map(|_| (rand::random::() % 26) + 'a' as u8) - .map(|c| { - if rand::random::() { - (c as char).to_ascii_uppercase() - } else { - c as char - } - }) - .map(|c| c.to_string()) - .collect::>() - .join("") -} - -// extract a network definiton from testdata. templates in a new name. -fn network_definition( - name: String, -) -> Result, anyhow::Error> { - let mut res: serde_json::Map = serde_json::from_reader( - std::fs::File::open(format!("testdata/networks/{}.json", name))?, - )?; - - if let serde_json::Value::Object(config) = res.clone().get("config").unwrap() { - let mut new_config = config.clone(); - new_config.insert( - "name".to_string(), - serde_json::Value::String(randstring(30)), - ); - - res.insert("config".to_string(), serde_json::Value::Object(new_config)); - } - - Ok(res) -} - -// returns the public identity of this instance of zerotier -pub(crate) async fn get_identity( - configuration: &zerotier_one_api::apis::configuration::Configuration, -) -> Result { - let status = zerotier_one_api::apis::status_api::get_status(configuration).await?; - - Ok(status - .public_identity - .unwrap() - .splitn(3, ":") - .nth(0) - .unwrap() - .to_owned()) -} - -// unpack the authtoken based on what we're passed -pub(crate) fn get_authtoken(or: Option<&str>) -> Result { - Ok(std::fs::read_to_string(authtoken_path( - or.map(|c| Path::new(c)), - ))?) -} - -// zerotier_config returns the openapi configuration required to talk to the local ztone instance -pub(crate) fn zerotier_config( - authtoken: String, -) -> zerotier_one_api::apis::configuration::Configuration { - let mut zerotier = zerotier_one_api::apis::configuration::Configuration::default(); - zerotier.api_key = Some(zerotier_one_api::apis::configuration::ApiKey { - prefix: None, - key: authtoken.clone(), - }); - - zerotier -} - -// TestRuntime is a tokio runtime made for testing. -pub(crate) type TestRuntime = Arc>; - -pub(crate) fn init_test_runtime() -> TestRuntime { - Arc::new(Mutex::new(init_runtime())) -} - -// monkeypatches to Member -pub(crate) trait MemberUtil { - // set some member defaults for testing - fn set_defaults(&mut self, network_id: String, identity: String); -} - -// monkeypatches to MemberConfig -pub(crate) trait MemberConfigUtil { - fn set_ip_assignments(&mut self, ips: Vec<&str>); - fn set_defaults(&mut self, identity: String); -} - -impl MemberUtil for Member { - fn set_defaults(&mut self, network_id: String, identity: String) { - self.node_id = Some(identity.clone()); - self.network_id = Some(network_id); - let mut mc = MemberConfig::new(); - mc.set_defaults(identity); - self.config = Some(Box::new(mc)); - } -} - -impl MemberConfigUtil for MemberConfig { - fn set_ip_assignments(&mut self, ips: Vec<&str>) { - self.ip_assignments = Some(ips.into_iter().map(|s| s.to_string()).collect()) - } - - fn set_defaults(&mut self, identity: String) { - self.v_rev = None; - self.v_major = None; - self.v_proto = None; - self.v_minor = None; - self.tags = None; - self.revision = None; - self.no_auto_assign_ips = Some(false); - self.last_authorized_time = None; - self.last_deauthorized_time = None; - self.id = None; - self.creation_time = None; - self.capabilities = None; - self.ip_assignments = None; - self.authorized = Some(true); - self.active_bridge = None; - self.identity = Some(identity); - } -} - -pub(crate) fn init_test_logger() { - stderrlog::new() - .verbosity( - std::env::var("VERBOSITY") - .unwrap_or(String::new()) - .parse() - .unwrap_or(0), - ) - .timestamp(stderrlog::Timestamp::Second) - .init() - .unwrap_or(()) -} - -// TestContext provides all the stuff we need to talk to run tests smoothly -#[derive(Clone)] -pub(crate) struct TestContext { - member_config: Option>, - identity: String, - zerotier: zerotier_one_api::apis::configuration::Configuration, - central: Configuration, -} - -impl TestContext { - pub fn get_member(&mut self, network_id: String) -> Member { - let mut member = Member::new(); - member.set_defaults(network_id, self.identity.clone()); - if self.member_config.is_some() { - member.config = self.member_config.clone(); - } - - member - } -} - -impl Default for TestContext { - fn default() -> Self { - let runtime = init_runtime(); - let authtoken = get_authtoken(None).expect("Could not read authtoken"); - let zerotier = zerotier_config(authtoken.clone()); - let identity = runtime - .block_on(get_identity(&zerotier)) - .expect("Could not retrieve identity from zerotier"); - - let token = std::env::var("TOKEN").expect("Please provide TOKEN in the environment"); - let central = central_config(token.clone()); - - Self { - member_config: None, - identity, - zerotier, - central, - } - } -} - -// TestNetwork creates a testnetwork in central and joins it. When this data is destroyed/dropped -// it will remove the network and leave it like nothing ever happened. -#[derive(Clone)] -pub(crate) struct TestNetwork { - pub network: Network, - runtime: Arc>, - context: TestContext, - member: Member, -} - -impl TestNetwork { - // new_multi_ip covers situations where zeronsd is using more than one listening ip. - pub fn new_multi_ip( - runtime: TestRuntime, - network_def: &str, - tc: &mut TestContext, - ips: Vec<&str>, - ) -> Result { - let mut mc = MemberConfig::new(); - mc.set_defaults(tc.identity.clone()); - mc.set_ip_assignments(ips); - tc.member_config = Some(Box::new(mc)); - Self::new(runtime, network_def, tc) - } - - // constructor. - pub fn new( - runtime: TestRuntime, - network_def: &str, - tc: &mut TestContext, - ) -> Result { - let network = runtime - .lock() - .unwrap() - .block_on(zerotier_central_api::apis::network_api::new_network( - &tc.central, - serde_json::Value::Object(network_definition(network_def.to_string())?), - )) - .unwrap(); - - let member = tc.get_member(network.clone().id.unwrap()); - - runtime - .lock() - .unwrap() - .block_on( - zerotier_central_api::apis::network_member_api::update_network_member( - &tc.central, - &network.clone().id.unwrap(), - &tc.identity, - member.clone(), - ), - ) - .unwrap(); - - let s = Self { - network, - member, - runtime: runtime.clone(), - context: tc.clone(), - }; - - s.join().unwrap(); - Ok(s) - } - - // join zerotier-one to the test network - pub fn join(&self) -> Result<(), anyhow::Error> { - let network = zerotier_one_api::models::Network::new(); - self.runtime.lock().unwrap().block_on( - zerotier_one_api::apis::network_api::update_network( - &self.context.zerotier, - &self.network.id.clone().unwrap(), - network, - ), - )?; - - let id = self.network.id.clone().unwrap(); - let mut count = 0; - - while let Err(e) = self - .runtime - .lock() - .unwrap() - .block_on(get_listen_ips(&authtoken_path(None), &id)) - { - sleep(Duration::new(1, 0)); - count += 1; - if count >= 5 { - warn!("5 attempts: While joining network: {:?}", e); - count = 0; - } - } - Ok(()) - } - - // leave the test network - pub fn leave(&self) -> Result<(), anyhow::Error> { - self.runtime.lock().unwrap().block_on( - zerotier_one_api::apis::network_api::delete_network( - &self.context.zerotier, - &self.network.id.clone().unwrap(), - ), - )?; - Ok(()) - } - - pub fn identity(&self) -> String { - self.context.identity.clone() - } - - pub fn central(&self) -> Configuration { - self.context.central.clone() - } - - pub fn member(&self) -> Member { - self.member.clone() - } -} - -// drop just removes the network from central and leaves it. it tries to recover, not get more -// angry, in the face of errors. -impl Drop for TestNetwork { - fn drop(&mut self) { - let opt = self.network.id.clone(); - self.leave().unwrap_or(()); - self.runtime - .lock() - .unwrap() - .block_on(zerotier_central_api::apis::network_api::delete_network( - &self.context.central, - &opt.unwrap(), - )) - .unwrap_or(()); - } -} - -#[test] -fn test_get_listen_ip() -> Result<(), anyhow::Error> { - init_test_logger(); - let tn = TestNetwork::new( - init_test_runtime(), - "basic-ipv4", - &mut TestContext::default(), - ) - .unwrap(); - let runtime = init_runtime(); - - let listen_ips = runtime.block_on(get_listen_ips( - &authtoken_path(None), - &tn.network.clone().id.unwrap(), - ))?; - - 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 runtime = init_test_runtime(); - let tn = TestNetwork::new_multi_ip( - runtime.clone(), - "basic-ipv4", - &mut TestContext::default(), - ips.clone(), - ) - .unwrap(); - - let mut listen_ips = runtime.lock().unwrap().block_on(get_listen_ips( - &authtoken_path(None), - &tn.network.clone().id.unwrap(), - ))?; - - assert_eq!(listen_ips.sort(), ips.sort()); - eprintln!("My listen IPs are {}", listen_ips.join(", ")); - - let tn = - TestNetwork::new(runtime.clone(), "rfc4193-only", &mut TestContext::default()).unwrap(); - - let mut listen_ips = runtime.lock().unwrap().block_on(get_listen_ips( - &authtoken_path(None), - &tn.network.clone().id.unwrap(), - ))?; - - let mut ips = vec![tn.member.clone().rfc4193()?.ip().to_string()]; - - assert_eq!(listen_ips.sort(), ips.sort()); - eprintln!("My listen IPs are {}", listen_ips.join(", ")); - - drop(tn); - - let tn = TestNetwork::new(runtime.clone(), "6plane-only", &mut TestContext::default()).unwrap(); - - let mut listen_ips = runtime.lock().unwrap().block_on(get_listen_ips( - &authtoken_path(None), - &tn.network.clone().id.unwrap(), - ))?; - - let mut ips = vec![tn.member.clone().sixplane()?.ip().to_string()]; - - assert_eq!(listen_ips.sort(), ips.sort()); - eprintln!("My listen IPs are {}", listen_ips.join(", ")); - - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0f7001d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,12 @@ +pub mod addresses; +pub mod authority; +pub mod cli; +pub mod hosts; +pub mod server; +pub mod supervise; +pub mod utils; + +pub mod init; + +#[cfg(test)] +mod tests; diff --git a/src/server.rs b/src/server.rs index c6696d4..e25b548 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,17 +9,17 @@ use trust_dns_server::server::ServerFuture; use crate::authority::{init_catalog, TokioZTAuthority}; -pub(crate) struct Server { +pub struct Server { zt: TokioZTAuthority, } impl Server { - pub(crate) fn new(zt: TokioZTAuthority) -> Self { + pub fn new(zt: TokioZTAuthority) -> Self { return Self { zt }; } // listener routine for TCP and UDP. - pub(crate) async fn listen( + pub async fn listen( self, listen_addr: SocketAddr, tcp_timeout: Duration, diff --git a/src/tests.rs b/src/tests.rs index 9b51e47..f74f4d6 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,8 +6,6 @@ use std::{ use crate::utils::{domain_or_default, ToHostname}; -pub const HOSTS_DIR: &str = "testdata/hosts-files"; - #[test] fn test_parse_member_name() { use crate::utils::parse_member_name; @@ -296,7 +294,7 @@ fn test_parse_hosts() { let domain = &Name::from_str("zombocom").unwrap(); - for path in std::fs::read_dir(HOSTS_DIR) + for path in std::fs::read_dir(crate::utils::TEST_HOSTS_DIR) .unwrap() .into_iter() .map(|p| p.unwrap()) diff --git a/src/utils.rs b/src/utils.rs index 9e1df4d..9c97da3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::{net::IpAddr, path::Path, str::FromStr}; +use std::{net::IpAddr, path::Path, str::FromStr, sync::Once}; use ipnetwork::IpNetwork; use log::warn; @@ -10,18 +10,33 @@ use zerotier_central_api::apis::configuration::Configuration; use anyhow::anyhow; +// collections of test hosts files +pub const TEST_HOSTS_DIR: &str = "testdata/hosts-files"; // default domain parameter. FIXME change to home.arpa. -pub(crate) const DOMAIN_NAME: &str = "domain."; +pub const DOMAIN_NAME: &str = "domain."; // zeronsd version calculated from Cargo.toml -pub(crate) const VERSION_STRING: &str = env!("CARGO_PKG_VERSION"); +pub const VERSION_STRING: &str = env!("CARGO_PKG_VERSION"); // this really needs to be replaced with lazy_static! magic fn version() -> String { "zeronsd ".to_string() + VERSION_STRING } +static LOGGER: Once = Once::new(); + +// initializes a logger +pub fn init_logger() { + LOGGER.call_once(|| { + env_logger::builder() + .filter_level(log::LevelFilter::Error) + .parse_default_env() + .parse_env("ZERONSD_LOG") + .init(); + }) +} + // this provides the production configuration for talking to central through the openapi libraries. -pub(crate) fn central_config(token: String) -> Configuration { +pub fn central_config(token: String) -> Configuration { let mut config = Configuration::default(); config.user_agent = Some(version()); config.bearer_access_token = Some(token); @@ -35,7 +50,7 @@ pub(crate) fn central_config(token: String) -> Configuration { // create a tokio runtime. We don't use the macros (they are hard to use) so this is the closest // we'll get to being able to get a runtime easily. -pub(crate) fn init_runtime() -> Runtime { +pub fn init_runtime() -> Runtime { tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(num_cpus::get()) @@ -45,14 +60,14 @@ pub(crate) fn init_runtime() -> Runtime { } // extracts the ip from the CIDR. 10.0.0.1/32 becomes 10.0.0.1 -pub(crate) fn parse_ip_from_cidr(ip_with_cidr: String) -> IpAddr { +pub fn parse_ip_from_cidr(ip_with_cidr: String) -> IpAddr { IpNetwork::from_str(&ip_with_cidr) .expect("Could not parse IP from CIDR") .ip() } // load and prepare the central API token -pub(crate) fn central_token(arg: Option<&Path>) -> Result { +pub fn central_token(arg: Option<&Path>) -> Result { if let Some(path) = arg { return Ok(std::fs::read_to_string(path) .expect("Could not load token file") @@ -70,7 +85,7 @@ pub(crate) fn central_token(arg: Option<&Path>) -> Result } // determine the path of the authtoken.secret -pub(crate) fn authtoken_path(arg: Option<&Path>) -> &Path { +pub fn authtoken_path(arg: Option<&Path>) -> &Path { if let Some(arg) = arg { return arg; } @@ -87,7 +102,7 @@ pub(crate) fn authtoken_path(arg: Option<&Path>) -> &Path { } // use the default tld if none is supplied. -pub(crate) fn domain_or_default(tld: Option<&str>) -> Result { +pub fn domain_or_default(tld: Option<&str>) -> Result { if let Some(tld) = tld { if tld.len() > 0 { return Ok(Name::from_str(&format!("{}.", tld))?); @@ -100,7 +115,7 @@ pub(crate) fn domain_or_default(tld: Option<&str>) -> Result, domain_name: Name) -> Option { +pub fn parse_member_name(name: Option, domain_name: Name) -> Option { if let Some(name) = name { let name = name.trim(); if name.len() > 0 { @@ -119,7 +134,7 @@ pub(crate) fn parse_member_name(name: Option, domain_name: Name) -> Opti // get_listen_ips returns the IPs that the network is providing to the instance running zeronsd. // 4193 and 6plane are handled up the stack. -pub(crate) async fn get_listen_ips( +pub async fn get_listen_ips( authtoken_path: &Path, network_id: &str, ) -> Result, anyhow::Error> { @@ -159,7 +174,7 @@ pub(crate) async fn get_listen_ips( } // update_central_dns pushes the search records -pub(crate) fn update_central_dns( +pub fn update_central_dns( runtime: &mut Runtime, domain_name: Name, ips: Vec, @@ -173,10 +188,10 @@ pub(crate) fn update_central_dns( let mut domain_name = domain_name.clone(); domain_name.set_fqdn(false); - let dns = Some(Box::new(zerotier_central_api::models::NetworkConfigDns { + let dns = Some(zerotier_central_api::models::Dns { domain: Some(domain_name.to_string()), servers: Some(ips), - })); + }); if let Some(mut zt_network_config) = zt_network.config.to_owned() { zt_network_config.dns = dns; @@ -198,7 +213,7 @@ fn translation_table() -> Vec<(Regex, &'static str)> { ] } -pub(crate) trait ToHostname { +pub trait ToHostname { fn to_hostname(self) -> Result; fn to_fqdn(self, domain: Name) -> Result; } diff --git a/src/authority/tests.rs b/tests/integration.rs similarity index 66% rename from src/authority/tests.rs rename to tests/integration.rs index 7fb8982..0907291 100644 --- a/src/authority/tests.rs +++ b/tests/integration.rs @@ -1,50 +1,6 @@ -use trust_dns_resolver::{IntoName, Name}; +use zeronsd::addresses::Calculator; -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -type SocketVec = Vec; - -pub(crate) trait ToIPv4Vec { - fn to_ipv4_vec(self) -> Vec; -} - -pub(crate) trait ToIPv6Vec { - fn to_ipv6_vec(self) -> Vec; -} - -pub(crate) trait ToPTRVec { - fn to_ptr_vec(self) -> Vec; -} - -impl ToIPv4Vec for SocketVec { - fn to_ipv4_vec(self) -> Vec { - self.into_iter() - .filter_map(|ip| match ip.ip() { - IpAddr::V4(ip) => Some(ip), - IpAddr::V6(_) => None, - }) - .collect::>() - } -} - -impl ToIPv6Vec for SocketVec { - fn to_ipv6_vec(self) -> Vec { - self.into_iter() - .filter_map(|ip| match ip.ip() { - IpAddr::V4(_) => None, - IpAddr::V6(ip) => Some(ip), - }) - .collect::>() - } -} - -impl ToPTRVec for SocketVec { - fn to_ptr_vec(self) -> Vec { - self.into_iter() - .map(|ip| ip.ip().into_name().unwrap()) - .collect::>() - } -} +mod service; mod sixplane { use std::{net::IpAddr, path::Path, str::FromStr, time::Duration}; @@ -53,21 +9,13 @@ mod sixplane { use rand::prelude::SliceRandom; use trust_dns_resolver::{IntoName, Name}; - use crate::{ - addresses::Calculator, - authority::{ - service::{HostsType, Lookup, Service, ServiceConfig}, - tests::ToIPv6Vec, - }, - hosts::parse_hosts, - integration_tests::init_test_logger, - tests::HOSTS_DIR, - }; + use crate::service::{HostsType, Lookup, Service, ServiceConfig, ToIPv6Vec}; + use zeronsd::{addresses::Calculator, hosts::parse_hosts, utils::init_logger}; - #[test] - fn test_battery_single_domain() { - init_test_logger(); - let service = Service::new(ServiceConfig::default().network_filename("6plane-only")); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_single_domain() { + init_logger(); + let service = Service::new(ServiceConfig::default().network_filename("6plane-only")).await; let record = service.member_record(); @@ -76,25 +24,26 @@ mod sixplane { listen_ips.sort(); for _ in 0..10000 { - let mut ips = service.lookup_aaaa(record.clone()); + let mut ips = service.lookup_aaaa(record.clone()).await; ips.sort(); assert_eq!(ips.sort(), listen_ips.clone().to_ipv6_vec().sort()); } } - #[test] - fn test_battery_single_domain_named() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_single_domain_named() { + init_logger(); let update_interval = Duration::new(20, 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"); + service.change_name("islay").await; let named_record = "islay.domain.".to_string(); @@ -105,28 +54,31 @@ mod sixplane { listen_ips.sort(); for _ in 0..10000 { - let mut ips = service.lookup_aaaa(record.clone()); + let mut ips = service.lookup_aaaa(record.clone()).await; ips.sort(); assert_eq!(ips, listen_ips.clone().to_ipv6_vec()); } } } - #[test] - fn test_battery_multi_domain_hosts_file() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_multi_domain_hosts_file() { + init_logger(); 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", HOSTS_DIR)).to_path_buf()), + Some( + Path::new(&format!("{}/basic-ipv6", zeronsd::utils::TEST_HOSTS_DIR)).to_path_buf(), + ), "domain.".into_name().unwrap(), ) .unwrap(); @@ -138,7 +90,7 @@ mod sixplane { for _ in 0..10000 { hosts.shuffle(&mut rand::thread_rng()); let host = *hosts.first().unwrap(); - let ip = service.lookup_aaaa(host.to_string()); + let ip = service.lookup_aaaa(host.to_string()).await; assert!(hosts_map .get(&IpAddr::V6(*ip.first().unwrap())) .unwrap() @@ -146,24 +98,26 @@ mod sixplane { } } - #[test] - fn test_wildcard_central() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_wildcard_central() { + init_logger(); let service = Service::new( ServiceConfig::default() .update_interval(Some(Duration::new(20, 0))) .network_filename("6plane-only") .wildcard_everything(true), - ); + ) + .await; let member_record = service.member_record(); let named_record = Name::from_str("islay.domain.").unwrap(); - service.change_name("islay"); + service.change_name("islay").await; assert_eq!( service .lookup_aaaa(named_record.to_string()) + .await .first() .unwrap(), &service.clone().any_listen_ip() @@ -172,6 +126,7 @@ mod sixplane { assert_eq!( service .lookup_aaaa(member_record.to_string()) + .await .first() .unwrap(), &service.clone().any_listen_ip() @@ -184,7 +139,7 @@ mod sixplane { .append_domain(&Name::from_str(&rec).unwrap()) .to_string(); assert_eq!( - service.lookup_aaaa(lookup).first().unwrap(), + service.lookup_aaaa(lookup).await.first().unwrap(), &service.clone().any_listen_ip() ); } @@ -199,21 +154,13 @@ mod rfc4193 { use rand::{prelude::SliceRandom, thread_rng}; use trust_dns_resolver::{IntoName, Name}; - use crate::{ - addresses::Calculator, - authority::{ - service::{HostsType, Lookup, Service, ServiceConfig}, - tests::{ToIPv6Vec, ToPTRVec}, - }, - hosts::parse_hosts, - integration_tests::init_test_logger, - tests::HOSTS_DIR, - }; + use crate::service::{HostsType, Lookup, Service, ServiceConfig, ToIPv6Vec, ToPTRVec}; + use zeronsd::{addresses::Calculator, hosts::parse_hosts, utils::init_logger}; - #[test] - fn test_battery_single_domain() { - init_test_logger(); - let service = Service::new(ServiceConfig::default().network_filename("rfc4193-only")); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_single_domain() { + init_logger(); + let service = Service::new(ServiceConfig::default().network_filename("rfc4193-only")).await; let record = service.member_record(); @@ -222,17 +169,17 @@ mod rfc4193 { listen_ips.sort(); for _ in 0..10000 { - let mut ips = service.lookup_aaaa(record.clone()); + let mut ips = service.lookup_aaaa(record.clone()).await; ips.sort(); assert_eq!(ips.sort(), listen_ips.clone().to_ipv6_vec().sort()); } - let ptr_records: Vec = service + let ptr_records: Vec = service .listen_ips .clone() .into_iter() - .map(|ip| ip.ip().into_name().unwrap()) + .map(|ip| ip.ip().to_string()) .collect(); for ptr_record in ptr_records.clone() { @@ -241,7 +188,11 @@ mod rfc4193 { for _ in 0..10000 { let service = service.clone(); assert_eq!( - service.lookup_ptr(ptr_record.to_string()).first().unwrap(), + service + .lookup_ptr(ptr_record.clone()) + .await + .first() + .unwrap(), &record.to_string() ); } @@ -253,7 +204,7 @@ mod rfc4193 { // randomly switch order if rand::random::() { assert_eq!( - service.lookup_aaaa(record.clone()).sort(), + service.lookup_aaaa(record.clone()).await.sort(), listen_ips.clone().to_ipv6_vec().sort() ); @@ -261,6 +212,7 @@ mod rfc4193 { service .clone() .lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string()) + .await .first() .unwrap(), &record.to_string() @@ -270,31 +222,34 @@ mod rfc4193 { service .clone() .lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string()) + .await .first() .unwrap(), &record.to_string() ); assert_eq!( - service.lookup_aaaa(record.clone()).sort(), + service.lookup_aaaa(record.clone()).await.sort(), listen_ips.clone().to_ipv6_vec().sort() ); } } } - #[test] - fn test_battery_single_domain_named() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_single_domain_named() { + init_logger(); let update_interval = Duration::new(20, 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"); + service.change_name("islay").await; let named_record = "islay.domain.".to_string(); @@ -305,13 +260,13 @@ mod rfc4193 { listen_ips.sort(); for _ in 0..10000 { - let mut ips = service.lookup_aaaa(record.clone()); + let mut ips = service.lookup_aaaa(record.clone()).await; ips.sort(); assert_eq!(ips, listen_ips.clone().to_ipv6_vec()); } } - let ptr_records: Vec = service.listen_ips.clone().to_ptr_vec(); + let ptr_records: Vec = service.listen_ips.clone().to_ptr_vec(); for ptr_record in ptr_records { info!("Looking up {}", ptr_record); @@ -319,28 +274,35 @@ mod rfc4193 { for _ in 0..10000 { let service = service.clone(); assert_eq!( - service.lookup_ptr(ptr_record.to_string()).first().unwrap(), + service + .lookup_ptr(ptr_record.clone()) + .await + .first() + .unwrap(), &named_record.to_string() ); } } } - #[test] - fn test_battery_multi_domain_hosts_file() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_multi_domain_hosts_file() { + init_logger(); 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", HOSTS_DIR)).to_path_buf()), + Some( + Path::new(&format!("{}/basic-ipv6", zeronsd::utils::TEST_HOSTS_DIR)).to_path_buf(), + ), "domain.".into_name().unwrap(), ) .unwrap(); @@ -352,7 +314,7 @@ mod rfc4193 { for _ in 0..10000 { hosts.shuffle(&mut rand::thread_rng()); let host = *hosts.first().unwrap(); - let ip = service.lookup_aaaa(host.to_string()); + let ip = service.lookup_aaaa(host.to_string()).await; assert!(hosts_map .get(&IpAddr::V6(*ip.first().unwrap())) .unwrap() @@ -360,24 +322,26 @@ mod rfc4193 { } } - #[test] - fn test_wildcard_central() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_wildcard_central() { + init_logger(); let service = Service::new( ServiceConfig::default() .update_interval(Some(Duration::new(20, 0))) .network_filename("rfc4193-only") .wildcard_everything(true), - ); + ) + .await; let member_record = service.member_record(); let named_record = Name::from_str("islay.domain.").unwrap(); - service.change_name("islay"); + service.change_name("islay").await; assert_eq!( service .lookup_aaaa(named_record.to_string()) + .await .first() .unwrap(), &service.clone().any_listen_ip() @@ -386,6 +350,7 @@ mod rfc4193 { assert_eq!( service .lookup_aaaa(member_record.to_string()) + .await .first() .unwrap(), &service.clone().any_listen_ip() @@ -398,7 +363,7 @@ mod rfc4193 { .append_domain(&Name::from_str(&rec).unwrap()) .to_string(); assert_eq!( - service.lookup_aaaa(lookup).first().unwrap(), + service.lookup_aaaa(lookup).await.first().unwrap(), &service.clone().any_listen_ip() ); } @@ -407,41 +372,46 @@ mod rfc4193 { } mod ipv4 { - use std::{str::FromStr, time::Duration}; + use std::time::Duration; use log::info; - use rand::{prelude::SliceRandom, thread_rng}; - use trust_dns_resolver::{IntoName, Name}; + use std::str::FromStr; + use trust_dns_resolver::Name; - use crate::{ - authority::{ - service::{Lookup, Service, ServiceConfig}, - tests::{ToIPv4Vec, ToPTRVec}, - }, - integration_tests::init_test_logger, - }; + use zeronsd::utils::init_logger; + + use crate::service::{Lookup, Service, ServiceConfig, ToIPv4Vec, ToPTRVec}; - #[test] - fn test_wildcard_central() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_wildcard_central() { + init_logger(); let service = Service::new( ServiceConfig::default() .update_interval(Some(Duration::new(20, 0))) .wildcard_everything(true), - ); + ) + .await; let member_record = service.member_record(); let named_record = Name::from_str("islay.domain.").unwrap(); - service.change_name("islay"); + service.change_name("islay").await; assert_eq!( - service.lookup_a(named_record.to_string()).first().unwrap(), + service + .lookup_a(named_record.to_string()) + .await + .first() + .unwrap(), &service.clone().any_listen_ip() ); assert_eq!( - service.lookup_a(member_record.to_string()).first().unwrap(), + service + .lookup_a(member_record.to_string()) + .await + .first() + .unwrap(), &service.clone().any_listen_ip() ); @@ -452,21 +422,24 @@ mod ipv4 { .append_domain(&Name::from_str(&rec).unwrap()) .to_string(); assert_eq!( - service.lookup_a(lookup).first().unwrap(), + service.lookup_a(lookup).await.first().unwrap(), &service.clone().any_listen_ip() ); } } } - #[test] - fn test_battery_single_domain() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_single_domain() { + use rand::{seq::SliceRandom, thread_rng}; + + init_logger(); 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(); @@ -475,17 +448,17 @@ mod ipv4 { listen_ips.sort(); for _ in 0..10000 { - let mut ips = service.lookup_a(record.clone()); + let mut ips = service.lookup_a(record.clone()).await; ips.sort(); assert_eq!(ips.sort(), listen_ips.clone().to_ipv4_vec().sort()); } - let ptr_records: Vec = service + let ptr_records: Vec = service .listen_ips .clone() .into_iter() - .map(|ip| ip.ip().into_name().unwrap()) + .map(|ip| ip.ip().to_string()) .collect(); for ptr_record in ptr_records.clone() { @@ -494,7 +467,11 @@ mod ipv4 { for _ in 0..10000 { let service = service.clone(); assert_eq!( - service.lookup_ptr(ptr_record.to_string()).first().unwrap(), + service + .lookup_ptr(ptr_record.clone()) + .await + .first() + .unwrap(), &record.to_string() ); } @@ -506,7 +483,7 @@ mod ipv4 { // randomly switch order if rand::random::() { assert_eq!( - service.lookup_a(record.clone()).sort(), + service.lookup_a(record.clone()).await.sort(), listen_ips.clone().to_ipv4_vec().sort() ); @@ -514,6 +491,7 @@ mod ipv4 { service .clone() .lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string()) + .await .first() .unwrap(), &record.to_string() @@ -523,31 +501,34 @@ mod ipv4 { service .clone() .lookup_ptr(ptr_records.choose(&mut thread_rng()).unwrap().to_string()) + .await .first() .unwrap(), &record.to_string() ); assert_eq!( - service.lookup_a(record.clone()).sort(), + service.lookup_a(record.clone()).await.sort(), listen_ips.clone().to_ipv4_vec().sort() ); } } } - #[test] - fn test_battery_single_domain_named() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_single_domain_named() { + init_logger(); let update_interval = Duration::new(20, 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"); + service.change_name("islay").await; let named_record = "islay.domain.".to_string(); @@ -558,13 +539,13 @@ mod ipv4 { listen_ips.sort(); for _ in 0..10000 { - let mut ips = service.lookup_a(record.clone()); + let mut ips = service.lookup_a(record.clone()).await; ips.sort(); assert_eq!(ips, listen_ips.clone().to_ipv4_vec()); } } - let ptr_records: Vec = service.listen_ips.clone().to_ptr_vec(); + let ptr_records: Vec = service.listen_ips.clone().to_ptr_vec(); for ptr_record in ptr_records { info!("Looking up {}", ptr_record); @@ -572,7 +553,11 @@ mod ipv4 { for _ in 0..10000 { let service = service.clone(); assert_eq!( - service.lookup_ptr(ptr_record.to_string()).first().unwrap(), + service + .lookup_ptr(ptr_record.clone()) + .await + .first() + .unwrap(), &named_record.to_string() ); } @@ -585,13 +570,13 @@ mod all { use rand::prelude::SliceRandom; use trust_dns_resolver::{IntoName, Name}; - use crate::{ - authority::service::{HostsType, Lookup, Service, ServiceConfig}, + use zeronsd::{ hosts::parse_hosts, - integration_tests::init_test_logger, - tests::HOSTS_DIR, + utils::{init_logger, TEST_HOSTS_DIR}, }; + use crate::service::{HostsType, Lookup, Service, ServiceConfig}; + use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, path::Path, @@ -600,22 +585,23 @@ mod all { time::Duration, }; - #[test] - fn test_battery_multi_domain_hosts_file() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_battery_multi_domain_hosts_file() { + init_logger(); 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", HOSTS_DIR)).to_path_buf()), + Some(Path::new(&format!("{}/basic", TEST_HOSTS_DIR)).to_path_buf()), "domain.".into_name().unwrap(), ) .unwrap(); @@ -631,7 +617,7 @@ mod all { for _ in 0..10000 { hosts.shuffle(&mut rand::thread_rng()); let host = *hosts.first().unwrap(); - let ips = service.lookup_a(host.to_string()); + let ips = service.lookup_a(host.to_string()).await; assert!(hosts_map .get(&IpAddr::from(*ips.first().unwrap())) .unwrap() @@ -639,20 +625,22 @@ mod all { } } - #[test] - fn test_hosts_file_reloading() { - init_test_logger(); + #[tokio::test(flavor = "multi_thread")] + async fn test_hosts_file_reloading() { + init_logger(); 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(20, 0))), - ); + ) + .await; assert_eq!( service .lookup_a("islay.domain.".to_string()) + .await .first() .unwrap(), &Ipv4Addr::from_str("127.0.0.2").unwrap() @@ -661,6 +649,7 @@ mod all { assert_eq!( service .lookup_aaaa("islay.domain.".to_string()) + .await .first() .unwrap(), &Ipv6Addr::from_str("::2").unwrap() @@ -672,6 +661,7 @@ mod all { assert_eq!( service .lookup_a("islay.domain.".to_string()) + .await .first() .unwrap(), &Ipv4Addr::from_str("127.0.0.3").unwrap() @@ -680,9 +670,70 @@ mod all { assert_eq!( service .lookup_aaaa("islay.domain.".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 service::*; + use zeronsd::utils::*; + + init_logger(); + + 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()).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(); + + let mut listen_ips = + get_listen_ips(&authtoken_path(None), &tn.network.clone().id.unwrap()).await?; + + assert_eq!(listen_ips.sort(), ips.sort()); + eprintln!("My listen IPs are {}", listen_ips.join(", ")); + + let tn = TestNetwork::new("rfc4193-only", &mut TestContext::default().await) + .await + .unwrap(); + + let mut listen_ips = + get_listen_ips(&authtoken_path(None), &tn.network.clone().id.unwrap()).await?; + + let mut ips = vec![tn.member().clone().rfc4193()?.ip().to_string()]; + + assert_eq!(listen_ips.sort(), ips.sort()); + 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 = + get_listen_ips(&authtoken_path(None), &tn.network.clone().id.unwrap()).await?; + + let mut ips = vec![tn.member().clone().sixplane()?.ip().to_string()]; + + assert_eq!(listen_ips.sort(), ips.sort()); + eprintln!("My listen IPs are {}", listen_ips.join(", ")); + + Ok(()) +} diff --git a/tests/service.rs b/tests/service.rs new file mode 100644 index 0000000..1363d53 --- /dev/null +++ b/tests/service.rs @@ -0,0 +1,702 @@ +/* + * Service abstraction provides a way to automatically generate services that are attached to + * TestNetworks for testing against. Each Service is composed of a DNS service attached to a + * TestNetwork. The service can then be resolved against, for example. Several parameters for + * managing the underlying TestNetwork, and the Service are available via the ServiceConfig struct. + */ + +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + path::Path, + str::FromStr, + sync::Arc, + time::Duration, +}; + +use async_trait::async_trait; +use ipnetwork::IpNetwork; +use lazy_static::lazy_static; +use log::{info, warn}; +use rand::prelude::{IteratorRandom, SliceRandom}; +use tokio::{ + sync::{Mutex, RwLock}, + task::JoinHandle, +}; +use trust_dns_resolver::{ + config::{NameServerConfig, ResolverConfig, ResolverOpts}, + name_server::{GenericConnection, GenericConnectionProvider, TokioRuntime}, + AsyncResolver, +}; + +use zeronsd::{ + addresses::Calculator, + authority::{find_members, init_trust_dns_authority, new_ptr_authority, ZTAuthority}, + server::Server, + utils::{ + authtoken_path, central_config, domain_or_default, get_listen_ips, parse_ip_from_cidr, + }, +}; + +use zerotier_central_api::{ + apis::configuration::Configuration, + models::{Member, MemberConfig, Network}, +}; + +fn randstring(len: u8) -> String { + (0..len) + .map(|_| (rand::random::() % 26) + 'a' as u8) + .map(|c| { + if rand::random::() { + (c as char).to_ascii_uppercase() + } else { + c as char + } + }) + .map(|c| c.to_string()) + .collect::>() + .join("") +} + +// extract a network definiton from testdata. templates in a new name. +fn network_definition( + name: String, +) -> Result, anyhow::Error> { + let mut res: serde_json::Map = serde_json::from_reader( + std::fs::File::open(format!("testdata/networks/{}.json", name))?, + )?; + + if let serde_json::Value::Object(config) = res.clone().get("config").unwrap() { + let mut new_config = config.clone(); + new_config.insert( + "name".to_string(), + serde_json::Value::String(randstring(30)), + ); + + res.insert("config".to_string(), serde_json::Value::Object(new_config)); + } + + Ok(res) +} + +// returns the public identity of this instance of zerotier +pub async fn get_identity( + configuration: &zerotier_one_api::apis::configuration::Configuration, +) -> Result { + let status = zerotier_one_api::apis::status_api::get_status(configuration).await?; + + Ok(status + .public_identity + .unwrap() + .splitn(3, ":") + .nth(0) + .unwrap() + .to_owned()) +} + +// unpack the authtoken based on what we're passed +pub fn get_authtoken(or: Option<&str>) -> Result { + Ok(std::fs::read_to_string(authtoken_path( + or.map(|c| Path::new(c)), + ))?) +} + +// zerotier_config returns the openapi configuration required to talk to the local ztone instance +pub fn zerotier_config(authtoken: String) -> zerotier_one_api::apis::configuration::Configuration { + let mut zerotier = zerotier_one_api::apis::configuration::Configuration::default(); + zerotier.api_key = Some(zerotier_one_api::apis::configuration::ApiKey { + prefix: None, + key: authtoken.clone(), + }); + + zerotier +} + +// monkeypatches to Member +pub trait MemberUtil { + // set some member defaults for testing + fn set_defaults(&mut self, network_id: String, identity: String); +} + +// monkeypatches to MemberConfig +pub trait MemberConfigUtil { + fn set_ip_assignments(&mut self, ips: Vec<&str>); + fn set_defaults(&mut self, identity: String); +} + +impl MemberUtil for Member { + fn set_defaults(&mut self, network_id: String, identity: String) { + self.node_id = Some(identity.clone()); + self.network_id = Some(network_id); + let mut mc = MemberConfig::new(); + mc.set_defaults(identity); + self.config = Some(Box::new(mc)); + } +} + +impl MemberConfigUtil for MemberConfig { + fn set_ip_assignments(&mut self, ips: Vec<&str>) { + self.ip_assignments = Some(ips.into_iter().map(|s| s.to_string()).collect()) + } + + fn set_defaults(&mut self, identity: String) { + self.v_rev = None; + self.v_major = None; + self.v_proto = None; + self.v_minor = None; + self.tags = None; + self.revision = None; + self.no_auto_assign_ips = Some(false); + self.last_authorized_time = None; + self.last_deauthorized_time = None; + self.id = None; + self.creation_time = None; + self.capabilities = None; + self.ip_assignments = None; + self.authorized = Some(true); + self.active_bridge = None; + self.identity = Some(identity); + } +} + +// TestContext provides all the stuff we need to talk to run tests smoothly +#[derive(Clone)] +pub struct TestContext { + member_config: Option>, + identity: String, + zerotier: zerotier_one_api::apis::configuration::Configuration, + central: Configuration, +} + +impl TestContext { + pub fn get_member(&mut self, network_id: String) -> Member { + let mut member = Member::new(); + member.set_defaults(network_id, self.identity.clone()); + if self.member_config.is_some() { + member.config = self.member_config.clone(); + } + + member + } + + pub async fn default() -> Self { + let authtoken = get_authtoken(None).expect("Could not read authtoken"); + let zerotier = zerotier_config(authtoken.clone()); + let identity = get_identity(&zerotier) + .await + .expect("Could not retrieve identity from zerotier"); + + let token = std::env::var("TOKEN").expect("Please provide TOKEN in the environment"); + let central = central_config(token.clone()); + + Self { + member_config: None, + identity, + zerotier, + central, + } + } +} + +lazy_static! { + static ref NETWORKS: Arc>> = + Arc::new(Mutex::new(HashMap::new())); +} + +// TestNetwork creates a testnetwork in central and joins it. When this data is destroyed/dropped +// it will remove the network and leave it like nothing ever happened. +#[derive(Clone)] +pub struct TestNetwork { + pub network: Network, + context: TestContext, + member: Member, +} + +impl TestNetwork { + // new_multi_ip covers situations where zeronsd is using more than one listening ip. + pub async fn new_multi_ip( + network_def: &str, + tc: &mut TestContext, + ips: Vec<&str>, + ) -> Result { + let mut mc = MemberConfig::new(); + mc.set_defaults(tc.identity.clone()); + mc.set_ip_assignments(ips); + tc.member_config = Some(Box::new(mc)); + Self::new(network_def, tc).await + } + + // constructor. + pub async fn new(network_def: &str, tc: &mut TestContext) -> Result { + let network = zerotier_central_api::apis::network_api::new_network( + &tc.central, + serde_json::Value::Object(network_definition(network_def.to_string())?), + ) + .await + .unwrap(); + + let member = tc.get_member(network.clone().id.unwrap()); + + zerotier_central_api::apis::network_member_api::update_network_member( + &tc.central, + &network.clone().id.unwrap(), + &tc.identity, + member.clone(), + ) + .await + .unwrap(); + + let s = Self { + network, + member, + context: tc.clone(), + }; + + s.join().await.unwrap(); + + Ok(s) + } + + // join zerotier-one to the test network + pub async fn join(&self) -> Result<(), anyhow::Error> { + let network = zerotier_one_api::models::Network::new(); + zerotier_one_api::apis::network_api::update_network( + &self.context.zerotier, + &self.network.id.clone().unwrap(), + network, + ) + .await?; + + let id = self.network.id.clone().unwrap(); + let mut count = 0; + + while let Err(e) = get_listen_ips(&authtoken_path(None), &id).await { + tokio::time::sleep(Duration::new(1, 0)).await; + count += 1; + if count >= 5 { + warn!("5 attempts: While joining network: {:?}", e); + count = 0; + } + } + Ok(()) + } + + // leave the test network + pub async fn leave(&self) -> Result<(), anyhow::Error> { + Ok(zerotier_one_api::apis::network_api::delete_network( + &self.context.zerotier, + &self.network.id.clone().unwrap(), + ) + .await?) + } + + pub fn identity(&self) -> String { + self.context.identity.clone() + } + + pub fn central(&self) -> Configuration { + self.context.central.clone() + } + + pub fn member(&self) -> Member { + self.member.clone() + } + + pub fn teardown(&mut self) { + let identity = self.identity(); + tokio::task::block_in_place(move || { + tokio::runtime::Handle::current().block_on(async { + if let Some(network) = NETWORKS.lock().await.remove(&identity) { + network.leave().await.unwrap(); + let central = network.central(); + zerotier_central_api::apis::network_api::delete_network(¢ral, &identity) + .await + .unwrap(); + } + }) + }) + } +} + +// drop just removes the network from central and leaves it. it tries to recover, not get more +// angry, in the face of errors. +impl Drop for TestNetwork { + fn drop(&mut self) { + self.teardown() + } +} + +type SocketVec = Vec; + +pub trait ToIPv4Vec { + fn to_ipv4_vec(self) -> Vec; +} + +pub trait ToIPv6Vec { + fn to_ipv6_vec(self) -> Vec; +} + +pub trait ToPTRVec { + fn to_ptr_vec(self) -> Vec; +} + +impl ToIPv4Vec for SocketVec { + fn to_ipv4_vec(self) -> Vec { + self.into_iter() + .filter_map(|ip| match ip.ip() { + IpAddr::V4(ip) => Some(ip), + IpAddr::V6(_) => None, + }) + .collect::>() + } +} + +impl ToIPv6Vec for SocketVec { + fn to_ipv6_vec(self) -> Vec { + self.into_iter() + .filter_map(|ip| match ip.ip() { + IpAddr::V4(_) => None, + IpAddr::V6(ip) => Some(ip), + }) + .collect::>() + } +} + +impl ToPTRVec for SocketVec { + fn to_ptr_vec(self) -> Vec { + self.into_iter() + .map(|ip| ip.ip().to_string()) + .collect::>() + } +} + +lazy_static! { + static ref SERVERS: Arc>>>> = + Arc::new(Mutex::new(Vec::new())); + static ref AUTHORITY_HANDLE: Arc>>> = Arc::new(Mutex::new(None)); +} + +#[derive(Clone)] +pub struct Service { + tn: Arc, + resolvers: Resolvers, + update_interval: Option, + pub listen_ips: Vec, +} + +#[async_trait] +pub trait Lookup { + async fn lookup_a(&self, record: String) -> Vec; + async fn lookup_aaaa(&self, record: String) -> Vec; + async fn lookup_ptr(&self, record: String) -> Vec; +} + +#[async_trait] +impl Lookup for Resolver { + async fn lookup_a(&self, record: String) -> Vec { + self.ipv4_lookup(record) + .await + .unwrap() + .as_lookup() + .record_iter() + .map(|r| r.rdata().clone().into_a().unwrap()) + .collect() + } + + async fn lookup_aaaa(&self, record: String) -> Vec { + self.ipv6_lookup(record) + .await + .unwrap() + .as_lookup() + .record_iter() + .map(|r| r.rdata().clone().into_aaaa().unwrap()) + .collect() + } + + async fn lookup_ptr(&self, record: String) -> Vec { + self.reverse_lookup(record.parse().unwrap()) + .await + .unwrap() + .as_lookup() + .record_iter() + .map(|r| r.rdata().clone().into_ptr().unwrap().to_string()) + .collect() + } +} + +pub enum HostsType { + Path(&'static str), + Fixture(&'static str), + None, +} + +async fn create_listeners( + tn: &TestNetwork, + hosts: HostsType, + update_interval: Option, + wildcard_everything: bool, +) -> ( + Vec, + JoinHandle<()>, + Vec>>, +) { + let listen_cidrs = get_listen_ips(&authtoken_path(None), &tn.network.clone().id.unwrap()) + .await + .unwrap(); + + let mut listen_ips = Vec::new(); + + let mut ipmap = HashMap::new(); + let mut authority_map = HashMap::new(); + let authority = init_trust_dns_authority(domain_or_default(None).unwrap()); + + for cidr in listen_cidrs.clone() { + let listen_ip = parse_ip_from_cidr(cidr.clone()); + let socket_addr = SocketAddr::new(listen_ip.clone(), 53); + listen_ips.push(socket_addr); + let cidr = IpNetwork::from_str(&cidr.clone()).unwrap(); + if !ipmap.contains_key(&listen_ip) { + ipmap.insert(listen_ip, cidr.network()); + } + + if !authority_map.contains_key(&cidr) { + let ptr_authority = new_ptr_authority(cidr).unwrap(); + authority_map.insert(cidr, ptr_authority.clone()); + } + } + + if let Some(v6assign) = tn.network.config.clone().unwrap().v6_assign_mode { + if v6assign.rfc4193.unwrap_or(false) { + let cidr = tn.network.clone().rfc4193().unwrap(); + if !authority_map.contains_key(&cidr) { + let ptr_authority = new_ptr_authority(cidr).unwrap(); + authority_map.insert(cidr, ptr_authority); + } + } + } + + let update_interval = update_interval.unwrap_or(Duration::new(10, 0)); + + let mut ztauthority = ZTAuthority::new( + domain_or_default(None).unwrap(), + tn.network.clone().id.unwrap(), + tn.central(), + match hosts { + HostsType::Fixture(hosts) => Some( + Path::new(&format!("{}/{}", zeronsd::utils::TEST_HOSTS_DIR, hosts)).to_path_buf(), + ), + HostsType::Path(hosts) => Some(Path::new(hosts).to_path_buf()), + HostsType::None => None, + }, + authority_map, + update_interval, + authority.clone(), + ); + + if wildcard_everything { + ztauthority.wildcard_everything(); + } + + let arc_authority = Arc::new(RwLock::new(ztauthority)); + let authority_handle = tokio::spawn(find_members(arc_authority.clone())); + let mut servers = Vec::new(); + + tokio::time::sleep(Duration::new(5, 0)).await; + + for ip in listen_ips.clone() { + let server = Server::new(arc_authority.to_owned()); + info!("Serving {}", ip.clone()); + servers.push(tokio::spawn(server.listen(ip, Duration::new(0, 1000)))); + } + + (listen_ips, authority_handle, servers) +} + +type Resolver = AsyncResolver>; + +type Resolvers = Vec>; + +fn create_resolvers(sockets: Vec) -> Resolvers { + let mut resolvers = Vec::new(); + + for socket in sockets { + let mut resolver_config = ResolverConfig::new(); + resolver_config.add_search(domain_or_default(None).unwrap()); + resolver_config.add_name_server(NameServerConfig { + socket_addr: socket, + protocol: trust_dns_resolver::config::Protocol::Udp, + tls_dns_name: None, + trust_nx_responses: true, + }); + + let mut opts = ResolverOpts::default(); + opts.cache_size = 0; + opts.rotate = true; + opts.use_hosts_file = false; + opts.positive_min_ttl = Some(Duration::new(0, 0)); + opts.positive_max_ttl = Some(Duration::new(0, 0)); + opts.negative_min_ttl = Some(Duration::new(0, 0)); + opts.negative_max_ttl = Some(Duration::new(0, 0)); + + resolvers.push(Arc::new( + trust_dns_resolver::TokioAsyncResolver::tokio(resolver_config, opts).unwrap(), + )); + } + + resolvers +} + +pub struct ServiceConfig { + hosts: HostsType, + update_interval: Option, + ips: Option>, + wildcard_everything: bool, + network_filename: Option<&'static str>, +} + +impl Default for ServiceConfig { + fn default() -> Self { + Self { + network_filename: None, + hosts: HostsType::None, + update_interval: None, + ips: None, + wildcard_everything: false, + } + } +} + +impl ServiceConfig { + pub fn network_filename(mut self, n: &'static str) -> Self { + self.network_filename = Some(n); + self + } + + pub fn hosts(mut self, h: HostsType) -> Self { + self.hosts = h; + self + } + + pub fn update_interval(mut self, u: Option) -> Self { + self.update_interval = u; + self + } + + pub fn ips(mut self, ips: Option>) -> Self { + self.ips = ips; + self + } + + pub fn wildcard_everything(mut self, w: bool) -> Self { + self.wildcard_everything = w; + self + } +} + +impl Service { + pub async fn new(sc: ServiceConfig) -> Self { + let network_filename = sc.network_filename.unwrap_or("basic-ipv4"); + let tn = if let Some(ips) = sc.ips { + TestNetwork::new_multi_ip(network_filename, &mut TestContext::default().await, ips) + .await + .unwrap() + } else { + TestNetwork::new(network_filename, &mut TestContext::default().await) + .await + .unwrap() + }; + + let (listen_ips, authority_handle, servers) = + create_listeners(&tn, sc.hosts, sc.update_interval, sc.wildcard_everything).await; + + let mut lock = SERVERS.lock().await; + + for server in servers { + lock.push(server); + } + + let mut lock = AUTHORITY_HANDLE.lock().await; + lock.replace(authority_handle); + + Self { + tn: Arc::new(tn), + resolvers: create_resolvers(listen_ips.clone()), + listen_ips, + update_interval: sc.update_interval, + } + } + + pub fn any_listen_ip(self) -> IpAddr { + self.listen_ips + .clone() + .into_iter() + .choose(&mut rand::thread_rng()) + .unwrap() + .clone() + .ip() + } + + pub fn network(&self) -> Arc { + self.tn.clone() + } + + pub fn resolvers(&self) -> Resolvers { + self.resolvers.clone() + } + + pub fn any_resolver(&self) -> Arc { + self.resolvers() + .choose(&mut rand::thread_rng()) + .to_owned() + .unwrap() + .clone() + } + + pub fn member_record(&self) -> String { + format!("zt-{}.domain.", self.network().identity().clone()) + } + + pub async fn change_name(&self, name: &'static str) { + let mut member = zerotier_central_api::apis::network_member_api::get_network_member( + &self.network().central(), + &self.network().network.clone().id.unwrap(), + &self.network().identity(), + ) + .await + .unwrap(); + + member.name = Some(name.to_string()); + + zerotier_central_api::apis::network_member_api::update_network_member( + &self.network().central(), + &self.network().network.clone().id.unwrap(), + &self.network().identity(), + member, + ) + .await + .unwrap(); + + if self.update_interval.is_some() { + tokio::time::sleep(self.update_interval.unwrap()).await; // wait for it to update + } + } + + pub fn test_network(&self) -> Arc { + self.tn.clone() + } +} + +#[async_trait] +impl Lookup for Service { + async fn lookup_a(&self, record: String) -> Vec { + self.any_resolver().lookup_a(record).await + } + + async fn lookup_aaaa(&self, record: String) -> Vec { + self.any_resolver().lookup_aaaa(record).await + } + + async fn lookup_ptr(&self, record: String) -> Vec { + self.any_resolver().lookup_ptr(record).await + } +}