Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,14 @@ impl IpRange {
IpRange::V6(ip6) => IpRangeIter::V6(ip6.iter()),
}
}

// Has to be u128 to accommodate IPv6
pub fn len(&self) -> u128 {
match self {
IpRange::V4(ip4) => u128::from(ip4.len()),
IpRange::V6(ip6) => ip6.len(),
}
}
}

impl From<IpAddr> for IpRange {
Expand Down Expand Up @@ -508,6 +516,12 @@ impl Ipv4Range {
pub fn iter(&self) -> Ipv4RangeIter {
Ipv4RangeIter { next: Some(self.first.into()), last: self.last.into() }
}

pub fn len(&self) -> u32 {
let start_num = u32::from(self.first);
let end_num = u32::from(self.last);
end_num - start_num + 1
}
}

impl From<Ipv4Addr> for Ipv4Range {
Expand Down Expand Up @@ -564,6 +578,12 @@ impl Ipv6Range {
pub fn iter(&self) -> Ipv6RangeIter {
Ipv6RangeIter { next: Some(self.first.into()), last: self.last.into() }
}

pub fn len(&self) -> u128 {
let start_num = u128::from(self.first);
let end_num = u128::from(self.last);
end_num - start_num + 1
}
}

impl From<Ipv6Addr> for Ipv6Range {
Expand Down Expand Up @@ -781,6 +801,19 @@ mod test {
);
}

#[test]
fn test_ip_range_length() {
let lo = Ipv4Addr::new(10, 0, 0, 1);
let hi = Ipv4Addr::new(10, 0, 0, 3);
let range = IpRange::try_from((lo, hi)).unwrap();
assert_eq!(range.len(), 3);

let lo = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 1);
let hi = Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 1, 3);
let range = IpRange::try_from((lo, hi)).unwrap();
assert_eq!(range.len(), 2u128.pow(16) + 3);
}

