mirror of
https://gitlab.com/veilid/veilid.git
synced 2024-12-25 07:19:26 -05:00
[skip ci] correct addresspool implementation
This commit is contained in:
parent
f09b367d05
commit
657b66815d
@ -128,22 +128,22 @@ blueprints:
|
|||||||
# with both ipv4 and ipv6 networking
|
# with both ipv4 and ipv6 networking
|
||||||
direct:
|
direct:
|
||||||
ipv4:
|
ipv4:
|
||||||
additional_prefix: 24
|
prefix: 24
|
||||||
ipv6:
|
ipv6:
|
||||||
additional_prefix: 64
|
prefix: 64
|
||||||
# An ipv4-only subnet of the internet directly attached with no translation
|
# An ipv4-only subnet of the internet directly attached with no translation
|
||||||
direct_ipv4_no_ipv6:
|
direct_ipv4_no_ipv6:
|
||||||
ipv4:
|
ipv4:
|
||||||
additional_prefix: 24
|
prefix: 24
|
||||||
# An ipv6-only subnet of the internet directly attached with no translation
|
# An ipv6-only subnet of the internet directly attached with no translation
|
||||||
direct_ipv6_no_ipv4:
|
direct_ipv6_no_ipv4:
|
||||||
ipv6:
|
ipv6:
|
||||||
additional_prefix: 64
|
prefix: 64
|
||||||
# An ipv4-only subnet of the internet attached via NAT
|
# An ipv4-only subnet of the internet attached via NAT
|
||||||
nat_ipv4_no_ipv6:
|
nat_ipv4_no_ipv6:
|
||||||
ipv4:
|
ipv4:
|
||||||
allocation: "$private"
|
allocation: "$private"
|
||||||
additional_prefix: 0
|
prefix: 24
|
||||||
gateway:
|
gateway:
|
||||||
translation: "port_restricted"
|
translation: "port_restricted"
|
||||||
upnp: 0.25
|
upnp: 0.25
|
||||||
@ -152,12 +152,12 @@ blueprints:
|
|||||||
nat_ipv4_direct_ipv6:
|
nat_ipv4_direct_ipv6:
|
||||||
ipv4:
|
ipv4:
|
||||||
allocation: "$private"
|
allocation: "$private"
|
||||||
additional_prefix: 0
|
prefix: 24
|
||||||
gateway:
|
gateway:
|
||||||
translation: "port_restricted"
|
translation: "port_restricted"
|
||||||
upnp: 0.25
|
upnp: 0.25
|
||||||
ipv6:
|
ipv6:
|
||||||
additional_prefix: 56
|
prefix: 56
|
||||||
|
|
||||||
#################################################################
|
#################################################################
|
||||||
# Allocations
|
# Allocations
|
||||||
|
@ -4,6 +4,10 @@ use ipnet::*;
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AddressPool {
|
pub struct AddressPool {
|
||||||
srng: StableRng,
|
srng: StableRng,
|
||||||
|
|
||||||
|
scope_v4: Vec<Ipv4Net>,
|
||||||
|
scope_v6: Vec<Ipv6Net>,
|
||||||
|
|
||||||
allocated_v4: Vec<Ipv4Net>,
|
allocated_v4: Vec<Ipv4Net>,
|
||||||
allocated_v6: Vec<Ipv6Net>,
|
allocated_v6: Vec<Ipv6Net>,
|
||||||
}
|
}
|
||||||
@ -12,90 +16,349 @@ impl AddressPool {
|
|||||||
pub fn new(srng: StableRng) -> Self {
|
pub fn new(srng: StableRng) -> Self {
|
||||||
Self {
|
Self {
|
||||||
srng,
|
srng,
|
||||||
|
scope_v4: Vec::new(),
|
||||||
|
scope_v6: Vec::new(),
|
||||||
allocated_v4: Vec::new(),
|
allocated_v4: Vec::new(),
|
||||||
allocated_v6: Vec::new(),
|
allocated_v6: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains_v4(&self, allocation: &Ipv4Net) -> bool {
|
/////////////////////////////////////////////////////////////////////
|
||||||
for x in &self.allocated_v4 {
|
|
||||||
if x.contains(allocation) {
|
pub fn add_scope_v4(&mut self, allocation: Ipv4Net) {
|
||||||
|
self.scope_v4.push(allocation);
|
||||||
|
self.scope_v4 = Ipv4Net::aggregate(&self.scope_v4);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_scope_v6(&mut self, allocation: Ipv6Net) {
|
||||||
|
self.scope_v6.push(allocation);
|
||||||
|
self.scope_v6 = Ipv6Net::aggregate(&self.scope_v6);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_in_scope_v4(&self, allocation: Ipv4Net) -> bool {
|
||||||
|
for x in &self.scope_v4 {
|
||||||
|
if x.contains(&allocation) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains_v6(&self, allocation: &Ipv6Net) -> bool {
|
pub fn is_in_scope_v6(&self, allocation: Ipv6Net) -> bool {
|
||||||
for x in &self.allocated_v6 {
|
for x in &self.scope_v6 {
|
||||||
if x.contains(allocation) {
|
if x.contains(&allocation) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_v4(&mut self, allocation: Ipv4Net) {
|
/////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub fn allocate_v4(&mut self, allocation: Ipv4Net) -> MachineRegistryResult<()> {
|
||||||
|
// Ensure the allocation is in our scope
|
||||||
|
if !self.is_in_scope_v4(allocation) {
|
||||||
|
return Err(MachineRegistryError::NoAllocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to our allocated pool
|
||||||
self.allocated_v4.push(allocation);
|
self.allocated_v4.push(allocation);
|
||||||
self.allocated_v4 = Ipv4Net::aggregate(&self.allocated_v4);
|
self.allocated_v4 = Ipv4Net::aggregate(&self.allocated_v4);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_v6(&mut self, allocation: Ipv6Net) {
|
pub fn allocate_v6(&mut self, allocation: Ipv6Net) -> MachineRegistryResult<()> {
|
||||||
|
// Ensure the allocation is in our scope
|
||||||
|
if !self.is_in_scope_v6(allocation) {
|
||||||
|
return Err(MachineRegistryError::NoAllocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to our allocated pool
|
||||||
self.allocated_v6.push(allocation);
|
self.allocated_v6.push(allocation);
|
||||||
self.allocated_v6 = Ipv6Net::aggregate(&self.allocated_v6);
|
self.allocated_v6 = Ipv6Net::aggregate(&self.allocated_v6);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_random_subnet_v4(
|
pub fn get_overlaps_v4(&self, allocation: Ipv4Net) -> Vec<Ipv4Net> {
|
||||||
&mut self,
|
let mut overlaps = Vec::<Ipv4Net>::new();
|
||||||
allocation: &Ipv4Net,
|
for x in &self.allocated_v4 {
|
||||||
additional_prefix: u8,
|
if x.contains(&allocation) || allocation.contains(x) {
|
||||||
) -> Option<Ipv4Net> {
|
overlaps.push(*x);
|
||||||
// Apply the additional prefix
|
overlaps = Ipv4Net::aggregate(&overlaps);
|
||||||
let prefix = u8::max(
|
}
|
||||||
allocation.prefix_len() + additional_prefix,
|
}
|
||||||
allocation.max_prefix_len(),
|
overlaps
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_overlaps_v6(&self, allocation: Ipv6Net) -> Vec<Ipv6Net> {
|
||||||
|
let mut overlaps = Vec::<Ipv6Net>::new();
|
||||||
|
for x in &self.allocated_v6 {
|
||||||
|
if x.contains(&allocation) || allocation.contains(x) {
|
||||||
|
overlaps.push(*x);
|
||||||
|
overlaps = Ipv6Net::aggregate(&overlaps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overlaps
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_in_prefix_32(scope_prefix: u8, iterable_prefix_bits: u8) -> u32 {
|
||||||
|
// If we're allocating addresses, exclude scope's network and broadcast address
|
||||||
|
if scope_prefix + iterable_prefix_bits == 32 {
|
||||||
|
// Subtract two from total
|
||||||
|
if scope_prefix == 0 {
|
||||||
|
// Overflow case
|
||||||
|
0xFFFF_FFFEu32
|
||||||
|
} else {
|
||||||
|
// Non-overflow case
|
||||||
|
(1u32 << iterable_prefix_bits) - 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// network only iteration
|
||||||
|
1u32 << iterable_prefix_bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn range_in_prefix_128(scope_prefix: u8, iterable_prefix_bits: u8) -> u128 {
|
||||||
|
// If we're allocating addresses, exclude scope's network and broadcast address
|
||||||
|
if scope_prefix + iterable_prefix_bits == 128 {
|
||||||
|
// Subtract two from total
|
||||||
|
if scope_prefix == 0 {
|
||||||
|
// Overflow case
|
||||||
|
0xFFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFEu128
|
||||||
|
} else {
|
||||||
|
// Non-overflow case
|
||||||
|
(1u128 << iterable_prefix_bits) - 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// network only iteration
|
||||||
|
1u128 << iterable_prefix_bits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocate_random_v4(&mut self, prefix: u8) -> Option<Ipv4Net> {
|
||||||
|
// Scope ranges to iterate
|
||||||
|
let mut scope_ranges = Vec::<(Ipv4Net, u8, u32)>::new();
|
||||||
|
let mut total_subnets = 0u32;
|
||||||
|
|
||||||
|
// Build range set from scopes, minus the prefix to allocate
|
||||||
|
for scope in self.scope_v4.iter().copied() {
|
||||||
|
// If the prefix we are looking to allocate doesn't fit in this scope
|
||||||
|
// then we exclude it
|
||||||
|
if scope.prefix_len() > prefix {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the number of prefix bits we can iterate
|
||||||
|
let iterable_prefix_bits = prefix - scope.prefix_len();
|
||||||
|
let iterable_range = Self::range_in_prefix_32(scope.prefix_len(), iterable_prefix_bits);
|
||||||
|
|
||||||
|
// Scope ranges to try
|
||||||
|
scope_ranges.push((scope, iterable_prefix_bits, iterable_range));
|
||||||
|
total_subnets += iterable_range;
|
||||||
|
}
|
||||||
|
if total_subnets == 0 {
|
||||||
|
// No range
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a random subnet to start with
|
||||||
|
let chosen_subnet_index = self.srng.next_u32(0, total_subnets - 1);
|
||||||
|
|
||||||
|
// Find the starting scope and starting subnet index within
|
||||||
|
// the scope of the chosen subnet index
|
||||||
|
let mut scope_index = 0usize;
|
||||||
|
let mut scope_start_subnet_index = 0u32;
|
||||||
|
loop {
|
||||||
|
assert!(
|
||||||
|
scope_index < scope_ranges.len(),
|
||||||
|
"should always have chosen a starting point inside a scope"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the subnets with this prefix
|
let scope_end_subnet_index = scope_start_subnet_index + scope_ranges[scope_index].2;
|
||||||
let mut subnets = allocation.subnets(prefix).ok()?.collect::<Vec<Ipv4Net>>();
|
if chosen_subnet_index < scope_end_subnet_index {
|
||||||
|
break;
|
||||||
// Randomize the subnets
|
|
||||||
self.srng.shuffle_vec(&mut subnets);
|
|
||||||
|
|
||||||
// Pick the first available subnet
|
|
||||||
for subnet in subnets {
|
|
||||||
if !self.contains_v4(&subnet) {
|
|
||||||
self.add_v4(subnet);
|
|
||||||
return Some(subnet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chosen starting point is in the next scope
|
||||||
|
scope_index += 1;
|
||||||
|
scope_start_subnet_index = scope_end_subnet_index;
|
||||||
}
|
}
|
||||||
|
let initial_subnet_index = chosen_subnet_index;
|
||||||
|
let initial_scope_index = scope_index;
|
||||||
|
|
||||||
|
// Iterate forward until we find a free range
|
||||||
|
let mut current_subnet_index = initial_subnet_index;
|
||||||
|
let mut current_scope_index = initial_scope_index;
|
||||||
|
let mut current_scope_start_subnet_index = scope_start_subnet_index;
|
||||||
|
let mut current_scope_end_subnet_index =
|
||||||
|
scope_start_subnet_index + scope_ranges[scope_index].2;
|
||||||
|
|
||||||
|
let opt_allocation = loop {
|
||||||
|
// Get the net at this current subnet index
|
||||||
|
let netbits = u32::from(scope_ranges[current_scope_index].0.network());
|
||||||
|
let subnetbits = if prefix == 32 {
|
||||||
|
// Allocating addresses
|
||||||
|
((current_subnet_index - current_scope_start_subnet_index) + 1) << (32 - prefix)
|
||||||
|
} else {
|
||||||
|
// Allocating subnets
|
||||||
|
(current_subnet_index - current_scope_start_subnet_index) << (32 - prefix)
|
||||||
|
};
|
||||||
|
let net = Ipv4Net::new(Ipv4Addr::from(netbits | subnetbits), prefix)
|
||||||
|
.expect("prefix must be valid");
|
||||||
|
// See if this net is available
|
||||||
|
if self.get_overlaps_v4(net).is_empty() {
|
||||||
|
break Some(net);
|
||||||
|
}
|
||||||
|
// If not, go to the next subnet
|
||||||
|
current_subnet_index += 1;
|
||||||
|
|
||||||
|
// If we got back to the beginning we failed to allocate
|
||||||
|
if current_scope_index == initial_scope_index
|
||||||
|
&& current_subnet_index == initial_subnet_index
|
||||||
|
{
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've reached the end of this scope then go to the next scope
|
||||||
|
if current_subnet_index == current_scope_end_subnet_index {
|
||||||
|
current_scope_index += 1;
|
||||||
|
// Wrap around
|
||||||
|
if current_scope_index == scope_ranges.len() {
|
||||||
|
current_subnet_index = 0;
|
||||||
|
current_scope_index = 0;
|
||||||
|
current_scope_start_subnet_index = 0;
|
||||||
|
} else {
|
||||||
|
current_scope_start_subnet_index = current_scope_end_subnet_index;
|
||||||
|
}
|
||||||
|
current_scope_end_subnet_index =
|
||||||
|
current_scope_start_subnet_index + scope_ranges[current_scope_index].2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we found a free subnet, add it to our allocations
|
||||||
|
if let Some(allocation) = opt_allocation {
|
||||||
|
// Add to our allocated pool
|
||||||
|
self.allocated_v4.push(allocation);
|
||||||
|
self.allocated_v4 = Ipv4Net::aggregate(&self.allocated_v4);
|
||||||
|
return Some(allocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No allocation
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_random_subnet_v6(
|
pub fn allocate_random_v6(&mut self, prefix: u8) -> Option<Ipv6Net> {
|
||||||
&mut self,
|
// Scope ranges to iterate
|
||||||
allocation: &Ipv6Net,
|
let mut scope_ranges = Vec::<(Ipv6Net, u8, u128)>::new();
|
||||||
additional_prefix: u8,
|
let mut total_subnets = 0u128;
|
||||||
) -> Option<Ipv6Net> {
|
|
||||||
// Apply the additional prefix
|
// Build range set from scopes, minus the prefix to allocate
|
||||||
let prefix = u8::max(
|
for scope in self.scope_v6.iter().copied() {
|
||||||
allocation.prefix_len() + additional_prefix,
|
// If the prefix we are looking to allocate doesn't fit in this scope
|
||||||
allocation.max_prefix_len(),
|
// then we exclude it
|
||||||
|
if scope.prefix_len() > prefix {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the number of prefix bits we can iterate
|
||||||
|
let iterable_prefix_bits = prefix - scope.prefix_len();
|
||||||
|
let iterable_range =
|
||||||
|
Self::range_in_prefix_128(scope.prefix_len(), iterable_prefix_bits);
|
||||||
|
|
||||||
|
// Scope ranges to try
|
||||||
|
scope_ranges.push((scope, iterable_prefix_bits, iterable_range));
|
||||||
|
total_subnets += iterable_range;
|
||||||
|
}
|
||||||
|
if total_subnets == 0 {
|
||||||
|
// No range
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Choose a random subnet to start with
|
||||||
|
let chosen_subnet_index = self.srng.next_u128(0, total_subnets - 1);
|
||||||
|
|
||||||
|
// Find the starting scope and starting subnet index within
|
||||||
|
// the scope of the chosen subnet index
|
||||||
|
let mut scope_index = 0usize;
|
||||||
|
let mut scope_start_subnet_index = 0u128;
|
||||||
|
loop {
|
||||||
|
assert!(
|
||||||
|
scope_index < scope_ranges.len(),
|
||||||
|
"should always have chosen a starting point inside a scope"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get the subnets with this prefix
|
let scope_end_subnet_index = scope_start_subnet_index + scope_ranges[scope_index].2;
|
||||||
let mut subnets = allocation.subnets(prefix).ok()?.collect::<Vec<Ipv6Net>>();
|
if chosen_subnet_index < scope_end_subnet_index {
|
||||||
|
break;
|
||||||
// Randomize the subnets
|
|
||||||
self.srng.shuffle_vec(&mut subnets);
|
|
||||||
|
|
||||||
// Pick the first available subnet
|
|
||||||
for subnet in subnets {
|
|
||||||
if !self.contains_v6(&subnet) {
|
|
||||||
self.add_v6(subnet);
|
|
||||||
return Some(subnet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// chosen starting point is in the next scope
|
||||||
|
scope_index += 1;
|
||||||
|
scope_start_subnet_index = scope_end_subnet_index;
|
||||||
}
|
}
|
||||||
|
let initial_subnet_index = chosen_subnet_index;
|
||||||
|
let initial_scope_index = scope_index;
|
||||||
|
|
||||||
|
// Iterate forward until we find a free range
|
||||||
|
let mut current_subnet_index = initial_subnet_index;
|
||||||
|
let mut current_scope_index = initial_scope_index;
|
||||||
|
let mut current_scope_start_subnet_index = scope_start_subnet_index;
|
||||||
|
let mut current_scope_end_subnet_index =
|
||||||
|
scope_start_subnet_index + scope_ranges[scope_index].2;
|
||||||
|
|
||||||
|
let opt_allocation = loop {
|
||||||
|
// Get the net at this current subnet index
|
||||||
|
let netbits = u128::from(scope_ranges[current_scope_index].0.network());
|
||||||
|
let subnetbits = if prefix == 128 {
|
||||||
|
// Allocating addresses
|
||||||
|
((current_subnet_index - current_scope_start_subnet_index) + 1) << (128 - prefix)
|
||||||
|
} else {
|
||||||
|
// Allocating subnets
|
||||||
|
(current_subnet_index - current_scope_start_subnet_index) << (128 - prefix)
|
||||||
|
};
|
||||||
|
let net = Ipv6Net::new(Ipv6Addr::from(netbits | subnetbits), prefix)
|
||||||
|
.expect("prefix must be valid");
|
||||||
|
// See if this net is available
|
||||||
|
if self.get_overlaps_v6(net).is_empty() {
|
||||||
|
break Some(net);
|
||||||
|
}
|
||||||
|
// If not, go to the next subnet
|
||||||
|
current_subnet_index += 1;
|
||||||
|
|
||||||
|
// If we got back to the beginning we failed to allocate
|
||||||
|
if current_scope_index == initial_scope_index
|
||||||
|
&& current_subnet_index == initial_subnet_index
|
||||||
|
{
|
||||||
|
break None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we've reached the end of this scope then go to the next scope
|
||||||
|
if current_subnet_index == current_scope_end_subnet_index {
|
||||||
|
current_scope_index += 1;
|
||||||
|
// Wrap around
|
||||||
|
if current_scope_index == scope_ranges.len() {
|
||||||
|
current_subnet_index = 0;
|
||||||
|
current_scope_index = 0;
|
||||||
|
current_scope_start_subnet_index = 0;
|
||||||
|
} else {
|
||||||
|
current_scope_start_subnet_index = current_scope_end_subnet_index;
|
||||||
|
}
|
||||||
|
current_scope_end_subnet_index =
|
||||||
|
current_scope_start_subnet_index + scope_ranges[current_scope_index].2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// If we found a free subnet, add it to our allocations
|
||||||
|
if let Some(allocation) = opt_allocation {
|
||||||
|
// Add to our allocated pool
|
||||||
|
self.allocated_v6.push(allocation);
|
||||||
|
self.allocated_v6 = Ipv6Net::aggregate(&self.allocated_v6);
|
||||||
|
return Some(allocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No allocation
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,9 @@ impl MachineRegistryInner {
|
|||||||
pub fn srng(&self) -> StableRng {
|
pub fn srng(&self) -> StableRng {
|
||||||
self.unlocked_inner.srng.clone()
|
self.unlocked_inner.srng.clone()
|
||||||
}
|
}
|
||||||
|
pub fn config(&self) -> &config::Config {
|
||||||
|
&self.unlocked_inner.config
|
||||||
|
}
|
||||||
|
|
||||||
pub fn execute_config(&self, cfg: config::Config) -> MachineRegistryResult<()> {
|
pub fn execute_config(&self, cfg: config::Config) -> MachineRegistryResult<()> {
|
||||||
// Create all networks
|
// Create all networks
|
||||||
@ -479,7 +482,7 @@ impl MachineRegistryInner {
|
|||||||
// pub(super) fn choose_allocation_v4(
|
// pub(super) fn choose_allocation_v4(
|
||||||
// &mut self,
|
// &mut self,
|
||||||
// allocation: config::Allocation,
|
// allocation: config::Allocation,
|
||||||
// additional_prefix: u8,
|
// prefix: u8,
|
||||||
// ) -> MachineRegistryResult<Ipv4Net> {
|
// ) -> MachineRegistryResult<Ipv4Net> {
|
||||||
// // Get allocation subnet candidates
|
// // Get allocation subnet candidates
|
||||||
// let mut subnet4 = allocation
|
// let mut subnet4 = allocation
|
||||||
@ -495,7 +498,7 @@ impl MachineRegistryInner {
|
|||||||
// // Allocate within the subnet
|
// // Allocate within the subnet
|
||||||
// match self
|
// match self
|
||||||
// .address_pool
|
// .address_pool
|
||||||
// .add_random_subnet_v4(subnet, additional_prefix)
|
// .add_random_subnet_v4(subnet, prefix)
|
||||||
// {
|
// {
|
||||||
// Some(a) => {
|
// Some(a) => {
|
||||||
// // Got a sub-allocation
|
// // Got a sub-allocation
|
||||||
@ -520,7 +523,7 @@ impl MachineRegistryInner {
|
|||||||
// pub(super) fn choose_allocation_v6(
|
// pub(super) fn choose_allocation_v6(
|
||||||
// &mut self,
|
// &mut self,
|
||||||
// allocation: config::Allocation,
|
// allocation: config::Allocation,
|
||||||
// additional_prefix: u8,
|
// prefix: u8,
|
||||||
// ) -> MachineRegistryResult<Ipv6Net> {
|
// ) -> MachineRegistryResult<Ipv6Net> {
|
||||||
// // Get allocation subnet candidates
|
// // Get allocation subnet candidates
|
||||||
// let mut subnet6 = allocation
|
// let mut subnet6 = allocation
|
||||||
@ -536,7 +539,7 @@ impl MachineRegistryInner {
|
|||||||
// // Allocate within the subnet
|
// // Allocate within the subnet
|
||||||
// match self
|
// match self
|
||||||
// .address_pool
|
// .address_pool
|
||||||
// .add_random_subnet_v6(subnet, additional_prefix)
|
// .add_random_subnet_v6(subnet, prefix)
|
||||||
// {
|
// {
|
||||||
// Some(a) => {
|
// Some(a) => {
|
||||||
// // Got a sub-allocation
|
// // Got a sub-allocation
|
||||||
|
@ -33,6 +33,7 @@ pub enum MachineRegistryError {
|
|||||||
NetworkNotFound,
|
NetworkNotFound,
|
||||||
TemplateNotFound,
|
TemplateNotFound,
|
||||||
BlueprintNotFound,
|
BlueprintNotFound,
|
||||||
|
ModelNotFound,
|
||||||
NoAllocation,
|
NoAllocation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use ipnet::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct BlueprintStateUnlockedInner {
|
struct BlueprintStateUnlockedInner {
|
||||||
@ -9,14 +10,14 @@ struct BlueprintStateUnlockedInner {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BlueprintStateIpv4Params {
|
pub struct BlueprintStateIpv4Params {
|
||||||
pub allocation: Option<String>,
|
pub allocation: Option<String>,
|
||||||
pub additional_prefix: u8,
|
pub prefix: u8,
|
||||||
pub gateway: Option<BlueprintStateGatewayParams>,
|
pub gateway: Option<BlueprintStateGatewayParams>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct BlueprintStateIpv6Params {
|
pub struct BlueprintStateIpv6Params {
|
||||||
pub allocation: Option<String>,
|
pub allocation: Option<String>,
|
||||||
pub additional_prefix: u8,
|
pub prefix: u8,
|
||||||
pub gateway: Option<BlueprintStateGatewayParams>,
|
pub gateway: Option<BlueprintStateGatewayParams>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,7 +171,24 @@ impl BlueprintState {
|
|||||||
machine_registry_inner: &mut MachineRegistryInner,
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
network_state: NetworkState,
|
network_state: NetworkState,
|
||||||
) -> MachineRegistryResult<()> {
|
) -> MachineRegistryResult<()> {
|
||||||
xxx do generation
|
let model_name = match inner.model.clone() {
|
||||||
|
Some(models) => machine_registry_inner
|
||||||
|
.srng()
|
||||||
|
.weighted_choice(&models)
|
||||||
|
.clone(),
|
||||||
|
None => machine_registry_inner.config().default_model.clone(),
|
||||||
|
};
|
||||||
|
let Some(model) = machine_registry_inner.config().models.get(&model_name) else {
|
||||||
|
return Err(MachineRegistryError::ModelNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = NetworkStateModelParams {
|
||||||
|
latency: Some(model.latency.clone()),
|
||||||
|
distance: model.distance.clone(),
|
||||||
|
loss: model.loss,
|
||||||
|
};
|
||||||
|
network_state.set_model(params);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_ipv4_inner(
|
fn generate_ipv4_inner(
|
||||||
@ -178,7 +196,49 @@ impl BlueprintState {
|
|||||||
machine_registry_inner: &mut MachineRegistryInner,
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
network_state: NetworkState,
|
network_state: NetworkState,
|
||||||
) -> MachineRegistryResult<()> {
|
) -> MachineRegistryResult<()> {
|
||||||
//
|
network_state.clear_ipv4(machine_registry_inner);
|
||||||
|
let Some(ipv4) = inner.ipv4.as_ref() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let allocation = ipv4
|
||||||
|
.params
|
||||||
|
.allocation
|
||||||
|
.clone()
|
||||||
|
.map(|x| {
|
||||||
|
let allocation = machine_registry_inner
|
||||||
|
.config()
|
||||||
|
.allocations
|
||||||
|
.get(&x)
|
||||||
|
.ok_or(MachineRegistryError::InvalidName)?;
|
||||||
|
if let Some(subnet4) = &allocation.subnets.subnet4 {
|
||||||
|
let ipv4net = machine_registry_inner
|
||||||
|
.srng()
|
||||||
|
.weighted_choice(subnet4)
|
||||||
|
.clone();
|
||||||
|
Ok(Some(ipv4net))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
Ok(Some(
|
||||||
|
Ipv4Net::new(Ipv4Addr::UNSPECIFIED, 0).expect("must be valid"),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let Some(allocation) = allocation else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Do suballocation
|
||||||
|
xxx figure out how to do allocation inside internet vs private network
|
||||||
|
|
||||||
|
let params = NetworkStateIpv4Params { allocation };
|
||||||
|
|
||||||
|
let gateway_params = inner.
|
||||||
|
|
||||||
|
network_state.set_ipv4(, gateway_params);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_ipv6_inner(
|
fn generate_ipv6_inner(
|
||||||
|
@ -15,7 +15,7 @@ struct MachineStateInner {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MachineStateInterface {
|
pub struct MachineStateInterface {
|
||||||
/// The network this interface belongs to
|
/// The network this interface belongs to
|
||||||
pub network_id: NetworkStateId,
|
pub network_id: Option<NetworkStateId>,
|
||||||
/// The veilid NetworkInterface state
|
/// The veilid NetworkInterface state
|
||||||
pub network_interface: NetworkInterface,
|
pub network_interface: NetworkInterface,
|
||||||
}
|
}
|
||||||
@ -94,7 +94,6 @@ impl MachineState {
|
|||||||
|
|
||||||
pub fn allocate_interface(
|
pub fn allocate_interface(
|
||||||
&self,
|
&self,
|
||||||
network_id: NetworkStateId,
|
|
||||||
opt_name: Option<String>,
|
opt_name: Option<String>,
|
||||||
opt_interface_flags: Option<InterfaceFlags>,
|
opt_interface_flags: Option<InterfaceFlags>,
|
||||||
) -> MachineRegistryResult<String> {
|
) -> MachineRegistryResult<String> {
|
||||||
@ -112,7 +111,7 @@ impl MachineState {
|
|||||||
inner.interfaces.insert(
|
inner.interfaces.insert(
|
||||||
name.clone(),
|
name.clone(),
|
||||||
MachineStateInterface {
|
MachineStateInterface {
|
||||||
network_id,
|
network_id: None,
|
||||||
network_interface: NetworkInterface {
|
network_interface: NetworkInterface {
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
flags,
|
flags,
|
||||||
@ -143,9 +142,12 @@ impl MachineState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get the network state
|
// Get the network state
|
||||||
|
let Some(network_id) = intf.network_id else {
|
||||||
|
return Err(MachineRegistryError::NetworkNotFound);
|
||||||
|
};
|
||||||
let network_state = machine_registry_inner
|
let network_state = machine_registry_inner
|
||||||
.network_states()
|
.network_states()
|
||||||
.get_state(intf.network_id)?;
|
.get_state(network_id)?;
|
||||||
|
|
||||||
// Allocate interface address
|
// Allocate interface address
|
||||||
let is_dynamic = opt_address.is_none();
|
let is_dynamic = opt_address.is_none();
|
||||||
@ -180,9 +182,12 @@ impl MachineState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get the network state
|
// Get the network state
|
||||||
|
let Some(network_id) = intf.network_id else {
|
||||||
|
return Err(MachineRegistryError::NetworkNotFound);
|
||||||
|
};
|
||||||
let network_state = machine_registry_inner
|
let network_state = machine_registry_inner
|
||||||
.network_states()
|
.network_states()
|
||||||
.get_state(intf.network_id)?;
|
.get_state(network_id)?;
|
||||||
|
|
||||||
// Allocate interface address
|
// Allocate interface address
|
||||||
let is_dynamic = opt_address.is_none();
|
let is_dynamic = opt_address.is_none();
|
||||||
@ -204,6 +209,47 @@ impl MachineState {
|
|||||||
Ok(ifv6_addr)
|
Ok(ifv6_addr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn attach_network(
|
||||||
|
&self,
|
||||||
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
|
interface: &str,
|
||||||
|
network_id: NetworkStateId,
|
||||||
|
) -> MachineRegistryResult<()> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
let Some(intf) = inner.interfaces.get_mut(interface) else {
|
||||||
|
return Err(MachineRegistryError::InvalidName);
|
||||||
|
};
|
||||||
|
if intf.network_id.is_some() {
|
||||||
|
self.detach_network(machine_registry_inner, interface);
|
||||||
|
}
|
||||||
|
intf.network_id = Some(network_id);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detach_network(
|
||||||
|
&self,
|
||||||
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
|
interface: &str,
|
||||||
|
) -> MachineRegistryResult<()> {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
Self::detach_network_inner(&mut *inner, machine_registry_inner, interface)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn attached_network_interfaces(
|
||||||
|
&self,
|
||||||
|
network_id: NetworkStateId,
|
||||||
|
) -> MachineRegistryResult<Vec<String>> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let inner = self.inner.lock();
|
||||||
|
for intf in &inner.interfaces {
|
||||||
|
if intf.1.network_id == Some(network_id) {
|
||||||
|
out.push(intf.0.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn release_address(
|
pub fn release_address(
|
||||||
&self,
|
&self,
|
||||||
machine_registry_inner: &mut MachineRegistryInner,
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
@ -215,10 +261,14 @@ impl MachineState {
|
|||||||
return Err(MachineRegistryError::InvalidName);
|
return Err(MachineRegistryError::InvalidName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(network_id) = intf.network_id else {
|
||||||
|
return Err(MachineRegistryError::NetworkNotFound);
|
||||||
|
};
|
||||||
|
|
||||||
// Get the network state
|
// Get the network state
|
||||||
let network_state = machine_registry_inner
|
let network_state = machine_registry_inner
|
||||||
.network_states()
|
.network_states()
|
||||||
.get_state(intf.network_id)?;
|
.get_state(network_id)?;
|
||||||
|
|
||||||
// Release the address from the network
|
// Release the address from the network
|
||||||
match address {
|
match address {
|
||||||
@ -243,6 +293,19 @@ impl MachineState {
|
|||||||
Self::release_all_addresses_inner(&mut *inner, machine_registry_inner, interface)
|
Self::release_all_addresses_inner(&mut *inner, machine_registry_inner, interface)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn detach_network_inner(
|
||||||
|
inner: &mut MachineStateInner,
|
||||||
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
|
interface: &str,
|
||||||
|
) -> MachineRegistryResult<()> {
|
||||||
|
Self::release_all_addresses_inner(inner, machine_registry_inner, interface)?;
|
||||||
|
let Some(intf) = inner.interfaces.get_mut(interface) else {
|
||||||
|
return Err(MachineRegistryError::InvalidName);
|
||||||
|
};
|
||||||
|
intf.network_id = None;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn release_all_addresses_inner(
|
fn release_all_addresses_inner(
|
||||||
inner: &mut MachineStateInner,
|
inner: &mut MachineStateInner,
|
||||||
machine_registry_inner: &mut MachineRegistryInner,
|
machine_registry_inner: &mut MachineRegistryInner,
|
||||||
@ -252,10 +315,14 @@ impl MachineState {
|
|||||||
return Err(MachineRegistryError::InvalidName);
|
return Err(MachineRegistryError::InvalidName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let Some(network_id) = intf.network_id else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
// Get the network state
|
// Get the network state
|
||||||
let network_state = machine_registry_inner
|
let network_state = machine_registry_inner
|
||||||
.network_states()
|
.network_states()
|
||||||
.get_state(intf.network_id)?;
|
.get_state(network_id)?;
|
||||||
|
|
||||||
// Release the addresses from the network
|
// Release the addresses from the network
|
||||||
for addr in &intf.network_interface.addrs {
|
for addr in &intf.network_interface.addrs {
|
||||||
@ -277,7 +344,7 @@ impl MachineState {
|
|||||||
interface: &str,
|
interface: &str,
|
||||||
) -> MachineRegistryResult<()> {
|
) -> MachineRegistryResult<()> {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
Self::release_all_addresses_inner(&mut *inner, machine_registry_inner, interface)?;
|
Self::detach_network_inner(&mut *inner, machine_registry_inner, interface)?;
|
||||||
inner
|
inner
|
||||||
.interfaces
|
.interfaces
|
||||||
.remove(interface)
|
.remove(interface)
|
||||||
|
@ -23,17 +23,22 @@ struct NetworkStateInner {
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NetworkStateModel {
|
struct NetworkStateModel {
|
||||||
/// Network latency distribution
|
params: NetworkStateModelParams,
|
||||||
latency: Option<config::Distribution>,
|
|
||||||
/// Distance simulation metric
|
|
||||||
distance: Option<config::Distance>,
|
|
||||||
/// Packet loss probability
|
|
||||||
loss: Probability,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NetworkStateIpv4Params {
|
pub struct NetworkStateModelParams {
|
||||||
allocation: Ipv4Net,
|
/// Network latency distribution
|
||||||
|
pub latency: Option<config::Distribution>,
|
||||||
|
/// Distance simulation metric
|
||||||
|
pub distance: Option<config::Distance>,
|
||||||
|
/// Packet loss probability
|
||||||
|
pub loss: Probability,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct NetworkStateIpv4Params {
|
||||||
|
pub allocation: Ipv4Net,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -44,8 +49,8 @@ struct NetworkStateIpv4 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NetworkStateIpv6Params {
|
pub struct NetworkStateIpv6Params {
|
||||||
allocation: Ipv6Net,
|
pub allocation: Ipv6Net,
|
||||||
}
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NetworkStateIpv6 {
|
struct NetworkStateIpv6 {
|
||||||
@ -55,10 +60,10 @@ struct NetworkStateIpv6 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct NetworkStateGatewayParams {
|
pub struct NetworkStateGatewayParams {
|
||||||
translation: config::Translation,
|
pub translation: config::Translation,
|
||||||
upnp: bool,
|
pub upnp: bool,
|
||||||
network: Option<NetworkStateId>,
|
pub network: Option<NetworkStateId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -88,10 +93,12 @@ impl NetworkState {
|
|||||||
inner: Arc::new(Mutex::new(NetworkStateInner {
|
inner: Arc::new(Mutex::new(NetworkStateInner {
|
||||||
generating_blueprint: None,
|
generating_blueprint: None,
|
||||||
model: NetworkStateModel {
|
model: NetworkStateModel {
|
||||||
|
params: NetworkStateModelParams {
|
||||||
latency: None,
|
latency: None,
|
||||||
distance: None,
|
distance: None,
|
||||||
loss: 0.0,
|
loss: 0.0,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
ipv4: None,
|
ipv4: None,
|
||||||
ipv6: None,
|
ipv6: None,
|
||||||
})),
|
})),
|
||||||
@ -109,9 +116,38 @@ impl NetworkState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_model(&self, model: NetworkStateModel) {
|
pub fn set_model(&self, params: NetworkStateModelParams) {
|
||||||
let mut inner = self.inner.lock();
|
let mut inner = self.inner.lock();
|
||||||
inner.model = model;
|
inner.model = NetworkStateModel { params };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_ipv4(&self, machine_registry_inner: &mut MachineRegistryInner) {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
let Some(ipv4) = inner.ipv4.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut address_machines = HashMap::<MachineStateId, Vec<Ipv4Addr>>::new();
|
||||||
|
for (k, v) in &ipv4.machine_addresses {
|
||||||
|
address_machines.entry(*v).or_default().push(*k);
|
||||||
|
}
|
||||||
|
for (machine_id, addresses) in address_machines {
|
||||||
|
let machine_state = machine_registry_inner
|
||||||
|
.machine_states()
|
||||||
|
.get_state(machine_id)
|
||||||
|
.expect("must exist");
|
||||||
|
|
||||||
|
let interfaces = machine_state
|
||||||
|
.attached_network_interfaces(self.id())
|
||||||
|
.expect("must exist");
|
||||||
|
for address in addresses {
|
||||||
|
for interface in &interfaces {
|
||||||
|
machine_state
|
||||||
|
.release_address(machine_registry_inner, interface, IpAddr::V4(address))
|
||||||
|
.expect("must succeed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_ipv4(
|
pub fn set_ipv4(
|
||||||
@ -145,6 +181,35 @@ impl NetworkState {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear_ipv6(&self, machine_registry_inner: &mut MachineRegistryInner) {
|
||||||
|
let mut inner = self.inner.lock();
|
||||||
|
let Some(ipv6) = inner.ipv6.as_mut() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut address_machines = HashMap::<MachineStateId, Vec<Ipv6Addr>>::new();
|
||||||
|
for (k, v) in &ipv6.machine_addresses {
|
||||||
|
address_machines.entry(*v).or_default().push(*k);
|
||||||
|
}
|
||||||
|
for (machine_id, addresses) in address_machines {
|
||||||
|
let machine_state = machine_registry_inner
|
||||||
|
.machine_states()
|
||||||
|
.get_state(machine_id)
|
||||||
|
.expect("must exist");
|
||||||
|
|
||||||
|
let interfaces = machine_state
|
||||||
|
.attached_network_interfaces(self.id())
|
||||||
|
.expect("must exist");
|
||||||
|
for address in addresses {
|
||||||
|
for interface in &interfaces {
|
||||||
|
machine_state
|
||||||
|
.release_address(machine_registry_inner, interface, IpAddr::V6(address))
|
||||||
|
.expect("must succeed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn set_ipv6(
|
pub fn set_ipv6(
|
||||||
&self,
|
&self,
|
||||||
params: NetworkStateIpv6Params,
|
params: NetworkStateIpv6Params,
|
||||||
|
@ -289,7 +289,8 @@ impl TemplateState {
|
|||||||
machine_state.set_bootstrap(false);
|
machine_state.set_bootstrap(false);
|
||||||
|
|
||||||
// Make the default route interface
|
// Make the default route interface
|
||||||
let vin0 = machine_state.allocate_interface(network_state.id(), None, None)?;
|
let vin0 = machine_state.allocate_interface(None, None)?;
|
||||||
|
machine_state.attach_network(machine_registry_inner, &vin0, network_state.id())?;
|
||||||
if network_state.is_ipv4() {
|
if network_state.is_ipv4() {
|
||||||
machine_state.allocate_address_ipv4(machine_registry_inner, &vin0, None, None)?;
|
machine_state.allocate_address_ipv4(machine_registry_inner, &vin0, None, None)?;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user