mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-15 00:43:45 +02:00
Use ordered sets and maps in network configs to make them deterministic, and some other cleanup.
This commit is contained in:
parent
3be8a7aa6f
commit
49128a55cc
5 changed files with 84 additions and 48 deletions
|
@ -23,7 +23,7 @@ use zerotier_utils::{ms_monotonic, ms_since_epoch};
|
|||
use zerotier_vl1_service::VL1Service;
|
||||
|
||||
use crate::database::*;
|
||||
use crate::model::{AuthorizationResult, Member, RequestLogItem, CREDENTIAL_WINDOW_SIZE_DEFAULT};
|
||||
use crate::model::{AuthenticationResult, Member, RequestLogItem, CREDENTIAL_WINDOW_SIZE_DEFAULT};
|
||||
|
||||
// A netconf per-query task timeout, just a sanity limit.
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
@ -270,10 +270,10 @@ impl Controller {
|
|||
source_identity: &Verified<Identity>,
|
||||
network_id: NetworkId,
|
||||
time_clock: i64,
|
||||
) -> Result<(AuthorizationResult, Option<NetworkConfig>, Option<Vec<vl2::v1::Revocation>>), Box<dyn Error + Send + Sync>> {
|
||||
) -> Result<(AuthenticationResult, Option<NetworkConfig>, Option<Vec<vl2::v1::Revocation>>), Box<dyn Error + Send + Sync>> {
|
||||
let network = self.database.get_network(network_id).await?;
|
||||
if network.is_none() {
|
||||
return Ok((AuthorizationResult::Rejected, None, None));
|
||||
return Ok((AuthenticationResult::Rejected, None, None));
|
||||
}
|
||||
let network = network.unwrap();
|
||||
|
||||
|
@ -284,37 +284,45 @@ impl Controller {
|
|||
// 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.
|
||||
// upgraded identities to replace old versions if they are properly formed and their signatures
|
||||
// all check out (see Identity::is_upgraded_from()). Note that we do not pin the identity here
|
||||
// if it is unspecified. That's not done until we fully authorize this member, since we don't
|
||||
// want to have a way to somehow pin the wrong person's identity (if someone manages to somehow
|
||||
// create a colliding identity and get it to us).
|
||||
if let Some(member) = member.as_mut() {
|
||||
if let Some(pinned_identity) = member.identity.as_ref() {
|
||||
if !pinned_identity.eq(&source_identity) {
|
||||
if source_identity.is_upgraded_from(pinned_identity) {
|
||||
// Upgrade identity types if we have a V2 identity upgraded from a V1 identity.
|
||||
let _ = member.identity.replace(source_identity.clone_without_secret());
|
||||
let _ = member.identity_fingerprint.replace(Blob::from(source_identity.fingerprint));
|
||||
member_changed = true;
|
||||
} else {
|
||||
return Ok((AuthorizationResult::RejectedIdentityMismatch, None, None));
|
||||
return Ok((AuthenticationResult::RejectedIdentityMismatch, None, None));
|
||||
}
|
||||
}
|
||||
} else if let Some(pinned_fingerprint) = member.identity_fingerprint.as_ref() {
|
||||
}
|
||||
|
||||
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. 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;
|
||||
if member.identity.is_none() {
|
||||
// Learn the FULL identity if the fingerprint is pinned and they match. This
|
||||
// lets us add members by address/fingerprint with full SHA384 identity
|
||||
// verification instead of just by short address.
|
||||
let _ = member.identity.replace(source_identity.clone_without_secret());
|
||||
member_changed = true;
|
||||
}
|
||||
} else {
|
||||
return Ok((AuthorizationResult::RejectedIdentityMismatch, None, None));
|
||||
return Ok((AuthenticationResult::RejectedIdentityMismatch, None, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut authorization_result = AuthorizationResult::Rejected;
|
||||
let mut authentication_result = AuthenticationResult::Rejected;
|
||||
|
||||
// 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.
|
||||
// This is the main "authorized" state of 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 auto-authorized via a mechanism like public networks below.
|
||||
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.
|
||||
|
@ -324,14 +332,14 @@ impl Controller {
|
|||
let _ = member.insert(Member::new_with_identity(source_identity.as_ref().clone(), network_id));
|
||||
member_changed = true;
|
||||
} else {
|
||||
return Ok((AuthorizationResult::Rejected, None, None));
|
||||
return Ok((AuthenticationResult::Rejected, None, None));
|
||||
}
|
||||
}
|
||||
|
||||
if network.private {
|
||||
// TODO: check token authorization
|
||||
} else {
|
||||
authorization_result = AuthorizationResult::ApprovedOnPublicNetwork;
|
||||
authentication_result = AuthenticationResult::ApprovedIsPublicNetwork;
|
||||
member.as_mut().unwrap().last_authorized_time = Some(time_clock);
|
||||
member_authorized = true;
|
||||
member_changed = true;
|
||||
|
@ -344,21 +352,48 @@ impl Controller {
|
|||
// 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() {
|
||||
if !authentication_result.approved() {
|
||||
// TODO: check SSO if enabled on network!
|
||||
authorization_result = AuthorizationResult::Approved;
|
||||
authentication_result = AuthenticationResult::Approved;
|
||||
}
|
||||
} else {
|
||||
// This should not be able to be in approved state if member_authorized is still false.
|
||||
assert!(!authorization_result.approved());
|
||||
assert!(!authentication_result.approved());
|
||||
}
|
||||
|
||||
// drop 'mut' from these
|
||||
let member_authorized = member_authorized;
|
||||
let authentication_result = authentication_result;
|
||||
|
||||
let mut network_config = None;
|
||||
let mut revocations = None;
|
||||
if authorization_result.approved() {
|
||||
if authentication_result.approved() {
|
||||
// We should not be able to make it here if this is still false.
|
||||
assert!(member_authorized);
|
||||
|
||||
// Pin member identity if not pinned already. This is analogous to SSH "trust on first use" except
|
||||
// that the ZeroTier address is akin to the host name. Once we've seen the full identity once then
|
||||
// it becomes truly "impossible" to collide the address. (Unless you can break ECC and SHA384.)
|
||||
if member.identity.is_none() {
|
||||
let _ = member.identity.replace(source_identity.clone_without_secret());
|
||||
debug_assert!(member.identity_fingerprint.is_none());
|
||||
let _ = member.identity_fingerprint.replace(Blob::from(source_identity.fingerprint));
|
||||
member_changed = true;
|
||||
}
|
||||
|
||||
// Make sure these agree. It should be impossible to end up with a member that's authorized and
|
||||
// whose identity and identity fingerprint don't match.
|
||||
if !member
|
||||
.identity
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.fingerprint
|
||||
.eq(member.identity_fingerprint.as_ref().unwrap().as_bytes())
|
||||
{
|
||||
debug_assert!(false);
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
|
||||
// Figure out TTL for credentials (time window in V1).
|
||||
let credential_ttl = network.credential_ttl.unwrap_or(CREDENTIAL_WINDOW_SIZE_DEFAULT);
|
||||
|
||||
|
@ -373,10 +408,10 @@ impl Controller {
|
|||
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);
|
||||
nc.routes = network.ip_routes;
|
||||
nc.routes = network.ip_routes.iter().cloned().collect();
|
||||
nc.static_ips = member.ip_assignments.iter().cloned().collect();
|
||||
nc.rules = network.rules;
|
||||
nc.dns = network.dns;
|
||||
nc.dns = network.dns.iter().map(|(k, v)| (k.clone(), v.iter().cloned().collect())).collect();
|
||||
|
||||
if network.min_supported_version.unwrap_or(0) < (protocol::PROTOCOL_VERSION_V2 as u32) {
|
||||
// If this network supports V1 nodes we have to include V1 credentials. Otherwise we can skip
|
||||
|
@ -399,7 +434,7 @@ impl Controller {
|
|||
coo.add_ip(ip);
|
||||
}
|
||||
if !coo.sign(&self.local_identity, &source_identity) {
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None, None));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
v1cred.certificates_of_ownership.push(coo);
|
||||
}
|
||||
|
@ -407,7 +442,7 @@ impl Controller {
|
|||
for (id, value) in member.tags.iter() {
|
||||
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));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
let _ = v1cred.tags.insert(*id, tag.unwrap());
|
||||
}
|
||||
|
@ -433,7 +468,7 @@ impl Controller {
|
|||
|
||||
nc.v1_credentials = Some(v1cred);
|
||||
} else {
|
||||
return Ok((AuthorizationResult::RejectedDueToError, None, None));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -458,7 +493,7 @@ impl Controller {
|
|||
self.database.save_member(member, false).await?;
|
||||
}
|
||||
|
||||
Ok((authorization_result, network_config, revocations))
|
||||
Ok((authentication_result, network_config, revocations))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ pub struct NetworkExport {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[repr(u8)]
|
||||
pub enum AuthorizationResult {
|
||||
pub enum AuthenticationResult {
|
||||
#[serde(rename = "r")]
|
||||
Rejected = 0,
|
||||
#[serde(rename = "rs")]
|
||||
|
@ -51,10 +51,10 @@ pub enum AuthorizationResult {
|
|||
#[serde(rename = "at")]
|
||||
ApprovedViaToken = 130,
|
||||
#[serde(rename = "ap")]
|
||||
ApprovedOnPublicNetwork = 131,
|
||||
ApprovedIsPublicNetwork = 131,
|
||||
}
|
||||
|
||||
impl AuthorizationResult {
|
||||
impl AuthenticationResult {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
// These short codes should match the serde enum names above.
|
||||
match self {
|
||||
|
@ -67,20 +67,20 @@ impl AuthorizationResult {
|
|||
Self::Approved => "a",
|
||||
Self::ApprovedViaSSO => "as",
|
||||
Self::ApprovedViaToken => "at",
|
||||
Self::ApprovedOnPublicNetwork => "ap",
|
||||
Self::ApprovedIsPublicNetwork => "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,
|
||||
Self::Approved | Self::ApprovedViaSSO | Self::ApprovedViaToken | Self::ApprovedIsPublicNetwork => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for AuthorizationResult {
|
||||
impl ToString for AuthenticationResult {
|
||||
fn to_string(&self) -> String {
|
||||
self.as_str().to_string()
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ pub struct RequestLogItem {
|
|||
pub source_hops: u8,
|
||||
|
||||
#[serde(rename = "r")]
|
||||
pub result: AuthorizationResult,
|
||||
pub result: AuthenticationResult,
|
||||
|
||||
#[serde(rename = "nc")]
|
||||
pub config: Option<NetworkConfig>,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// (c) 2020-2022 ZeroTier, Inc. -- currently proprietary pending actual release and licensing. See LICENSE.md.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::hash::Hash;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -28,7 +28,7 @@ pub struct Ipv6AssignMode {
|
|||
pub _6plane: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Debug)]
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash, Debug)]
|
||||
pub struct IpAssignmentPool {
|
||||
#[serde(rename = "ipRangeStart")]
|
||||
ip_range_start: InetAddress,
|
||||
|
@ -74,21 +74,21 @@ pub struct Network {
|
|||
pub v6_assign_mode: Option<Ipv6AssignMode>,
|
||||
|
||||
/// IPv4 or IPv6 auto-assignment pools available, must be present to use 'zt' mode.
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
|
||||
#[serde(rename = "ipAssignmentPools")]
|
||||
#[serde(default)]
|
||||
pub ip_assignment_pools: HashSet<IpAssignmentPool>,
|
||||
pub ip_assignment_pools: BTreeSet<IpAssignmentPool>,
|
||||
|
||||
/// IPv4 or IPv6 routes to advertise.
|
||||
#[serde(rename = "ipRoutes")]
|
||||
#[serde(skip_serializing_if = "HashSet::is_empty")]
|
||||
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
|
||||
#[serde(default)]
|
||||
pub ip_routes: HashSet<IpRoute>,
|
||||
pub ip_routes: BTreeSet<IpRoute>,
|
||||
|
||||
/// DNS records to push to members.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "HashMap::is_empty")]
|
||||
pub dns: HashMap<String, HashSet<InetAddress>>,
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub dns: BTreeMap<String, BTreeSet<InetAddress>>,
|
||||
|
||||
/// Network rule set.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
|
@ -148,9 +148,9 @@ impl Network {
|
|||
enable_broadcast: None,
|
||||
v4_assign_mode: None,
|
||||
v6_assign_mode: None,
|
||||
ip_assignment_pools: HashSet::new(),
|
||||
ip_routes: HashSet::new(),
|
||||
dns: HashMap::new(),
|
||||
ip_assignment_pools: BTreeSet::new(),
|
||||
ip_routes: BTreeSet::new(),
|
||||
dns: BTreeMap::new(),
|
||||
rules: Vec::new(),
|
||||
credential_ttl: None,
|
||||
min_supported_version: None,
|
||||
|
|
|
@ -22,6 +22,7 @@ pub(crate) enum PathServiceResult {
|
|||
}
|
||||
|
||||
/// A remote endpoint paired with a local socket and a local interface.
|
||||
///
|
||||
/// These are maintained in Node and canonicalized so that all unique paths have
|
||||
/// one and only one unique path object. That enables statistics to be tracked
|
||||
/// for them and uniform application of things like keepalives.
|
||||
|
|
|
@ -447,7 +447,7 @@ pub struct V1Credentials {
|
|||
}
|
||||
|
||||
/// Statically pushed L3 IP routes included with a network configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct IpRoute {
|
||||
pub target: InetAddress,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
|
|
Loading…
Add table
Reference in a new issue