#[test]
fn test_ipv6_subnet_deserialize() {
let value = json!({
Expand Down
37 changes: 37 additions & 0 deletions nexus/db-model/src/utilization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,40 @@ impl From<SiloUtilization> for views::Utilization {
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ipv4Utilization {
pub allocated: u32,
pub capacity: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ipv6Utilization {
pub allocated: u128,
pub capacity: u128,
}

// Not really a DB model, just the result of a datastore function
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpPoolUtilization {
pub ipv4: Ipv4Utilization,
pub ipv6: Ipv6Utilization,
}

impl From<Ipv4Utilization> for views::Ipv4Utilization {
fn from(util: Ipv4Utilization) -> Self {
Self { allocated: util.allocated, capacity: util.capacity }
}
}

impl From<Ipv6Utilization> for views::Ipv6Utilization {
fn from(util: Ipv6Utilization) -> Self {
Self { allocated: util.allocated, capacity: util.capacity }
}
}

impl From<IpPoolUtilization> for views::IpPoolUtilization {
fn from(util: IpPoolUtilization) -> Self {
Self { ipv4: util.ipv4.into(), ipv6: util.ipv6.into() }
}
}
241 changes: 240 additions & 1 deletion nexus/db-queries/src/db/datastore/ip_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ use omicron_common::api::external::UpdateResult;
use ref_cast::RefCast;
use uuid::Uuid;

pub struct IpsAllocated {
pub ipv4: i64,
pub ipv6: i64,
}

pub struct IpsCapacity {
pub ipv4: u32,
pub ipv6: u128,
}

impl DataStore {
/// List IP Pools
pub async fn ip_pools_list(
Expand Down Expand Up @@ -324,6 +334,80 @@ impl DataStore {
})
}

pub async fn ip_pool_allocated_count(
&self,
opctx: &OpContext,
authz_pool: &authz::IpPool,
) -> Result<IpsAllocated, Error> {
opctx.authorize(authz::Action::Read, authz_pool).await?;

use db::schema::external_ip;
use diesel::dsl::sql;
use diesel::sql_types::BigInt;

let (ipv4, ipv6) = external_ip::table
.filter(external_ip::ip_pool_id.eq(authz_pool.id()))
.filter(external_ip::time_deleted.is_null())
.select((
sql::<BigInt>("count(*) FILTER (WHERE family(ip) = 4)"),
sql::<BigInt>("count(*) FILTER (WHERE family(ip) = 6)"),
))
.first_async::<(i64, i64)>(
&*self.pool_connection_authorized(opctx).await?,
)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;

Ok(IpsAllocated { ipv4, ipv6 })
}

pub async fn ip_pool_total_capacity(
&self,
opctx: &OpContext,
authz_pool: &authz::IpPool,
) -> Result<IpsCapacity, Error> {
opctx.authorize(authz::Action::Read, authz_pool).await?;
opctx.authorize(authz::Action::ListChildren, authz_pool).await?;

use db::schema::ip_pool_range;

let ranges = ip_pool_range::table
.filter(ip_pool_range::ip_pool_id.eq(authz_pool.id()))
.filter(ip_pool_range::time_deleted.is_null())
.select(IpPoolRange::as_select())
// This is a rare unpaginated DB query, which means we are
// vulnerable to a resource exhaustion attack in which someone
// creates a very large number of ranges in order to make this
// query slow. In order to mitigate that, we limit the query to the
// (current) max allowed page size, effectively making this query
// exactly as vulnerable as if it were paginated. If there are more
// than 10,000 ranges in a pool, we will undercount, but I have a
// hard time seeing that as a practical problem.
.limit(10000)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In private chat we discussed the alternative of doing this calculation in SQL so we don't have to limit it here. What we found is that neither CRDB nor Postgres support subtraction of IPv6 addresses when the result is greater than a 64 bit integer, and we couldn't figure out how to convert the addresses to 128 bit integers first and then subtract. So it seems we are forced to do this in Rust.

> select 'ffff::'::inet - '::1';
ERROR: result out of range

https://github.com/cockroachdb/cockroach/blob/91c1d5cbb5a56cd9c082d78814cd03c5471dddfa/pkg/util/ipaddr/ipaddr_test.go#L862-L881

https://github.com/cockroachdb/cockroach/blob/4deb9e3b77f38396a58bb8186fe51bb01cf9f007/pkg/util/ipaddr/ipaddr.go#L607-L632

.get_results_async::<IpPoolRange>(
&*self.pool_connection_authorized(opctx).await?,
)
.await
.map_err(|e| {
public_error_from_diesel(
e,
ErrorHandler::NotFoundByResource(authz_pool),
)
})?;

let mut ipv4: u32 = 0;
let mut ipv6: u128 = 0;

for range in &ranges {
let r = IpRange::from(range);
match r {
IpRange::V4(r) => ipv4 += r.len(),
IpRange::V6(r) => ipv6 += r.len(),
}
}
Ok(IpsCapacity { ipv4, ipv6 })
}

pub async fn ip_pool_silo_list(
&self,
opctx: &OpContext,
Expand Down Expand Up @@ -846,10 +930,14 @@ mod test {

use crate::authz;
use crate::db::datastore::test_utils::datastore_test;
use crate::db::model::{IpPool, IpPoolResource, IpPoolResourceType};
use crate::db::model::{
IpPool, IpPoolResource, IpPoolResourceType, Project,
};
use assert_matches::assert_matches;
use nexus_test_utils::db::test_setup_database;
use nexus_types::external_api::params;
use nexus_types::identity::Resource;
use omicron_common::address::{IpRange, Ipv4Range, Ipv6Range};
use omicron_common::api::external::http_pagination::PaginatedBy;
use omicron_common::api::external::{
DataPageParams, Error, IdentityMetadataCreateParams, LookupType,
Expand Down Expand Up @@ -1075,4 +1163,155 @@ mod test {
db.cleanup().await.unwrap();
logctx.cleanup_successful();
}

#[tokio::test]
async fn test_ip_pool_utilization() {
let logctx = dev::test_setup_log("test_ip_utilization");
let mut db = test_setup_database(&logctx.log).await;
let (opctx, datastore) = datastore_test(&logctx, &db).await;

let authz_silo = opctx.authn.silo_required().unwrap();
let project = Project::new(
authz_silo.id(),
params::ProjectCreate {
identity: IdentityMetadataCreateParams {
name: "my-project".parse().unwrap(),
description: "".to_string(),
},
},
);
let (.., project) =
datastore.project_create(&opctx, project).await.unwrap();

// create an IP pool for the silo, add a range to it, and link it to the silo
let identity = IdentityMetadataCreateParams {
name: "my-pool".parse().unwrap(),
description: "".to_string(),
};
let pool = datastore
.ip_pool_create(&opctx, IpPool::new(&identity))
.await
.expect("Failed to create IP pool");
let authz_pool = authz::IpPool::new(
authz::FLEET,
pool.id(),
LookupType::ById(pool.id()),
);

// capacity of zero because there are no ranges
let max_ips = datastore
.ip_pool_total_capacity(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(max_ips.ipv4, 0);
assert_eq!(max_ips.ipv6, 0);

let range = IpRange::V4(
Ipv4Range::new(
std::net::Ipv4Addr::new(10, 0, 0, 1),
std::net::Ipv4Addr::new(10, 0, 0, 5),
)
.unwrap(),
);
datastore
.ip_pool_add_range(&opctx, &authz_pool, &range)
.await
.expect("Could not add range");

// now it has a capacity of 5 because we added the range
let max_ips = datastore
.ip_pool_total_capacity(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(max_ips.ipv4, 5);
assert_eq!(max_ips.ipv6, 0);

let link = IpPoolResource {
ip_pool_id: pool.id(),
resource_type: IpPoolResourceType::Silo,
resource_id: authz_silo.id(),
is_default: true,
};
datastore
.ip_pool_link_silo(&opctx, link)
.await
.expect("Could not link pool to silo");

let ip_count = datastore
.ip_pool_allocated_count(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(ip_count.ipv4, 0);
assert_eq!(ip_count.ipv6, 0);

let identity = IdentityMetadataCreateParams {
name: "my-ip".parse().unwrap(),
description: "".to_string(),
};
let ip = datastore
.allocate_floating_ip(&opctx, project.id(), identity, None, None)
.await
.expect("Could not allocate floating IP");
assert_eq!(ip.ip.to_string(), "10.0.0.1/32");

let ip_count = datastore
.ip_pool_allocated_count(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(ip_count.ipv4, 1);
assert_eq!(ip_count.ipv6, 0);

// allocating one has nothing to do with total capacity
let max_ips = datastore
.ip_pool_total_capacity(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(max_ips.ipv4, 5);
assert_eq!(max_ips.ipv6, 0);

let ipv6_range = IpRange::V6(
Ipv6Range::new(
std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 10),
std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 1, 20),
)
.unwrap(),
);
datastore
.ip_pool_add_range(&opctx, &authz_pool, &ipv6_range)
.await
.expect("Could not add range");

// now test with additional v6 range
let max_ips = datastore
.ip_pool_total_capacity(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(max_ips.ipv4, 5);
assert_eq!(max_ips.ipv6, 11 + 65536);

// add a giant range for fun
let ipv6_range = IpRange::V6(
Ipv6Range::new(
std::net::Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 1, 21),
std::net::Ipv6Addr::new(
0xfd00, 0, 0, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff,
),
)
.unwrap(),
);
datastore
.ip_pool_add_range(&opctx, &authz_pool, &ipv6_range)
.await
.expect("Could not add range");

let max_ips = datastore
.ip_pool_total_capacity(&opctx, &authz_pool)
.await
.unwrap();
assert_eq!(max_ips.ipv4, 5);
assert_eq!(max_ips.ipv6, 1208925819614629174706166);

db.cleanup().await.unwrap();
logctx.cleanup_successful();
}
}
Loading