diff --git a/syncwhole/src/utils.rs b/syncwhole/src/utils.rs index 9c72d5264..43213523e 100644 --- a/syncwhole/src/utils.rs +++ b/syncwhole/src/utils.rs @@ -72,7 +72,7 @@ pub fn random() -> u64 { s0 = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos() as u64; } if s1 == 0 { - s1 = splitmix64((std::process::id() as u64).wrapping_add((unsafe { &RANDOM_STATE_0 } as *const u64) as u64)); + s1 = splitmix64(std::process::id() as u64); } let s1_new = xorshift64(s1); s0 = splitmix64(s0.wrapping_add(s1)); diff --git a/zerotier-network-hypervisor/src/networkhypervisor.rs b/zerotier-network-hypervisor/src/networkhypervisor.rs index c7d4a4984..f8ceb2733 100644 --- a/zerotier-network-hypervisor/src/networkhypervisor.rs +++ b/zerotier-network-hypervisor/src/networkhypervisor.rs @@ -10,7 +10,7 @@ use std::num::NonZeroI64; use std::time::Duration; use crate::error::InvalidParameterError; -use crate::vl1::{Address, Endpoint, Identity, Node, SystemInterface}; +use crate::vl1::{Address, Endpoint, Identity, Node, RootSet, SystemInterface}; use crate::vl2::{Switch, SwitchInterface}; use crate::PacketBuffer; @@ -29,9 +29,6 @@ impl NetworkHypervisor { }) } - /// Obtain a new packet buffer from the buffer pool. - /// - /// The returned object is a Pooled> instance. The buffer is returned to the pool when the container is destroyed. #[inline(always)] pub fn get_packet_buffer(&self) -> PacketBuffer { self.vl1.get_packet_buffer() @@ -47,12 +44,18 @@ impl NetworkHypervisor { &self.vl1.identity } + #[inline(always)] pub fn do_background_tasks(&self, ii: &I) -> Duration { self.vl1.do_background_tasks(ii) } #[inline(always)] - pub fn wire_receive(&self, ii: &I, source_endpoint: &Endpoint, source_local_socket: Option, source_local_interface: Option, mut data: PacketBuffer) { + pub fn wire_receive(&self, ii: &I, source_endpoint: &Endpoint, source_local_socket: Option, source_local_interface: Option, data: PacketBuffer) { self.vl1.wire_receive(ii, &self.vl2, source_endpoint, source_local_socket, source_local_interface, data) } + + #[inline(always)] + pub fn add_update_root_set(&self, rs: RootSet) -> bool { + self.vl1.add_update_root_set(rs) + } } diff --git a/zerotier-network-hypervisor/src/vl1/node.rs b/zerotier-network-hypervisor/src/vl1/node.rs index f8ea74614..d37699bb5 100644 --- a/zerotier-network-hypervisor/src/vl1/node.rs +++ b/zerotier-network-hypervisor/src/vl1/node.rs @@ -414,6 +414,16 @@ impl Node { self.roots.lock().roots.contains_key(peer) } + /// Add or update a root set. + /// + /// If no root set exists by this name, a new root set is added. If one already + /// exists it's checked against the new one and updated if the new set is valid + /// and should supersede it. + /// + /// Changes will take effect within a few seconds when root sets are next + /// examined and synchronized with peer and root list state. + /// + /// This returns true if the new root set was accepted and false otherwise. pub fn add_update_root_set(&self, rs: RootSet) -> bool { let mut roots = self.roots.lock(); let entry = roots.sets.get_mut(&rs.name); @@ -422,16 +432,17 @@ impl Node { if rs.should_replace(old_rs) { *old_rs = rs; roots.sets_modified = true; - return true; + true + } else { + false } + } else if rs.verify() { + roots.sets.insert(rs.name.clone(), rs); + roots.sets_modified = true; + true } else { - if rs.verify() { - roots.sets.insert(rs.name.clone(), rs); - roots.sets_modified = true; - return true; - } + false } - return false; } /// Get the canonical Path object for a given endpoint and local socket information. diff --git a/zerotier-system-service/src/datadir.rs b/zerotier-system-service/src/datadir.rs new file mode 100644 index 000000000..85502f4ac --- /dev/null +++ b/zerotier-system-service/src/datadir.rs @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * (c)2021 ZeroTier, Inc. + * https://www.zerotier.com/ + */ + +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use crate::localconfig; +use crate::utils::{read_limit, DEFAULT_FILE_IO_READ_LIMIT}; + +use tokio::sync::{Mutex, RwLock, RwLockReadGuard}; + +use zerotier_core_crypto::random::next_u32_secure; +use zerotier_network_hypervisor::vl1::identity::{Identity, IDENTITY_ALGORITHM_ALL}; + +const AUTH_TOKEN_DEFAULT_LENGTH: usize = 64; +const AUTH_TOKEN_POSSIBLE_CHARS: &'static str = "0123456789abcdefghijklmnopqrstuvwxyz"; +const AUTH_TOKEN_FILENAME: &'static str = "authtoken.secret"; +const IDENTITY_PUBLIC_FILENAME: &'static str = "identity.public"; +const IDENTITY_SECRET_FILENAME: &'static str = "identity.secret"; +const CONFIG_FILENAME: &'static str = "local.conf"; + +/// Abstraction around ZeroTier's home data directory. +pub struct DataDir { + pub base_path: PathBuf, + config: RwLock, + authtoken: Mutex, +} + +impl DataDir { + pub async fn open>(path: P) -> std::io::Result { + let base_path = path.as_ref().to_path_buf(); + if !base_path.is_dir() { + let _ = std::fs::create_dir_all(&base_path); + if !base_path.is_dir() { + return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "base path not found and cannot be created")); + } + } + + let config_path = base_path.join(CONFIG_FILENAME); + let config_data = read_limit(&config_path, DEFAULT_FILE_IO_READ_LIMIT).await; + let config = RwLock::new(if config_data.is_ok() { + let c = serde_json::from_slice::(config_data.unwrap().as_slice()); + if c.is_err() { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, c.err().unwrap())); + } + c.unwrap() + } else { + if config_path.is_file() { + return Err(std::io::Error::new(std::io::ErrorKind::PermissionDenied, "local.conf not readable")); + } else { + localconfig::Config::default() + } + }); + + return Ok(Self { base_path, config, authtoken: Mutex::new(String::new()) }); + } + + /// Load identity.secret from data directory. + pub async fn load_identity(&self) -> std::io::Result { + let id_data = Identity::from_str(String::from_utf8_lossy(read_limit(self.base_path.join(IDENTITY_PUBLIC_FILENAME), 4096).await?.as_slice()).as_ref()); + if id_data.is_err() { + return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, id_data.err().unwrap())); + } + Ok(id_data.unwrap()) + } + + /// Save identity.secret and identity.public to data directory. + pub async fn save_identity(&self, id: &Identity) -> std::io::Result<()> { + assert!(id.secret.is_some()); + let id_secret_str = id.to_string_with_options(IDENTITY_ALGORITHM_ALL, true); + let id_public_str = id.to_string_with_options(IDENTITY_ALGORITHM_ALL, false); + let secret_path = self.base_path.join(IDENTITY_SECRET_FILENAME); + tokio::fs::write(&secret_path, id_secret_str.as_bytes()).await?; + assert!(crate::utils::fs_restrict_permissions(&secret_path)); + tokio::fs::write(self.base_path.join(IDENTITY_PUBLIC_FILENAME), id_public_str.as_bytes()).await + } + + /// Get authorization token for local API, creating and saving if it does not exist. + pub async fn authtoken(&self) -> std::io::Result { + let mut authtoken = self.authtoken.lock().await; + if authtoken.is_empty() { + let authtoken_path = self.base_path.join(AUTH_TOKEN_FILENAME); + let authtoken_bytes = read_limit(&authtoken_path, 4096).await; + if authtoken_bytes.is_err() { + let mut tmp = String::with_capacity(AUTH_TOKEN_DEFAULT_LENGTH); + for _ in 0..AUTH_TOKEN_DEFAULT_LENGTH { + tmp.push(AUTH_TOKEN_POSSIBLE_CHARS.as_bytes()[(next_u32_secure() as usize) % AUTH_TOKEN_POSSIBLE_CHARS.len()] as char); + } + tokio::fs::write(&authtoken_path, tmp.as_bytes()).await?; + *authtoken = tmp; + } else { + *authtoken = String::from_utf8_lossy(authtoken_bytes.unwrap().as_slice()).into(); + } + } + Ok(authtoken.clone()) + } + + /// Get a readable locked reference to this node's configuration. + /// + /// Use clone() to get a copy of the configuration if you want to modify it. Then use + /// save_config() to save the modified configuration and update the internal copy in + /// this structure. + pub async fn config(&self) -> RwLockReadGuard<'_, localconfig::Config> { + self.config.read().await + } + + /// Save a modified copy of the configuration and replace the internal copy in this structure (if it's actually changed). + pub async fn save_config(&self, modified_config: localconfig::Config) -> std::io::Result<()> { + let mut config = self.config.write().await; + if !config.eq(&modified_config) { + let config_data = crate::utils::to_json_pretty(&modified_config); + tokio::fs::write(self.base_path.join(CONFIG_FILENAME), config_data.as_bytes()).await?; + *config = modified_config; + } + Ok(()) + } +} diff --git a/zerotier-system-service/src/localconfig.rs b/zerotier-system-service/src/localconfig.rs index 63a4c9f96..4fc7ac671 100644 --- a/zerotier-system-service/src/localconfig.rs +++ b/zerotier-system-service/src/localconfig.rs @@ -136,9 +136,6 @@ pub struct Config { pub virtual_: BTreeMap, pub network: BTreeMap, pub settings: GlobalSettings, - - #[serde(skip_serializing, skip_deserializing)] - file_content_hash: [u8; 48], } impl Default for Config { @@ -148,48 +145,6 @@ impl Default for Config { virtual_: BTreeMap::new(), network: BTreeMap::new(), settings: GlobalSettings::default(), - file_content_hash: [0_u8; 48], - } - } -} - -impl Config { - /// Load this configuration from disk. - pub async fn load(path: &str) -> std::io::Result { - let mut t = Self::default(); - t.reload(path).await.map(|_| t) - } - - /// Load this configuration from disk if its contents have changed. - /// - /// This returns true if a new configuration was loaded, false if the file hasn't changed, or an error. - pub async fn reload(&mut self, path: &str) -> std::io::Result { - let config_data = crate::utils::read_limit(path, crate::utils::DEFAULT_FILE_IO_READ_LIMIT).await?; - let hash = zerotier_core_crypto::hash::SHA384::hash(config_data.as_slice()); - Ok(if hash != self.file_content_hash { - let config = serde_json::from_slice(config_data.as_slice()); - if config.is_err() { - return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, config.err().unwrap().to_string())); - } - *self = config.unwrap(); - self.file_content_hash = hash; - true - } else { - false - }) - } - - /// Write this configuration to a file (with human-friendly formatting). - /// - /// This is mutable because it updates an external state if the write is successful. - pub async fn save(&mut self, path: &str) -> std::io::Result<()> { - let config_data = crate::utils::to_json_pretty(self); - let result = tokio::fs::write(path, &config_data).await; - if result.is_ok() { - self.file_content_hash = zerotier_core_crypto::hash::SHA384::hash(config_data.as_bytes()); - Ok(()) - } else { - result } } } diff --git a/zerotier-system-service/src/main.rs b/zerotier-system-service/src/main.rs index c2cf2c45d..333523467 100644 --- a/zerotier-system-service/src/main.rs +++ b/zerotier-system-service/src/main.rs @@ -14,6 +14,7 @@ use clap::{Arg, ArgMatches, Command}; use zerotier_network_hypervisor::{VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION}; pub mod cli; +pub mod datadir; pub mod exitcode; pub mod getifaddrs; pub mod jsonformatter; diff --git a/zerotier-system-service/src/utils.rs b/zerotier-system-service/src/utils.rs index eddb09309..a566d77bd 100644 --- a/zerotier-system-service/src/utils.rs +++ b/zerotier-system-service/src/utils.rs @@ -6,6 +6,7 @@ * https://www.zerotier.com/ */ +use std::path::Path; use std::str::FromStr; use std::time::{Instant, SystemTime, UNIX_EPOCH}; @@ -145,7 +146,7 @@ pub fn to_json_pretty(o: &O) -> String { /// Convenience function to read up to limit bytes from a file. /// /// If the file is larger than limit, the excess is not read. -pub async fn read_limit(path: &str, limit: usize) -> std::io::Result> { +pub async fn read_limit>(path: P, limit: usize) -> std::io::Result> { let mut f = File::open(path).await?; let bytes = f.metadata().await?.len().min(limit as u64) as usize; let mut v: Vec = Vec::with_capacity(bytes); @@ -154,9 +155,13 @@ pub async fn read_limit(path: &str, limit: usize) -> std::io::Result> { Ok(v) } -/// Returns true if the file exists and is a regular file (or a link to one). -pub async fn file_exists(path: &str) -> bool { - tokio::fs::metadata(path).await.is_ok() +/// Set permissions on a file or directory to be most restrictive (visible only to the service's user). +#[cfg(unix)] +pub fn fs_restrict_permissions>(path: P) -> bool { + unsafe { + let c_path = std::ffi::CString::new(path.as_ref().to_str().unwrap()).unwrap(); + libc::chmod(c_path.as_ptr(), if path.as_ref().is_dir() { 0o700 } else { 0o600 }) == 0 + } } /// Read an identity as either a literal or from a file. @@ -173,8 +178,10 @@ pub async fn parse_cli_identity(input: &str, validate: bool) -> Result