diff --git a/controller/Cargo.toml b/controller/Cargo.toml index 3406a81ff..4867b09d1 100644 --- a/controller/Cargo.toml +++ b/controller/Cargo.toml @@ -18,7 +18,3 @@ serde_json = { version = "^1", features = ["std"], default-features = false } serde_yaml = "^0" clap = { version = "^3", features = ["std", "suggestions"], default-features = false } notify = { version = "^5", features = ["macos_fsevent"], default-features = false } - -[target."cfg(not(windows))".dependencies] -signal-hook = "^0" -libc = "^0" diff --git a/controller/src/controller.rs b/controller/src/controller.rs index d38f8d572..e2feba9ce 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -1,5 +1,6 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. +use std::collections::HashMap; use std::error::Error; use std::sync::{Arc, Mutex, RwLock, Weak}; @@ -8,7 +9,9 @@ use tokio::time::{Duration, Instant}; use zerotier_network_hypervisor::protocol; use zerotier_network_hypervisor::protocol::{PacketBuffer, DEFAULT_MULTICAST_LIMIT, ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU}; use zerotier_network_hypervisor::vl1::{HostSystem, Identity, InnerProtocol, Node, PacketHandlerResult, Path, PathFilter, Peer}; -use zerotier_network_hypervisor::vl2::{CertificateOfMembership, CertificateOfOwnership, NetworkConfig, NetworkId, Tag}; +use zerotier_network_hypervisor::vl2; +use zerotier_network_hypervisor::vl2::networkconfig::*; +use zerotier_network_hypervisor::vl2::NetworkId; use zerotier_utils::blob::Blob; use zerotier_utils::buffer::OutOfBoundsError; use zerotier_utils::dictionary::Dictionary; @@ -37,6 +40,8 @@ pub struct Controller { impl Controller { /// Start an inner protocol handler answer ZeroTier VL2 network controller queries. + /// + /// The start() method must be called once the service this will run within is also created. pub async fn new(database: Arc, runtime: tokio::runtime::Handle) -> Result, Box> { if let Some(local_identity) = database.load_node_identity() { assert!(local_identity.secret.is_some()); @@ -56,13 +61,13 @@ impl Controller { } } - /// Set the service and HostSystem implementation for this controller. + /// Set the service and HostSystem implementation for this controller and start daemons. /// /// This must be called once the service that uses this handler is up or the controller /// won't actually do anything. The reference the handler holds is weak to prevent /// a circular reference, so if the VL1Service is dropped this must be called again to /// tell the controller handler about a new instance. - pub async fn set_service(&self, service: &Arc>) { + pub async fn start(&self, service: &Arc>) { *self.service.write().unwrap() = Arc::downgrade(service); if let Some(cw) = self.database.changes().await.map(|mut ch| { @@ -137,10 +142,24 @@ impl Controller { } /// Called when the DB informs us of a change. - async fn handle_change_notification(self: Arc, _change: Change) {} + async fn handle_change_notification(self: Arc, change: Change) { + match change { + Change::MemberAuthorized(_, _) => {} + Change::MemberDeauthorized(network_id, node_id) => { + if let Ok(Some(member)) = self.database.get_member(network_id, node_id).await { + if !member.authorized() { + // TODO + } + } + } + } + } /// Attempt to create a network configuration and return the result. /// + /// This is the central function of the controller that looks up members, checks their + /// permissions, and generates a network config and other credentials (or not). + /// /// An error is only returned if a database or other unusual error occurs. Otherwise a rejection /// reason is returned with None or an acceptance reason with a network configuration is returned. async fn get_network_config( @@ -151,27 +170,37 @@ impl Controller { ) -> Result<(AuthorizationResult, Option), Box> { let network = self.database.get_network(network_id).await?; if network.is_none() { - // TODO: send error return Ok((AuthorizationResult::Rejected, None)); } let network = network.unwrap(); let mut member = self.database.get_member(network_id, source_identity.address).await?; let mut member_changed = false; - let legacy_v1 = source_identity.p384.is_none(); - // If we have a member object and a pinned identity, check to make sure it matches. - if let Some(member) = member.as_ref() { + // If we have a member object and a pinned identity, check to make sure it matches. Also accept + // upgraded identities to replace old versions if they are properly formed and inherit. + if let Some(member) = member.as_mut() { if let Some(pinned_identity) = member.identity.as_ref() { if !pinned_identity.eq(&source_identity) { return Ok((AuthorizationResult::RejectedIdentityMismatch, None)); + } else if source_identity.is_upgraded_from(pinned_identity) { + let _ = member.identity.replace(source_identity.clone_without_secret()); + member_changed = true; } } } + // This is the final verdict after everything has been checked. let mut authorization_result = AuthorizationResult::Rejected; - let mut authorized = member.as_ref().map_or(false, |m| m.authorized()); - if !authorized { + + // This is the main "authorized" flag on the member record. If it is true then + // the member is allowed, but with the caveat that SSO must be checked if it's + // enabled on the network. If this is false then the member is rejected unless + // authorized by a token or unless it's a public network. + let mut member_authorized = member.as_ref().map_or(false, |m| m.authorized()); + + // If the member isn't authorized, check to see if it should be auto-authorized. + if !member_authorized { if member.is_none() { if network.learn_members.unwrap_or(true) { let _ = member.insert(Member::new_with_identity(source_identity.clone(), network_id)); @@ -181,33 +210,38 @@ impl Controller { } } - if !network.private { + if network.private { + // TODO: check token authorization + } else { authorization_result = AuthorizationResult::ApprovedOnPublicNetwork; - authorized = true; member.as_mut().unwrap().last_authorized_time = Some(now); + member_authorized = true; member_changed = true; } } let mut member = member.unwrap(); - let nc: Option = if authorized { - // ==================================================================================== - // Authorized requests are handled here - // ==================================================================================== + // If the member is authorized set the final verdict to reflect this unless SSO (third party auth) + // is enabled on the network and disagrees. Skip if the verdict is already one of the approved + // values, which would indicate auth-authorization above. + if member_authorized { + if !authorization_result.approved() { + // TODO: check SSO if enabled on network! + authorization_result = AuthorizationResult::Approved; + } + } else { + // This should not be able to be in approved state if member_authorized is still false. + assert!(!authorization_result.approved()); + } - // TODO: check SSO + let nc: Option = if authorization_result.approved() { + // We should not be able to make it here if this is still false. + assert!(member_authorized); - // Figure out time bounds for the certificate to generate. + // Figure out TTL for credentials (time window in V1). let credential_ttl = network.credential_ttl.unwrap_or(CREDENTIAL_WINDOW_SIZE_DEFAULT); - // Get a list of all network members that were deauthorized but are still within the time window. - // These will be issued revocations to remind the node not to speak to them until they fall off. - let deauthed_members_still_in_window = self - .database - .list_members_deauthorized_after(network.id, now - credential_ttl) - .await; - // Check and if necessary auto-assign static IPs for this member. member_changed |= network.check_zt_ip_assignments(self.database.as_ref(), &mut member).await; @@ -225,35 +259,52 @@ impl Controller { nc.rules = network.rules; nc.dns = network.dns; - nc.certificate_of_membership = - CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, now, credential_ttl, legacy_v1); - if nc.certificate_of_membership.is_none() { - return Ok((AuthorizationResult::RejectedDueToError, None)); - } + if network.min_supported_version.unwrap_or(0) < (protocol::PROTOCOL_VERSION_V2 as u32) { + // Get a list of all network members that were deauthorized but are still within the time window. + // These will be issued revocations to remind the node not to speak to them until they fall off. + let deauthed_members_still_in_window = self + .database + .list_members_deauthorized_after(network.id, now - credential_ttl) + .await; - let mut coo = CertificateOfOwnership::new(network_id, now, source_identity.address, legacy_v1); - for ip in nc.static_ips.iter() { - coo.add_ip(ip); - } - if !coo.sign(&self.local_identity, &source_identity) { - return Ok((AuthorizationResult::RejectedDueToError, None)); - } - nc.certificates_of_ownership.push(coo); + if let Some(com) = + vl2::v1::CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, now, credential_ttl) + { + let mut v1cred = V1Credentials { + certificate_of_membership: com, + certificates_of_ownership: Vec::new(), + tags: HashMap::new(), + }; - for (id, value) in member.tags.iter() { - let tag = Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, now, legacy_v1); - if tag.is_none() { + let mut coo = vl2::v1::CertificateOfOwnership::new(network_id, now, source_identity.address); + for ip in nc.static_ips.iter() { + coo.add_ip(ip); + } + if !coo.sign(&self.local_identity, &source_identity) { + return Ok((AuthorizationResult::RejectedDueToError, None)); + } + v1cred.certificates_of_ownership.push(coo); + + for (id, value) in member.tags.iter() { + let tag = vl2::v1::Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, now); + if tag.is_none() { + return Ok((AuthorizationResult::RejectedDueToError, None)); + } + let _ = v1cred.tags.insert(*id, tag.unwrap()); + } + + nc.v1_credentials = Some(v1cred); + } else { return Ok((AuthorizationResult::RejectedDueToError, None)); } - let _ = nc.tags.insert(*id, tag.unwrap()); + } else { + // TODO: create V2 type credential for V2-only networks + // TODO: populate node info for V2 networks } - // TODO: node info, which isn't supported in v1 so not needed yet - // TODO: revocations! Some(nc) - // ==================================================================================== } else { None }; @@ -409,6 +460,7 @@ impl InnerProtocol for Controller { } fn should_respond_to(&self, _: &Identity) -> bool { + // Controllers respond to anyone. true } } diff --git a/controller/src/database.rs b/controller/src/database.rs index 89fa965a2..b2700bf05 100644 --- a/controller/src/database.rs +++ b/controller/src/database.rs @@ -11,7 +11,6 @@ use crate::model::*; /// Database change relevant to the controller and that was NOT initiated by the controller. #[derive(Clone)] pub enum Change { - NetworkDeleted(NetworkId), MemberAuthorized(NetworkId, Address), MemberDeauthorized(NetworkId, Address), } diff --git a/controller/src/filedatabase.rs b/controller/src/filedatabase.rs index 8960e21b5..5b0e21471 100644 --- a/controller/src/filedatabase.rs +++ b/controller/src/filedatabase.rs @@ -86,13 +86,13 @@ impl FileDatabase { } fn network_path(&self, network_id: NetworkId) -> PathBuf { - self.base_path.join(format!("n{:06x}", network_id.network_no())).join("config.yaml") + self.base_path.join(format!("N{:06x}", network_id.network_no())).join("config.yaml") } fn member_path(&self, network_id: NetworkId, member_id: Address) -> PathBuf { self.base_path - .join(format!("n{:06x}", network_id.network_no())) - .join(format!("m{}.yaml", member_id.to_string())) + .join(format!("N{:06x}", network_id.network_no())) + .join(format!("M{}.yaml", member_id.to_string())) } } @@ -154,7 +154,7 @@ impl Database for FileDatabase { let osname = ent.file_name(); let name = osname.to_string_lossy(); if name.len() == (zerotier_network_hypervisor::protocol::ADDRESS_SIZE_STRING + 6) - && name.starts_with("m") + && name.starts_with("M") && name.ends_with(".yaml") { if let Ok(member_address) = u64::from_str_radix(&name[1..11], 16) { diff --git a/controller/src/main.rs b/controller/src/main.rs index 8ccc90f39..e7f44c75f 100644 --- a/controller/src/main.rs +++ b/controller/src/main.rs @@ -1,8 +1,6 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -use std::time::Duration; use clap::{Arg, Command}; @@ -34,27 +32,9 @@ async fn run(database: Arc, runtime: &Runtime) -> i32 { let svc = svc.unwrap(); svc.node().init_default_roots(); - handler.set_service(&svc).await; - - // Wait for kill signal on Unix-like platforms. - #[cfg(unix)] - { - let term = Arc::new(AtomicBool::new(false)); - let _ = signal_hook::flag::register(libc::SIGINT, term.clone()); - let _ = signal_hook::flag::register(libc::SIGTERM, term.clone()); - let _ = signal_hook::flag::register(libc::SIGQUIT, term.clone()); - while !term.load(Ordering::Relaxed) { - std::thread::sleep(Duration::from_secs(1)); - } - } - - #[cfg(windows)] - { - // TODO: if anyone wants to use this on Windows you'll need to make it a service or wait - // for a stop signal or soemthing here. - todo!(); - } + handler.start(&svc).await; + zerotier_utils::wait_for_process_abort(); println!("Terminate signal received, shutting down..."); exitcode::OK } else { @@ -84,7 +64,7 @@ fn main() { .takes_value(true) .forbid_empty_values(true) .value_name("path") - .help(Some("Connect to postgres with parameters in JSON file")) + .help(Some("Connect to postgres with parameters in YAML file")) .required_unless_present_any(&REQUIRE_ONE_OF_ARGS), ) .version(format!("{}.{}.{}", VERSION_MAJOR, VERSION_MINOR, VERSION_REVISION).as_str()) diff --git a/controller/src/model/mod.rs b/controller/src/model/mod.rs index f96ee3ab2..5172793c8 100644 --- a/controller/src/model/mod.rs +++ b/controller/src/model/mod.rs @@ -11,7 +11,8 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use zerotier_network_hypervisor::vl1::{Address, Endpoint}; -use zerotier_network_hypervisor::vl2::{NetworkConfig, NetworkId}; +use zerotier_network_hypervisor::vl2::networkconfig::NetworkConfig; +use zerotier_network_hypervisor::vl2::NetworkId; use zerotier_utils::blob::Blob; /// A complete network with all member configuration information for import/export or blob storage. @@ -80,6 +81,14 @@ impl AuthorizationResult { Self::ApprovedOnPublicNetwork => "ap", } } + + /// Returns true if this result is one of the 'approved' result types. + pub fn approved(&self) -> bool { + match self { + Self::Approved | Self::ApprovedViaSSO | Self::ApprovedViaToken | Self::ApprovedOnPublicNetwork => true, + _ => false, + } + } } impl ToString for AuthorizationResult { diff --git a/controller/src/model/network.rs b/controller/src/model/network.rs index d3a8a7abc..dcecb0f2f 100644 --- a/controller/src/model/network.rs +++ b/controller/src/model/network.rs @@ -7,7 +7,8 @@ use serde::{Deserialize, Serialize}; use zerotier_network_hypervisor::vl1::InetAddress; use zerotier_network_hypervisor::vl2::networkconfig::IpRoute; -use zerotier_network_hypervisor::vl2::{NetworkId, Rule}; +use zerotier_network_hypervisor::vl2::rule::Rule; +use zerotier_network_hypervisor::vl2::NetworkId; use crate::database::Database; use crate::model::{Member, ObjectType}; @@ -102,6 +103,11 @@ pub struct Network { #[serde(rename = "credentialTtl")] pub credential_ttl: Option, + /// Minimum supported ZeroTier protocol version for this network (default: undefined, up to members) + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "minSupportedVersion")] + pub min_supported_version: Option, + /// MTU inside the virtual network, default of 2800 is used if not set. pub mtu: Option, diff --git a/network-hypervisor/src/protocol.rs b/network-hypervisor/src/protocol.rs index d6f4a1ea3..06aac3bf2 100644 --- a/network-hypervisor/src/protocol.rs +++ b/network-hypervisor/src/protocol.rs @@ -50,6 +50,9 @@ pub const PROTOCOL_VERSION: u8 = 20; /// We could probably push it back to 8 or 9 with some added support for sending Salsa/Poly packets. pub const PROTOCOL_VERSION_MIN: u8 = 11; +/// V2 is this protocol version or higher. +pub const PROTOCOL_VERSION_V2: u8 = 20; + /// Size of a pooled packet buffer. pub const PACKET_BUFFER_SIZE: usize = 16384; @@ -565,6 +568,10 @@ pub(crate) const PEER_EXPIRATION_TIME: i64 = (PEER_HELLO_INTERVAL_MAX * 2) + 100 /// Proof of work difficulty (threshold) for identity generation. pub(crate) const IDENTITY_POW_THRESHOLD: u8 = 17; +/// Maximum number of key/value pairs in a single Tag credential. +/// (This is for V2 only. In V1 tag credentials can have only one pair.) +pub(crate) const MAX_TAG_KEY_VALUE_PAIRS: usize = 128; + #[cfg(test)] mod tests { use std::mem::size_of; diff --git a/network-hypervisor/src/vl1/identity.rs b/network-hypervisor/src/vl1/identity.rs index 638c85685..2098790cc 100644 --- a/network-hypervisor/src/vl1/identity.rs +++ b/network-hypervisor/src/vl1/identity.rs @@ -157,6 +157,7 @@ impl Identity { + P384_ECDSA_SIGNATURE_SIZE; pub const FINGERPRINT_SIZE: usize = IDENTITY_FINGERPRINT_SIZE; + pub const MAX_SIGNATURE_SIZE: usize = IDENTITY_MAX_SIGNATURE_SIZE; const ALGORITHM_X25519: u8 = 0x01; const ALGORITHM_EC_NIST_P384: u8 = 0x02; @@ -320,6 +321,15 @@ impl Identity { return digest[0] < IDENTITY_POW_THRESHOLD && Address::from_bytes(&digest[59..64]).map_or(false, |a| a == self.address); } + /// Returns true if this identity was upgraded from another older version. + pub fn is_upgraded_from(&self, other: &Identity) -> bool { + self.address == other.address + && self.x25519 == other.x25519 + && self.ed25519 == other.ed25519 + && self.p384.is_some() + && other.p384.is_none() + } + /// Perform ECDH key agreement, returning a shared secret or None on error. /// /// An error can occur if this identity does not hold its secret portion or if either key is invalid. diff --git a/network-hypervisor/src/vl2/mod.rs b/network-hypervisor/src/vl2/mod.rs index 66e0278dc..3e93e3c29 100644 --- a/network-hypervisor/src/vl2/mod.rs +++ b/network-hypervisor/src/vl2/mod.rs @@ -1,20 +1,13 @@ // (c) 2020-2022 ZeroTier, Inc. -- currently propritery pending actual release and licensing. See LICENSE.md. -mod certificateofmembership; -mod certificateofownership; mod multicastgroup; mod networkid; -mod rule; mod switch; -mod tag; pub mod networkconfig; +pub mod rule; +pub mod v1; -pub use certificateofmembership::CertificateOfMembership; -pub use certificateofownership::CertificateOfOwnership; pub use multicastgroup::MulticastGroup; -pub use networkconfig::NetworkConfig; pub use networkid::NetworkId; -pub use rule::Rule; pub use switch::{Switch, SwitchInterface}; -pub use tag::Tag; diff --git a/network-hypervisor/src/vl2/networkconfig.rs b/network-hypervisor/src/vl2/networkconfig.rs index eb965fffe..f8b484111 100644 --- a/network-hypervisor/src/vl2/networkconfig.rs +++ b/network-hypervisor/src/vl2/networkconfig.rs @@ -7,10 +7,8 @@ use std::str::FromStr; use serde::{Deserialize, Serialize}; use crate::vl1::{Address, Identity, InetAddress}; -use crate::vl2::certificateofmembership::CertificateOfMembership; -use crate::vl2::certificateofownership::CertificateOfOwnership; use crate::vl2::rule::Rule; -use crate::vl2::tag::Tag; +use crate::vl2::v1::{CertificateOfMembership, CertificateOfOwnership, Tag}; use crate::vl2::NetworkId; use zerotier_utils::buffer::Buffer; @@ -18,6 +16,19 @@ use zerotier_utils::dictionary::Dictionary; use zerotier_utils::error::InvalidParameterError; use zerotier_utils::marshalable::Marshalable; +/// Credentials that must be sent to V1 nodes to allow access. +/// +/// These are also handed out to V2 nodes to use when communicating with V1 nodes on +/// networks that support older protocol versions. +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct V1Credentials { + pub certificate_of_membership: CertificateOfMembership, + pub certificates_of_ownership: Vec, + #[serde(skip_serializing_if = "HashMap::is_empty")] + #[serde(default)] + pub tags: HashMap, +} + /// Network configuration object sent to nodes by network controllers. #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct NetworkConfig { @@ -51,13 +62,9 @@ pub struct NetworkConfig { #[serde(default)] pub dns: HashMap>, - pub certificate_of_membership: Option, // considered invalid if None - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] - pub certificates_of_ownership: Vec, - #[serde(skip_serializing_if = "HashMap::is_empty")] - #[serde(default)] - pub tags: HashMap, + pub v1_credentials: Option, #[serde(skip_serializing_if = "HashSet::is_empty")] #[serde(default)] @@ -91,9 +98,7 @@ impl NetworkConfig { static_ips: HashSet::new(), rules: Vec::new(), dns: HashMap::new(), - certificate_of_membership: None, - certificates_of_ownership: Vec::new(), - tags: HashMap::new(), + v1_credentials: None, banned: HashSet::new(), node_info: HashMap::new(), central_url: String::new(), @@ -171,25 +176,27 @@ impl NetworkConfig { d.set_bytes(proto_v1_field_name::network_config::DNS, dns_bin); } - d.set_bytes( - proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP, - self.certificate_of_membership.as_ref()?.to_bytes()?, - ); + if let Some(v1cred) = self.v1_credentials.as_ref() { + d.set_bytes( + proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP, + v1cred.certificate_of_membership.to_bytes()?, + ); - if !self.certificates_of_ownership.is_empty() { - let mut certs = Vec::with_capacity(self.certificates_of_ownership.len() * 256); - for c in self.certificates_of_ownership.iter() { - let _ = certs.write_all(c.v1_proto_to_bytes(controller_identity.address)?.as_slice()); + if !v1cred.certificates_of_ownership.is_empty() { + let mut certs = Vec::with_capacity(v1cred.certificates_of_ownership.len() * 256); + for c in v1cred.certificates_of_ownership.iter() { + let _ = certs.write_all(c.to_bytes(controller_identity.address)?.as_slice()); + } + d.set_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP, certs); } - d.set_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP, certs); - } - if !self.tags.is_empty() { - let mut certs = Vec::with_capacity(self.tags.len() * 256); - for (_, t) in self.tags.iter() { - let _ = certs.write_all(t.v1_proto_to_bytes(controller_identity.address)?.as_slice()); + if !v1cred.tags.is_empty() { + let mut certs = Vec::with_capacity(v1cred.tags.len() * 256); + for (_, t) in v1cred.tags.iter() { + let _ = certs.write_all(t.to_bytes(controller_identity.address)?.as_slice()); + } + d.set_bytes(proto_v1_field_name::network_config::TAGS, certs); } - d.set_bytes(proto_v1_field_name::network_config::TAGS, certs); } // node_info is not supported by V1 nodes @@ -299,27 +306,33 @@ impl NetworkConfig { } } - nc.certificate_of_membership = Some(CertificateOfMembership::v1_proto_from_bytes( - d.get_bytes(proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP) - .ok_or(InvalidParameterError("missing certificate of membership"))?, - )?); + let mut v1cred = V1Credentials { + certificate_of_membership: CertificateOfMembership::from_bytes( + d.get_bytes(proto_v1_field_name::network_config::CERTIFICATE_OF_MEMBERSHIP) + .ok_or(InvalidParameterError("missing certificate of membership"))?, + )?, + certificates_of_ownership: Vec::new(), + tags: HashMap::new(), + }; if let Some(mut coo_bin) = d.get_bytes(proto_v1_field_name::network_config::CERTIFICATES_OF_OWNERSHIP) { while !coo_bin.is_empty() { - let c = CertificateOfOwnership::v1_proto_from_bytes(coo_bin)?; - nc.certificates_of_ownership.push(c.0); + let c = CertificateOfOwnership::from_bytes(coo_bin)?; + v1cred.certificates_of_ownership.push(c.0); coo_bin = c.1; } } if let Some(mut tag_bin) = d.get_bytes(proto_v1_field_name::network_config::TAGS) { while !tag_bin.is_empty() { - let t = Tag::v1_proto_from_bytes(tag_bin)?; - let _ = nc.tags.insert(t.0.id, t.0); + let t = Tag::from_bytes(tag_bin)?; + let _ = v1cred.tags.insert(t.0.id, t.0); tag_bin = t.1; } } + nc.v1_credentials = Some(v1cred); + if let Some(central_url) = d.get_str(proto_v1_field_name::network_config::CENTRAL_URL) { nc.central_url = central_url.to_string(); } diff --git a/network-hypervisor/src/vl2/certificateofmembership.rs b/network-hypervisor/src/vl2/v1/certificateofmembership.rs similarity index 71% rename from network-hypervisor/src/vl2/certificateofmembership.rs rename to network-hypervisor/src/vl2/v1/certificateofmembership.rs index 1bff8f402..a99ee2eff 100644 --- a/network-hypervisor/src/vl2/certificateofmembership.rs +++ b/network-hypervisor/src/vl2/v1/certificateofmembership.rs @@ -1,6 +1,5 @@ use std::io::Write; -use crate::vl1::identity; use crate::vl1::identity::Identity; use crate::vl1::Address; use crate::vl2::NetworkId; @@ -20,22 +19,14 @@ pub struct CertificateOfMembership { pub timestamp: i64, pub max_delta: i64, pub issued_to: Address, - pub issued_to_fingerprint: Blob<{ identity::IDENTITY_FINGERPRINT_SIZE }>, - pub signature: ArrayVec, - pub version: u8, + pub issued_to_fingerprint: Blob<{ Identity::FINGERPRINT_SIZE }>, + pub signature: ArrayVec, } impl CertificateOfMembership { /// Create a new signed certificate of membership. /// None is returned if an error occurs, such as the issuer missing its secrets. - pub fn new( - issuer: &Identity, - network_id: NetworkId, - issued_to: &Identity, - timestamp: i64, - max_delta: i64, - legacy_v1: bool, - ) -> Option { + pub fn new(issuer: &Identity, network_id: NetworkId, issued_to: &Identity, timestamp: i64, max_delta: i64) -> Option { let mut com = CertificateOfMembership { network_id, timestamp, @@ -43,26 +34,18 @@ impl CertificateOfMembership { issued_to: issued_to.address, issued_to_fingerprint: Blob::default(), signature: ArrayVec::new(), - version: 0, }; - if legacy_v1 { - com.issued_to_fingerprint = Blob::from(Self::v1_proto_issued_to_fingerprint(issued_to, Some(issuer.address))); - com.version = 1; - if let Some(signature) = issuer.sign(&com.v1_proto_get_qualifier_bytes(), true) { - com.signature = signature; - Some(com) - } else { - None - } + + com.issued_to_fingerprint = Blob::from(Self::v1_proto_issued_to_fingerprint(issued_to, Some(issuer.address))); + if let Some(signature) = issuer.sign(&com.v1_proto_get_qualifier_bytes(), true) { + com.signature = signature; + Some(com) } else { - com.issued_to_fingerprint = Blob::from(issued_to.fingerprint); - com.version = 2; - todo!() + None } } fn v1_proto_get_qualifier_bytes(&self) -> [u8; 168] { - assert_eq!(self.version, 1); let mut q = [0u64; 21]; q[0] = 0; @@ -109,25 +92,21 @@ impl CertificateOfMembership { /// Get this certificate of membership in byte encoded format. pub fn to_bytes(&self) -> Option> { - if self.version == 1 { - if self.signature.len() == 96 { - let mut v: Vec = Vec::with_capacity(3 + 168 + 5 + 96); - v.push(1); // version byte from v1 protocol - v.push(0); - v.push(7); // 7 qualifiers, big-endian 16-bit - let _ = v.write_all(&self.v1_proto_get_qualifier_bytes()); - let _ = v.write_all(&self.issued_to_fingerprint.as_bytes()[32..38]); // issuer address - let _ = v.write_all(self.signature.as_bytes()); - return Some(v); - } - } else if self.version == 2 { - todo!() + if self.signature.len() == 96 { + let mut v: Vec = Vec::with_capacity(3 + 168 + 5 + 96); + v.push(1); // version byte from v1 protocol + v.push(0); + v.push(7); // 7 qualifiers, big-endian 16-bit + let _ = v.write_all(&self.v1_proto_get_qualifier_bytes()); + let _ = v.write_all(&self.issued_to_fingerprint.as_bytes()[32..38]); // issuer address + let _ = v.write_all(self.signature.as_bytes()); + return Some(v); } return None; } /// Decode a V1 legacy format certificate of membership in byte format. - pub fn v1_proto_from_bytes(mut b: &[u8]) -> Result { + pub fn from_bytes(mut b: &[u8]) -> Result { if b.len() <= 3 || b[0] != 1 { return Err(InvalidParameterError("version mismatch")); } @@ -185,20 +164,15 @@ impl CertificateOfMembership { s.push_slice(&b[..96]); s }, - version: 1, }) } /// Verify this certificate of membership. pub fn verify(self, issuer: &Identity, expect_issued_to: &Identity) -> Option> { - if self.version == 1 { - if Self::v1_proto_issued_to_fingerprint(expect_issued_to, None).eq(&self.issued_to_fingerprint.as_bytes()[..32]) { - if issuer.verify(&self.v1_proto_get_qualifier_bytes(), self.signature.as_bytes()) { - return Some(Verified(self)); - } + if Self::v1_proto_issued_to_fingerprint(expect_issued_to, None).eq(&self.issued_to_fingerprint.as_bytes()[..32]) { + if issuer.verify(&self.v1_proto_get_qualifier_bytes(), self.signature.as_bytes()) { + return Some(Verified(self)); } - } else if self.version == 2 { - todo!() } return None; } diff --git a/network-hypervisor/src/vl2/certificateofownership.rs b/network-hypervisor/src/vl2/v1/certificateofownership.rs similarity index 88% rename from network-hypervisor/src/vl2/certificateofownership.rs rename to network-hypervisor/src/vl2/v1/certificateofownership.rs index 2a76efeef..9334d8376 100644 --- a/network-hypervisor/src/vl2/certificateofownership.rs +++ b/network-hypervisor/src/vl2/v1/certificateofownership.rs @@ -34,23 +34,17 @@ pub struct CertificateOfOwnership { pub things: HashSet, pub issued_to: Address, pub signature: ArrayVec, - pub version: u8, } impl CertificateOfOwnership { /// Create a new empty and unsigned certificate. - pub fn new(network_id: NetworkId, timestamp: i64, issued_to: Address, legacy_v1: bool) -> Self { + pub fn new(network_id: NetworkId, timestamp: i64, issued_to: Address) -> Self { Self { network_id, timestamp, things: HashSet::with_capacity(4), issued_to, signature: ArrayVec::new(), - version: if legacy_v1 { - 1 - } else { - 2 - }, } } @@ -117,13 +111,13 @@ impl CertificateOfOwnership { } #[inline(always)] - pub fn v1_proto_to_bytes(&self, signed_by: Address) -> Option> { + pub fn to_bytes(&self, signed_by: Address) -> Option> { self.internal_v1_proto_to_bytes(false, signed_by) } /// Decode a V1 legacy format certificate of ownership in byte format. /// The certificate and the current position slice are returned so multiple certs can be easily read from a buffer. - pub fn v1_proto_from_bytes(mut b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> { + pub fn from_bytes(mut b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> { if b.len() < 30 { return Err(InvalidParameterError("incomplete")); } @@ -167,7 +161,6 @@ impl CertificateOfOwnership { s.push_slice(&b[13..109]); s }, - version: 1, }, &b[END_LEN..], )) @@ -175,16 +168,12 @@ impl CertificateOfOwnership { /// Sign certificate of ownership for use by V1 nodes. pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool { - if self.version == 1 { - self.issued_to = issued_to.address; - if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) { - if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) { - self.signature = signature; - return true; - } + self.issued_to = issued_to.address; + if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) { + if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) { + self.signature = signature; + return true; } - } else if self.version == 2 { - todo!() } return false; } diff --git a/network-hypervisor/src/vl2/v1/mod.rs b/network-hypervisor/src/vl2/v1/mod.rs new file mode 100644 index 000000000..3576f0844 --- /dev/null +++ b/network-hypervisor/src/vl2/v1/mod.rs @@ -0,0 +1,9 @@ +mod certificateofmembership; +mod certificateofownership; +mod revocation; +mod tag; + +pub use certificateofmembership::CertificateOfMembership; +pub use certificateofownership::{CertificateOfOwnership, Thing}; +pub use revocation::Revocation; +pub use tag::Tag; diff --git a/network-hypervisor/src/vl2/v1/revocation.rs b/network-hypervisor/src/vl2/v1/revocation.rs new file mode 100644 index 000000000..14ecd71fb --- /dev/null +++ b/network-hypervisor/src/vl2/v1/revocation.rs @@ -0,0 +1,15 @@ +use zerotier_utils::arrayvec::ArrayVec; + +use serde::{Deserialize, Serialize}; + +use crate::vl1::{Address, Identity}; +use crate::vl2::NetworkId; + +#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Revocation { + pub network_id: NetworkId, + pub threshold: i64, + pub issued_to: Address, + pub signature: ArrayVec, + pub version: u8, +} diff --git a/network-hypervisor/src/vl2/tag.rs b/network-hypervisor/src/vl2/v1/tag.rs similarity index 73% rename from network-hypervisor/src/vl2/tag.rs rename to network-hypervisor/src/vl2/v1/tag.rs index ffae0a1a1..b056f9436 100644 --- a/network-hypervisor/src/vl2/tag.rs +++ b/network-hypervisor/src/vl2/v1/tag.rs @@ -1,6 +1,5 @@ use std::io::Write; -use crate::vl1::identity; use crate::vl1::identity::Identity; use crate::vl1::Address; use crate::vl2::NetworkId; @@ -17,20 +16,11 @@ pub struct Tag { pub issued_to: Address, pub id: u32, pub value: u32, - pub signature: ArrayVec, - pub version: u8, + pub signature: ArrayVec, } impl Tag { - pub fn new( - id: u32, - value: u32, - issuer: &Identity, - network_id: NetworkId, - issued_to: &Identity, - timestamp: i64, - legacy_v1: bool, - ) -> Option { + pub fn new(id: u32, value: u32, issuer: &Identity, network_id: NetworkId, issued_to: &Identity, timestamp: i64) -> Option { let mut tag = Self { network_id, timestamp, @@ -38,26 +28,17 @@ impl Tag { id, value, signature: ArrayVec::new(), - version: if legacy_v1 { - 1 - } else { - 2 - }, }; - if legacy_v1 { - let to_sign = tag.internal_v1_proto_to_bytes(true, issuer.address)?; - if let Some(signature) = issuer.sign(to_sign.as_slice(), true) { - tag.signature = signature; - return Some(tag); - } - } else { - todo!() + let to_sign = tag.internal_v1_proto_to_bytes(true, issuer.address)?; + if let Some(signature) = issuer.sign(to_sign.as_slice(), true) { + tag.signature = signature; + return Some(tag); } return None; } fn internal_v1_proto_to_bytes(&self, for_sign: bool, signed_by: Address) -> Option> { - if self.version == 1 && self.signature.len() == 96 { + if self.signature.len() == 96 { let mut v = Vec::with_capacity(256); if for_sign { let _ = v.write_all(&[0x7f; 8]); @@ -85,11 +66,11 @@ impl Tag { } #[inline(always)] - pub fn v1_proto_to_bytes(&self, signed_by: Address) -> Option> { + pub fn to_bytes(&self, signed_by: Address) -> Option> { self.internal_v1_proto_to_bytes(false, signed_by) } - pub fn v1_proto_sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool { + pub fn sign(&mut self, issuer: &Identity, issued_to: &Identity) -> bool { self.issued_to = issued_to.address; if let Some(to_sign) = self.internal_v1_proto_to_bytes(true, issuer.address) { if let Some(signature) = issuer.sign(&to_sign.as_slice(), true) { @@ -100,7 +81,7 @@ impl Tag { return false; } - pub fn v1_proto_from_bytes(b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> { + pub fn from_bytes(b: &[u8]) -> Result<(Self, &[u8]), InvalidParameterError> { const LEN: usize = 8 + 8 + 4 + 4 + 5 + 5 + 3 + 96 + 2; if b.len() < LEN { return Err(InvalidParameterError("incomplete")); @@ -117,7 +98,6 @@ impl Tag { s.push_slice(&b[37..133]); s }, - version: 1, }, &b[LEN..], )) diff --git a/utils/Cargo.toml b/utils/Cargo.toml index 758b799c8..b548cf60f 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -19,3 +19,4 @@ winapi = { version = "^0", features = ["handleapi", "ws2ipdef", "ws2tcpip"] } [target."cfg(not(windows))".dependencies] libc = "^0" +signal-hook = "^0" diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 99a49073c..de7bd725e 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -51,6 +51,26 @@ pub fn ms_monotonic() -> i64 { std::time::Instant::now().duration_since(instant_zero).as_millis() as i64 } +/// Wait for a kill signal (e.g. SIGINT or OS-equivalent) and return when received. +#[cfg(unix)] +pub fn wait_for_process_abort() { + if let Ok(mut signals) = signal_hook::iterator::Signals::new(&[libc::SIGINT, libc::SIGTERM, libc::SIGQUIT]) { + 'wait_for_exit: loop { + for signal in signals.wait() { + match signal as libc::c_int { + libc::SIGINT | libc::SIGTERM | libc::SIGQUIT => { + break 'wait_for_exit; + } + _ => {} + } + } + std::thread::sleep(std::time::Duration::from_millis(500)); + } + } else { + panic!("unable to listen to OS signals"); + } +} + #[cold] #[inline(never)] pub extern "C" fn unlikely_branch() {}