diff --git a/controller/src/controller.rs b/controller/src/controller.rs index c8cc6d644..7ee8174c0 100644 --- a/controller/src/controller.rs +++ b/controller/src/controller.rs @@ -123,6 +123,22 @@ impl Controller { })); } + /// Launched as a task when the DB informs us of a change. + async fn handle_change_notification(self: Arc, change: Change) { + match change { + Change::NetworkCreated(_) => {} + Change::NetworkChanged(_, _) => {} + Change::NetworkDeleted(_, _) => {} // TODO: somehow poison whole network + Change::MemberCreated(_) => {} + Change::MemberChanged(old_member, new_member) => { + if !new_member.authorized() && old_member.authorized() { + self.deauthorize_member(&new_member).await; + } + } + Change::MemberDeleted(member) => self.deauthorize_member(&member).await, + } + } + /// Compose and send network configuration packet (either V1 or V2) fn send_network_config( &self, @@ -182,8 +198,8 @@ impl Controller { } } - /// Send one or more revocation object(s) to a peer. - fn send_revocations(&self, peer: &Peer, mut revocations: Vec) { + /// Send one or more revocation object(s) to a peer. The provided vector is drained. + fn v1_proto_send_revocations(&self, peer: &Peer, revocations: &mut Vec) { if let Some(host_system) = self.service.read().unwrap().upgrade() { let time_ticks = ms_monotonic(); while !revocations.is_empty() { @@ -218,8 +234,26 @@ impl Controller { } } - /// Called when the DB informs us of a change. - async fn handle_change_notification(self: Arc, change: Change) {} + async fn deauthorize_member(&self, member: &Member) { + let time_clock = ms_since_epoch(); + let mut revocations = Vec::with_capacity(1); + if let Ok(all_network_members) = self.database.list_members(member.network_id).await { + for m in all_network_members.iter() { + if member.node_id != *m { + if let Some(peer) = self.service.read().unwrap().upgrade().and_then(|s| s.node().peer(*m)) { + if peer.is_v2() { + todo!(); + } else { + revocations.clear(); + Revocation::new(member.network_id, time_clock, member.node_id, *m, &self.local_identity, false) + .map(|r| revocations.push(r)); + self.v1_proto_send_revocations(&peer, &mut revocations); + } + } + } + } + } + } /// Attempt to create a network configuration and return the result. /// @@ -231,11 +265,11 @@ impl Controller { /// /// 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( + async fn authorize( self: &Arc, source_identity: &Verified, network_id: NetworkId, - now: i64, + time_clock: i64, ) -> Result<(AuthorizationResult, Option, Option>), Box> { let network = self.database.get_network(network_id).await?; if network.is_none() { @@ -246,8 +280,8 @@ impl Controller { let mut member = self.database.get_member(network_id, source_identity.address).await?; let mut member_changed = false; - // WARNING: this is where members are verified before they get admitted to a network. Read and edit - // very carefully! + // SECURITY WARNING: this is a critical code path where members of networks are authorized. + // Read and modify with extreme care. // 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. @@ -264,7 +298,9 @@ impl Controller { } } else if let Some(pinned_fingerprint) = member.identity_fingerprint.as_ref() { if pinned_fingerprint.as_bytes().eq(&source_identity.fingerprint) { - // Learn the FULL identity if the fingerprint is pinned and they match. + // Learn the FULL identity if the fingerprint is pinned and they match. This + // lets us add membrers by address/fingerprint with full SHA384 identity + // verification instead of just the address. let _ = member.identity.replace(source_identity.clone_without_secret()); member_changed = true; } else { @@ -273,7 +309,6 @@ impl Controller { } } - // This will be the final verdict after everything has been checked. let mut authorization_result = AuthorizationResult::Rejected; // This is the main "authorized" flag on the member record. If it is true then @@ -297,7 +332,7 @@ impl Controller { // TODO: check token authorization } else { authorization_result = AuthorizationResult::ApprovedOnPublicNetwork; - member.as_mut().unwrap().last_authorized_time = Some(now); + member.as_mut().unwrap().last_authorized_time = Some(time_clock); member_authorized = true; member_changed = true; } @@ -334,7 +369,7 @@ impl Controller { nc.name = network.name.clone(); nc.private = network.private; - nc.timestamp = now; + nc.timestamp = time_clock; nc.multicast_limit = network.multicast_limit.unwrap_or(DEFAULT_MULTICAST_LIMIT as u32); nc.multicast_like_expire = Some(protocol::VL2_DEFAULT_MULTICAST_LIKE_EXPIRE as u32); nc.mtu = network.mtu.unwrap_or(ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU as u16); @@ -348,10 +383,10 @@ impl Controller { // the overhead (bandwidth and CPU) of generating these. if let Some(com) = - vl2::v1::CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, now, credential_ttl) + vl2::v1::CertificateOfMembership::new(&self.local_identity, network_id, &source_identity, time_clock, credential_ttl) { let mut v1cred = V1Credentials { - revision: now as u64, + revision: time_clock as u64, max_delta: credential_ttl, certificate_of_membership: com, certificates_of_ownership: Vec::new(), @@ -359,7 +394,7 @@ impl Controller { }; if !nc.static_ips.is_empty() { - let mut coo = vl2::v1::CertificateOfOwnership::new(network_id, now, source_identity.address); + let mut coo = vl2::v1::CertificateOfOwnership::new(network_id, time_clock, source_identity.address); for ip in nc.static_ips.iter() { coo.add_ip(ip); } @@ -370,39 +405,33 @@ impl Controller { } for (id, value) in member.tags.iter() { - let tag = vl2::v1::Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, now); + let tag = vl2::v1::Tag::new(*id, *value, &self.local_identity, network_id, &source_identity, time_clock); if tag.is_none() { return Ok((AuthorizationResult::RejectedDueToError, None, None)); } let _ = v1cred.tags.insert(*id, tag.unwrap()); } - nc.v1_credentials = Some(v1cred); - // For anyone who has been deauthorized but is still in the window, send revocations. if let Ok(deauthed_members_still_in_window) = self .database - .list_members_deauthorized_after(network.id, now - (credential_ttl as i64)) + .list_members_deauthorized_after(network.id, time_clock - (credential_ttl as i64)) .await { if !deauthed_members_still_in_window.is_empty() { let mut revs = Vec::with_capacity(deauthed_members_still_in_window.len()); for dm in deauthed_members_still_in_window.iter() { - if let Some(rev) = Revocation::new( - network_id, - now, - *dm, - source_identity.address, - &self.local_identity, - vl2::v1::CredentialType::CertificateOfMembership, - false, - ) { + if let Some(rev) = + Revocation::new(network_id, time_clock, *dm, source_identity.address, &self.local_identity, false) + { revs.push(rev); } } revocations = Some(revs); } } + + nc.v1_credentials = Some(v1cred); } else { return Ok((AuthorizationResult::RejectedDueToError, None, None)); } @@ -493,12 +522,12 @@ impl InnerProtocol for Controller { let node_fingerprint = Blob::from(source.identity.fingerprint); let now = ms_since_epoch(); - let (result, config) = match self2.get_network_config(&source.identity, network_id, now).await { + let (result, config) = match self2.authorize(&source.identity, network_id, now).await { Result::Ok((result, Some(config), revocations)) => { //println!("{}", serde_yaml::to_string(&config).unwrap()); self2.send_network_config(source.as_ref(), &config, Some(message_id)); - if let Some(revocations) = revocations { - self2.send_revocations(source.as_ref(), revocations); + if let Some(mut revocations) = revocations { + self2.v1_proto_send_revocations(source.as_ref(), &mut revocations); } (result, Some(config)) } diff --git a/controller/src/database.rs b/controller/src/database.rs index b7e63552d..34fcfc34a 100644 --- a/controller/src/database.rs +++ b/controller/src/database.rs @@ -13,7 +13,7 @@ use crate::model::*; pub enum Change { NetworkCreated(Network), NetworkChanged(Network, Network), - NetworkDeleted(Network), + NetworkDeleted(Network, Vec), MemberCreated(Member), MemberChanged(Member, Member), MemberDeleted(Member), diff --git a/controller/src/filedatabase.rs b/controller/src/filedatabase.rs index 5469bc9bd..42efd6066 100644 --- a/controller/src/filedatabase.rs +++ b/controller/src/filedatabase.rs @@ -119,13 +119,11 @@ impl FileDatabase { if deleted.is_some() { match record_type { RecordType::Network => { - if let Some((network, mut members)) = + if let Some((network, members)) = db.cache.on_network_deleted(network_id) { - for m in members.drain(..) { - let _ = db.change_sender.send(Change::MemberDeleted(m)); - } - let _ = db.change_sender.send(Change::NetworkDeleted(network)); + let _ = + db.change_sender.send(Change::NetworkDeleted(network, members)); } } RecordType::Member => { diff --git a/controller/src/model/mod.rs b/controller/src/model/mod.rs index f21d98b64..ac9ae9504 100644 --- a/controller/src/model/mod.rs +++ b/controller/src/model/mod.rs @@ -81,7 +81,6 @@ impl AuthorizationResult { } impl ToString for AuthorizationResult { - #[inline(always)] fn to_string(&self) -> String { self.as_str().to_string() } diff --git a/crypto/src/zssp.rs b/crypto/src/zssp.rs index 09d5d40b6..f2de5cb5e 100644 --- a/crypto/src/zssp.rs +++ b/crypto/src/zssp.rs @@ -158,15 +158,15 @@ pub enum Error { /// Data object is too large to send, even fragmented. DataTooLarge, - /// An unexpected error occurred elsewhere in the code (may indicate a bug). - OtherError(Box), + /// An unexpected I/O error such as a buffer overrun occurred. + IoError(std::io::Error), } impl From for Error { #[cold] #[inline(never)] fn from(e: std::io::Error) -> Self { - Self::OtherError(Box::new(e)) + Self::IoError(e) } } @@ -184,7 +184,7 @@ impl std::fmt::Display for Error { Self::UnknownProtocolVersion => f.write_str("UnknownProtocolVersion"), Self::DataBufferTooSmall => f.write_str("DataBufferTooSmall"), Self::DataTooLarge => f.write_str("DataTooLarge"), - Self::OtherError(e) => f.write_str(format!("OtherError({})", e.to_string()).as_str()), + Self::IoError(e) => f.write_str(format!("OtherError({})", e.to_string()).as_str()), } } } @@ -261,7 +261,7 @@ impl From for u64 { /// /// This holds the data structures used to defragment incoming packets that are not associated with an /// existing session, which would be new attempts to create sessions. Typically one of these is associated -/// with a single listen socket or other inbound endpoint. +/// with a single listen socket, local bound port, or other inbound endpoint. pub struct ReceiveContext { initial_offer_defrag: Mutex, 1024, 128>>, incoming_init_header_check_cipher: Aes, @@ -348,6 +348,7 @@ pub struct Session { state: RwLock, // Mutable parts of state (other than defrag buffers) remote_s_public_hash: [u8; 48], // SHA384(remote static public key blob) remote_s_public_p384: [u8; P384_PUBLIC_KEY_SIZE], // Remote NIST P-384 static public key + defrag: Mutex, 16, 4>>, } diff --git a/network-hypervisor/src/vl1/identity.rs b/network-hypervisor/src/vl1/identity.rs index d9cb44ed8..3422dcafb 100644 --- a/network-hypervisor/src/vl1/identity.rs +++ b/network-hypervisor/src/vl1/identity.rs @@ -328,6 +328,8 @@ impl Identity { } /// Returns true if this identity was upgraded from another older version. + /// + /// This does NOT validate either identity. Ensure that validation has been performed. pub fn is_upgraded_from(&self, other: &Identity) -> bool { self.address == other.address && self.x25519 == other.x25519 diff --git a/network-hypervisor/src/vl2/v1/revocation.rs b/network-hypervisor/src/vl2/v1/revocation.rs index 7566e52aa..d1ee14bf7 100644 --- a/network-hypervisor/src/vl2/v1/revocation.rs +++ b/network-hypervisor/src/vl2/v1/revocation.rs @@ -1,8 +1,8 @@ use std::io::Write; use zerotier_crypto::random; +use zerotier_crypto::verified::Verified; use zerotier_utils::arrayvec::ArrayVec; -use zerotier_utils::blob::Blob; use serde::{Deserialize, Serialize}; @@ -17,8 +17,7 @@ pub struct Revocation { pub threshold: i64, pub target: Address, pub issued_to: Address, - pub signature: Blob<96>, - pub type_being_revoked: u8, + pub signature: ArrayVec, pub fast_propagate: bool, } @@ -28,8 +27,7 @@ impl Revocation { threshold: i64, target: Address, issued_to: Address, - signer: &Identity, - type_being_revoked: CredentialType, + signer: &Verified, fast_propagate: bool, ) -> Option { let mut r = Self { @@ -38,8 +36,7 @@ impl Revocation { threshold, target, issued_to, - signature: Blob::default(), - type_being_revoked: type_being_revoked as u8, + signature: ArrayVec::new(), fast_propagate, }; if let Some(sig) = signer.sign(r.internal_to_bytes(true, signer.address).as_bytes(), true) { @@ -64,14 +61,15 @@ impl Revocation { let _ = v.write_all(&(self.fast_propagate as u64).to_be_bytes()); // 0x1 is the flag for this let _ = v.write_all(&self.target.to_bytes()); let _ = v.write_all(&signed_by.to_bytes()); - v.push(self.type_being_revoked); + v.push(CredentialType::CertificateOfMembership as u8); if for_sign { let _ = v.write_all(&[0x7f; 8]); } else { v.push(1); // ed25519 signature - let _ = v.write_all(&[0u8, 96u8]); - let _ = v.write_all(self.signature.as_bytes()); + assert!(self.signature.len() <= 255); + let _ = v.write_all(&[0u8, self.signature.len() as u8]); + let _ = v.write_all(self.signature.as_ref()); } v