mirror of
https://github.com/zerotier/ZeroTierOne.git
synced 2025-06-11 15:03:45 +02:00
More controller work: fix to Rule, change strategy for blacklisting lingering deauthed members to something cleaner and lighter, move Revocation into VL2 since it will likely stick around post-V1.
This commit is contained in:
parent
52770ddaef
commit
c94c0cfa7f
6 changed files with 162 additions and 97 deletions
|
@ -12,8 +12,7 @@ use zerotier_network_hypervisor::vl1::*;
|
|||
use zerotier_network_hypervisor::vl2;
|
||||
use zerotier_network_hypervisor::vl2::multicastauthority::MulticastAuthority;
|
||||
use zerotier_network_hypervisor::vl2::networkconfig::*;
|
||||
use zerotier_network_hypervisor::vl2::v1::Revocation;
|
||||
use zerotier_network_hypervisor::vl2::NetworkId;
|
||||
use zerotier_network_hypervisor::vl2::{NetworkId, Revocation};
|
||||
use zerotier_utils::blob::Blob;
|
||||
use zerotier_utils::buffer::OutOfBoundsError;
|
||||
use zerotier_utils::error::InvalidParameterError;
|
||||
|
@ -199,7 +198,7 @@ impl Controller {
|
|||
}
|
||||
|
||||
/// 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<Revocation>) {
|
||||
fn send_revocations(&self, peer: &Peer, revocations: &mut Vec<Revocation>) {
|
||||
if let Some(host_system) = self.service.read().unwrap().upgrade() {
|
||||
let time_ticks = ms_monotonic();
|
||||
while !revocations.is_empty() {
|
||||
|
@ -220,7 +219,7 @@ impl Controller {
|
|||
packet.append_u16(send_count as u16)?;
|
||||
for _ in 0..send_count {
|
||||
let r = revocations.pop().unwrap();
|
||||
packet.append_bytes(r.to_bytes(self.local_identity.address).as_bytes())?;
|
||||
packet.append_bytes(r.v1_proto_to_bytes(self.local_identity.address).as_bytes())?;
|
||||
}
|
||||
packet.append_u16(0)?;
|
||||
|
||||
|
@ -241,14 +240,10 @@ impl Controller {
|
|||
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);
|
||||
}
|
||||
revocations.clear();
|
||||
Revocation::new(member.network_id, time_clock, member.node_id, *m, &self.local_identity, false)
|
||||
.map(|r| revocations.push(r));
|
||||
self.send_revocations(&peer, &mut revocations);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -260,9 +255,6 @@ impl Controller {
|
|||
/// 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).
|
||||
///
|
||||
/// This may also return revocations. If it does these should be sent along with or right after
|
||||
/// the network config. This is for V1 nodes only, since V2 has another mechanism.
|
||||
///
|
||||
/// 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 authorize(
|
||||
|
@ -270,10 +262,10 @@ impl Controller {
|
|||
source_identity: &Verified<Identity>,
|
||||
network_id: NetworkId,
|
||||
time_clock: i64,
|
||||
) -> Result<(AuthenticationResult, Option<NetworkConfig>, Option<Vec<vl2::v1::Revocation>>), Box<dyn Error + Send + Sync>> {
|
||||
) -> Result<(AuthenticationResult, Option<NetworkConfig>), Box<dyn Error + Send + Sync>> {
|
||||
let network = self.database.get_network(network_id).await?;
|
||||
if network.is_none() {
|
||||
return Ok((AuthenticationResult::Rejected, None, None));
|
||||
return Ok((AuthenticationResult::Rejected, None));
|
||||
}
|
||||
let network = network.unwrap();
|
||||
|
||||
|
@ -298,7 +290,7 @@ impl Controller {
|
|||
let _ = member.identity_fingerprint.replace(Blob::from(source_identity.fingerprint));
|
||||
member_changed = true;
|
||||
} else {
|
||||
return Ok((AuthenticationResult::RejectedIdentityMismatch, None, None));
|
||||
return Ok((AuthenticationResult::RejectedIdentityMismatch, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -313,7 +305,7 @@ impl Controller {
|
|||
member_changed = true;
|
||||
}
|
||||
} else {
|
||||
return Ok((AuthenticationResult::RejectedIdentityMismatch, None, None));
|
||||
return Ok((AuthenticationResult::RejectedIdentityMismatch, None));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -332,7 +324,7 @@ impl Controller {
|
|||
let _ = member.insert(Member::new_with_identity(source_identity.as_ref().clone(), network_id));
|
||||
member_changed = true;
|
||||
} else {
|
||||
return Ok((AuthenticationResult::Rejected, None, None));
|
||||
return Ok((AuthenticationResult::Rejected, None));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -361,13 +353,12 @@ impl Controller {
|
|||
assert!(!authentication_result.approved());
|
||||
}
|
||||
|
||||
// drop 'mut' from these
|
||||
// drop 'mut' from these since they should no longer change
|
||||
let member_authorized = member_authorized;
|
||||
let authentication_result = authentication_result;
|
||||
|
||||
let mut network_config = None;
|
||||
let mut revocations = None;
|
||||
if authentication_result.approved() {
|
||||
// Generate network configuration if the member is authorized.
|
||||
let network_config = if authentication_result.approved() {
|
||||
// We should not be able to make it here if this is still false.
|
||||
assert!(member_authorized);
|
||||
|
||||
|
@ -391,7 +382,7 @@ impl Controller {
|
|||
.eq(member.identity_fingerprint.as_ref().unwrap().as_bytes())
|
||||
{
|
||||
debug_assert!(false);
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None));
|
||||
}
|
||||
|
||||
// Figure out TTL for credentials (time window in V1).
|
||||
|
@ -410,7 +401,38 @@ impl Controller {
|
|||
nc.mtu = network.mtu.unwrap_or(ZEROTIER_VIRTUAL_NETWORK_DEFAULT_MTU as u16);
|
||||
nc.routes = network.ip_routes.iter().cloned().collect();
|
||||
nc.static_ips = member.ip_assignments.iter().cloned().collect();
|
||||
nc.rules = network.rules;
|
||||
|
||||
// For any members that have been deauthorized but may still be in the cert agreement window,
|
||||
// insert rules to drop packets to/from those members. This lets us ban them without
|
||||
// adjusting the window, which is a simpler approach and has less risk of interrupting
|
||||
// connectivity between valid members.
|
||||
if let Ok(mut deauthed_members_still_in_window) = self
|
||||
.database
|
||||
.list_members_deauthorized_after(network.id, time_clock - (credential_ttl as i64))
|
||||
.await
|
||||
{
|
||||
if !deauthed_members_still_in_window.is_empty() {
|
||||
deauthed_members_still_in_window.sort_unstable(); // may improve packet compression
|
||||
nc.rules.reserve(deauthed_members_still_in_window.len() + 1);
|
||||
let mut or = false;
|
||||
for dead in deauthed_members_still_in_window.iter() {
|
||||
nc.rules.push(vl2::rule::Rule::match_source_zerotier_address(false, or, *dead));
|
||||
or = true;
|
||||
}
|
||||
nc.rules.push(vl2::rule::Rule::action_drop());
|
||||
}
|
||||
}
|
||||
|
||||
// Then add the rest of the user-defined rules, or a blanket accept if there are none.
|
||||
if let Some(rules) = network.rules.as_ref() {
|
||||
nc.rules.reserve(rules.len());
|
||||
for r in rules.iter() {
|
||||
nc.rules.push(r.clone());
|
||||
}
|
||||
} else {
|
||||
nc.rules.push(vl2::rule::Rule::action_accept());
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -434,7 +456,7 @@ impl Controller {
|
|||
coo.add_ip(ip);
|
||||
}
|
||||
if !coo.sign(&self.local_identity, &source_identity) {
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None));
|
||||
}
|
||||
v1cred.certificates_of_ownership.push(coo);
|
||||
}
|
||||
|
@ -442,33 +464,14 @@ 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((AuthenticationResult::RejectedDueToError, None, None));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None));
|
||||
}
|
||||
let _ = v1cred.tags.insert(*id, tag.unwrap());
|
||||
}
|
||||
|
||||
// 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, 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, time_clock, *dm, source_identity.address, &self.local_identity, false)
|
||||
{
|
||||
revs.push(rev);
|
||||
}
|
||||
}
|
||||
revocations = Some(revs);
|
||||
}
|
||||
}
|
||||
|
||||
nc.v1_credentials = Some(v1cred);
|
||||
} else {
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None, None));
|
||||
return Ok((AuthenticationResult::RejectedDueToError, None));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -486,14 +489,17 @@ impl Controller {
|
|||
.or_default()
|
||||
.insert(network_id, ms_monotonic() + (credential_ttl as i64));
|
||||
|
||||
network_config = Some(nc);
|
||||
}
|
||||
Some(nc)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Save any changes to member record.
|
||||
if member_changed {
|
||||
self.database.save_member(member, false).await?;
|
||||
}
|
||||
|
||||
Ok((authentication_result, network_config, revocations))
|
||||
Ok((authentication_result, network_config))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -558,15 +564,12 @@ impl InnerProtocol for Controller {
|
|||
let now = ms_since_epoch();
|
||||
|
||||
let (result, config) = match self2.authorize(&source.identity, network_id, now).await {
|
||||
Result::Ok((result, Some(config), revocations)) => {
|
||||
Result::Ok((result, Some(config))) => {
|
||||
//println!("{}", serde_yaml::to_string(&config).unwrap());
|
||||
self2.send_network_config(source.as_ref(), &config, Some(message_id));
|
||||
if let Some(mut revocations) = revocations {
|
||||
self2.v1_proto_send_revocations(source.as_ref(), &mut revocations);
|
||||
}
|
||||
(result, Some(config))
|
||||
}
|
||||
Result::Ok((result, None, _)) => (result, None),
|
||||
Result::Ok((result, None)) => (result, None),
|
||||
Result::Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
let host = self2.service.read().unwrap().clone().upgrade().unwrap();
|
||||
|
|
|
@ -13,7 +13,7 @@ use zerotier_network_hypervisor::vl2::NetworkId;
|
|||
use crate::database::Database;
|
||||
use crate::model::Member;
|
||||
|
||||
pub const CREDENTIAL_WINDOW_SIZE_DEFAULT: u64 = 1000 * 60 * 60;
|
||||
pub const CREDENTIAL_WINDOW_SIZE_DEFAULT: u64 = 1000 * 60 * 60; // 1 hour
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Default, Debug)]
|
||||
pub struct Ipv4AssignMode {
|
||||
|
@ -90,10 +90,10 @@ pub struct Network {
|
|||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub dns: BTreeMap<String, BTreeSet<InetAddress>>,
|
||||
|
||||
/// Network rule set.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
/// Network rule set. (Default: one 'accept' rule.)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[serde(default)]
|
||||
pub rules: Vec<Rule>,
|
||||
pub rules: Option<Vec<Rule>>,
|
||||
|
||||
/// If set this overrides the default TTL for certificates and credentials.
|
||||
///
|
||||
|
@ -151,7 +151,7 @@ impl Network {
|
|||
ip_assignment_pools: BTreeSet::new(),
|
||||
ip_routes: BTreeSet::new(),
|
||||
dns: BTreeMap::new(),
|
||||
rules: Vec::new(),
|
||||
rules: None,
|
||||
credential_ttl: None,
|
||||
min_supported_version: None,
|
||||
mtu: None,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
mod multicastgroup;
|
||||
mod networkid;
|
||||
mod revocation;
|
||||
mod switch;
|
||||
|
||||
pub mod multicastauthority;
|
||||
|
@ -11,4 +12,5 @@ pub mod v1;
|
|||
|
||||
pub use multicastgroup::MulticastGroup;
|
||||
pub use networkid::NetworkId;
|
||||
pub use revocation::Revocation;
|
||||
pub use switch::{Switch, SwitchInterface};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::io::Write;
|
||||
|
||||
use zerotier_crypto::random;
|
||||
use zerotier_crypto::verified::Verified;
|
||||
use zerotier_utils::arrayvec::ArrayVec;
|
||||
|
||||
|
@ -10,9 +9,9 @@ use crate::vl1::{Address, Identity};
|
|||
use crate::vl2::v1::CredentialType;
|
||||
use crate::vl2::NetworkId;
|
||||
|
||||
/// "Anti-credential" revoking a network member's permission to communicate on a network.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct Revocation {
|
||||
pub id: u32,
|
||||
pub network_id: NetworkId,
|
||||
pub threshold: i64,
|
||||
pub target: Address,
|
||||
|
@ -31,7 +30,6 @@ impl Revocation {
|
|||
fast_propagate: bool,
|
||||
) -> Option<Self> {
|
||||
let mut r = Self {
|
||||
id: random::xorshift64_random() as u32, // arbitrary
|
||||
network_id,
|
||||
threshold,
|
||||
target,
|
||||
|
@ -54,7 +52,7 @@ impl Revocation {
|
|||
}
|
||||
|
||||
let _ = v.write_all(&[0; 4]);
|
||||
let _ = v.write_all(&self.id.to_be_bytes());
|
||||
let _ = v.write_all(&((self.threshold as u32) ^ (u64::from(self.target) as u32)).to_be_bytes()); // ID only used in V1, arbitrary
|
||||
let _ = v.write_all(&self.network_id.to_bytes());
|
||||
let _ = v.write_all(&[0; 8]);
|
||||
let _ = v.write_all(&self.threshold.to_be_bytes());
|
||||
|
@ -76,7 +74,7 @@ impl Revocation {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_bytes(&self, controller_address: Address) -> ArrayVec<u8, 256> {
|
||||
pub fn v1_proto_to_bytes(&self, controller_address: Address) -> ArrayVec<u8, 256> {
|
||||
self.internal_to_bytes(false, controller_address)
|
||||
}
|
||||
}
|
|
@ -135,28 +135,39 @@ mod rule_value {
|
|||
}
|
||||
}
|
||||
|
||||
fn t(not: bool, or: bool, action_or_condition: u8) -> u8 {
|
||||
(not as u8).wrapping_shl(7) | (or as u8).wrapping_shl(4) | action_or_condition
|
||||
}
|
||||
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy)]
|
||||
union RuleValue {
|
||||
pub ipv6: rule_value::Ipv6,
|
||||
pub ipv4: rule_value::Ipv4,
|
||||
pub int_range: rule_value::IntRange,
|
||||
pub characteristics: u64,
|
||||
pub port_range: [u16; 2],
|
||||
pub zt: u64,
|
||||
pub random_probability: u32,
|
||||
pub mac: [u8; 6],
|
||||
pub vlan_id: u16,
|
||||
pub vlan_pcp: u8,
|
||||
pub vlan_dei: u8,
|
||||
pub ethertype: u16,
|
||||
pub ip_protocol: u8,
|
||||
pub ip_tos: rule_value::IpTos,
|
||||
pub frame_size_range: [u16; 2],
|
||||
pub icmp: rule_value::Icmp,
|
||||
pub tag: rule_value::Tag,
|
||||
pub forward: rule_value::Forward,
|
||||
pub qos_bucket: u8,
|
||||
ipv6: rule_value::Ipv6,
|
||||
ipv4: rule_value::Ipv4,
|
||||
int_range: rule_value::IntRange,
|
||||
characteristics: u64,
|
||||
port_range: [u16; 2],
|
||||
zt: u64,
|
||||
random_probability: u32,
|
||||
mac: [u8; 6],
|
||||
vlan_id: u16,
|
||||
vlan_pcp: u8,
|
||||
vlan_dei: u8,
|
||||
ethertype: u16,
|
||||
ip_protocol: u8,
|
||||
ip_tos: rule_value::IpTos,
|
||||
frame_size_range: [u16; 2],
|
||||
icmp: rule_value::Icmp,
|
||||
tag: rule_value::Tag,
|
||||
forward: rule_value::Forward,
|
||||
qos_bucket: u8,
|
||||
}
|
||||
|
||||
impl Default for RuleValue {
|
||||
#[inline(always)]
|
||||
fn default() -> Self {
|
||||
unsafe { zeroed() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait to implement in order to evaluate rules.
|
||||
|
@ -216,6 +227,65 @@ impl Default for Rule {
|
|||
}
|
||||
|
||||
impl Rule {
|
||||
pub fn action_accept() -> Self {
|
||||
Self { t: action::ACCEPT, v: RuleValue::default() }
|
||||
}
|
||||
|
||||
pub fn action_drop() -> Self {
|
||||
Self { t: action::DROP, v: RuleValue::default() }
|
||||
}
|
||||
|
||||
pub fn action_tee(address: Address, flags: u32, length: u16) -> Self {
|
||||
Self {
|
||||
t: action::TEE,
|
||||
v: RuleValue {
|
||||
forward: rule_value::Forward { address: address.into(), flags, length },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_watch(address: Address, flags: u32, length: u16) -> Self {
|
||||
Self {
|
||||
t: action::TEE,
|
||||
v: RuleValue {
|
||||
forward: rule_value::Forward { address: address.into(), flags, length },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_redirect(address: Address, flags: u32, length: u16) -> Self {
|
||||
Self {
|
||||
t: action::TEE,
|
||||
v: RuleValue {
|
||||
forward: rule_value::Forward { address: address.into(), flags, length },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_break() -> Self {
|
||||
Self { t: action::BREAK, v: RuleValue::default() }
|
||||
}
|
||||
|
||||
pub fn action_priority(qos_bucket: u8) -> Self {
|
||||
Self { t: action::PRIORITY, v: RuleValue { qos_bucket } }
|
||||
}
|
||||
|
||||
pub fn match_source_zerotier_address(not: bool, or: bool, address: Address) -> Self {
|
||||
Self {
|
||||
t: t(not, or, match_cond::SOURCE_ZEROTIER_ADDRESS),
|
||||
v: RuleValue { zt: address.into() },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_dest_zerotier_address(not: bool, or: bool, address: Address) -> Self {
|
||||
Self {
|
||||
t: t(not, or, match_cond::DEST_ZEROTIER_ADDRESS),
|
||||
v: RuleValue { zt: address.into() },
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: implement the rest of these static constructor methods if/when needed
|
||||
|
||||
#[inline(always)]
|
||||
pub fn action_or_condition(&self) -> u8 {
|
||||
self.t & 0x3f
|
||||
|
@ -229,6 +299,9 @@ impl Rule {
|
|||
let not = (t & 0x80) != 0;
|
||||
let or = (t & 0x40) != 0;
|
||||
match t & 0x3f {
|
||||
action::DROP => {
|
||||
return v.action_drop();
|
||||
}
|
||||
action::ACCEPT => {
|
||||
return v.action_accept();
|
||||
}
|
||||
|
@ -760,16 +833,7 @@ impl<'a> HumanReadableRule<'a> {
|
|||
fn to_rule(&self) -> Option<Rule> {
|
||||
if let Some(t) = HR_NAME_TO_RULE_TYPE.get(self._type.to_uppercase().as_str()) {
|
||||
let mut r = Rule::default();
|
||||
r.t =
|
||||
*t | if self.not.unwrap_or(false) {
|
||||
0x80
|
||||
} else {
|
||||
0
|
||||
} | if self.or.unwrap_or(false) {
|
||||
0x40
|
||||
} else {
|
||||
0
|
||||
};
|
||||
r.t = (self.not.unwrap_or(false) as u8).wrapping_shl(7) | (self.or.unwrap_or(false) as u8).wrapping_shl(6);
|
||||
unsafe {
|
||||
match *t {
|
||||
action::TEE | action::WATCH | action::REDIRECT => {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
mod certificateofmembership;
|
||||
mod certificateofownership;
|
||||
mod revocation;
|
||||
mod tag;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
|
@ -16,5 +15,4 @@ pub enum CredentialType {
|
|||
|
||||
pub use certificateofmembership::CertificateOfMembership;
|
||||
pub use certificateofownership::{CertificateOfOwnership, Thing};
|
||||
pub use revocation::Revocation;
|
||||
pub use tag::Tag;
|
||||
|
|
Loading…
Add table
Reference in a new issue