Skip to content

Commit 926bdd2

Browse files
authored
Support IPv6 VPC-private addresses for network interfaces (#9269)
- Make more explicit how we assign IP addresses for NICs, by adding a full configuration type. That includes details of whether an address is auto assigned or explicitly requested, and the corresponding transit IPs. Also adds the notion of single-stack IPv4 or IPv6 NICs, or explicitly dual-stack. - Restructure some of the internals of the network interface `InsertQuery` to more clearly generate SQL based on the assignment preferences - Add `NextIpv6Address` type (needs tests) - Rework callers of `IncompleteNetworkInterface` to use new assignment-request enum
1 parent af3b89f commit 926bdd2

File tree

9 files changed

+732
-199
lines changed

9 files changed

+732
-199
lines changed

nexus/db-model/src/network_interface.rs

Lines changed: 196 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ use nexus_db_schema::schema::service_network_interface;
1919
use nexus_sled_agent_shared::inventory::ZoneKind;
2020
use nexus_types::external_api::params;
2121
use nexus_types::identity::Resource;
22-
use omicron_common::api::external::Error;
2322
use omicron_common::api::{external, internal};
2423
use omicron_uuid_kinds::GenericUuid;
2524
use omicron_uuid_kinds::InstanceUuid;
2625
use omicron_uuid_kinds::OmicronZoneUuid;
2726
use omicron_uuid_kinds::VnicUuid;
27+
use oxnet::IpNet;
28+
use oxnet::Ipv4Net;
29+
use oxnet::Ipv6Net;
30+
use std::net::IpAddr;
2831
use uuid::Uuid;
2932

3033
/// The max number of interfaces that may be associated with a resource,
@@ -346,6 +349,179 @@ impl From<ServiceNetworkInterface> for NetworkInterface {
346349
}
347350
}
348351

352+
mod private {
353+
pub trait IpSealed: Clone + Copy + std::fmt::Debug {
354+
fn into_ipnet(self) -> ipnetwork::IpNetwork;
355+
}
356+
357+
impl IpSealed for std::net::Ipv4Addr {
358+
fn into_ipnet(self) -> ipnetwork::IpNetwork {
359+
ipnetwork::IpNetwork::V4(ipnetwork::Ipv4Network::from(self))
360+
}
361+
}
362+
impl IpSealed for std::net::Ipv6Addr {
363+
fn into_ipnet(self) -> ipnetwork::IpNetwork {
364+
ipnetwork::IpNetwork::V6(ipnetwork::Ipv6Network::from(self))
365+
}
366+
}
367+
}
368+
369+
pub trait Ip: private::IpSealed {}
370+
impl<T> Ip for T where T: private::IpSealed {}
371+
372+
/// How an IP address is assigned to an interface.
373+
#[derive(Clone, Copy, Debug, Default)]
374+
pub enum IpAssignment<T: Ip> {
375+
/// Automatically assign an IP address.
376+
#[default]
377+
Auto,
378+
/// Explicitly assign a specific address, if available.
379+
Explicit(T),
380+
}
381+
382+
/// How to assign an IPv4 address.
383+
pub type Ipv4Assignment = IpAssignment<std::net::Ipv4Addr>;
384+
385+
/// How to assign an IPv6 address.
386+
pub type Ipv6Assignment = IpAssignment<std::net::Ipv6Addr>;
387+
388+
/// Configuration for a network interface's IPv4 addressing.
389+
#[derive(Clone, Debug, Default)]
390+
pub struct Ipv4Config {
391+
/// The VPC-private address to assign to the interface.
392+
pub ip: Ipv4Assignment,
393+
/// Additional IP networks the interface can send / receive on.
394+
pub transit_ips: Vec<Ipv4Net>,
395+
}
396+
397+
/// Configuration for a network interface's IPv6 addressing.
398+
#[derive(Clone, Debug, Default)]
399+
pub struct Ipv6Config {
400+
/// The VPC-private address to assign to the interface.
401+
pub ip: Ipv6Assignment,
402+
/// Additional IP networks the interface can send / receive on.
403+
pub transit_ips: Vec<Ipv6Net>,
404+
}
405+
406+
/// Configuration for a network interface's IP addressing.
407+
#[derive(Clone, Debug)]
408+
pub enum IpConfig {
409+
/// The interface has only an IPv4 stack.
410+
V4(Ipv4Config),
411+
/// The interface has only an IPv6 stack.
412+
V6(Ipv6Config),
413+
/// The interface has both an IPv4 and IPv6 stack.
414+
DualStack { v4: Ipv4Config, v6: Ipv6Config },
415+
}
416+
417+
impl IpConfig {
418+
/// Construct an IPv4 configuration with no transit IPs.
419+
pub fn from_ipv4(addr: std::net::Ipv4Addr) -> Self {
420+
IpConfig::V4(Ipv4Config {
421+
ip: Ipv4Assignment::Explicit(addr),
422+
transit_ips: vec![],
423+
})
424+
}
425+
426+
/// Construct an IP configuration with only an automatic IPv4 address.
427+
pub fn auto_ipv4() -> Self {
428+
IpConfig::V4(Ipv4Config::default())
429+
}
430+
431+
/// Return the IPv4 address assignment.
432+
pub fn ipv4_assignment(&self) -> Option<&Ipv4Assignment> {
433+
match self {
434+
IpConfig::V4(Ipv4Config { ip, .. }) => Some(ip),
435+
IpConfig::V6(_) => None,
436+
IpConfig::DualStack { v4: Ipv4Config { ip, .. }, .. } => Some(ip),
437+
}
438+
}
439+
440+
/// Return the IPv4 address explicitly requested, if one exists.
441+
pub fn ipv4_addr(&self) -> Option<&std::net::Ipv4Addr> {
442+
self.ipv4_assignment().and_then(|assignment| match assignment {
443+
IpAssignment::Auto => None,
444+
IpAssignment::Explicit(addr) => Some(addr),
445+
})
446+
}
447+
448+
/// Construct an IPv6 configuration with no transit IPs.
449+
pub fn from_ipv6(addr: std::net::Ipv6Addr) -> Self {
450+
IpConfig::V6(Ipv6Config {
451+
ip: Ipv6Assignment::Explicit(addr),
452+
transit_ips: vec![],
453+
})
454+
}
455+
456+
/// Construct an IP configuration with only an automatic IPv6 address.
457+
pub fn auto_ipv6() -> Self {
458+
IpConfig::V6(Ipv6Config::default())
459+
}
460+
461+
/// Return the IPv6 address assignment.
462+
pub fn ipv6_assignment(&self) -> Option<&Ipv6Assignment> {
463+
match self {
464+
IpConfig::V6(Ipv6Config { ip, .. }) => Some(ip),
465+
IpConfig::V4(_) => None,
466+
IpConfig::DualStack { v6: Ipv6Config { ip, .. }, .. } => Some(ip),
467+
}
468+
}
469+
470+
/// Return the IPv6 address explicitly requested, if one exists.
471+
pub fn ipv6_addr(&self) -> Option<&std::net::Ipv6Addr> {
472+
self.ipv6_assignment().and_then(|assignment| match assignment {
473+
IpAssignment::Auto => None,
474+
IpAssignment::Explicit(addr) => Some(addr),
475+
})
476+
}
477+
478+
/// Return the transit IPs requested in this configuration.
479+
pub fn transit_ips(&self) -> Vec<IpNet> {
480+
match self {
481+
IpConfig::V4(Ipv4Config { transit_ips, .. }) => {
482+
transit_ips.iter().copied().map(Into::into).collect()
483+
}
484+
IpConfig::V6(Ipv6Config { transit_ips, .. }) => {
485+
transit_ips.iter().copied().map(Into::into).collect()
486+
}
487+
IpConfig::DualStack {
488+
v4: Ipv4Config { transit_ips: ipv4_addrs, .. },
489+
v6: Ipv6Config { transit_ips: ipv6_addrs, .. },
490+
} => ipv4_addrs
491+
.iter()
492+
.copied()
493+
.map(Into::into)
494+
.chain(ipv6_addrs.iter().copied().map(Into::into))
495+
.collect(),
496+
}
497+
}
498+
499+
/// Construct an IP configuration with both IPv4 / IPv6 addresses and no
500+
/// transit IPs.
501+
pub fn auto_dual_stack() -> Self {
502+
IpConfig::DualStack {
503+
v4: Ipv4Config::default(),
504+
v6: Ipv6Config::default(),
505+
}
506+
}
507+
508+
/// Return true if this config has any transit IPs
509+
fn has_transit_ips(&self) -> bool {
510+
match self {
511+
IpConfig::V4(Ipv4Config { transit_ips, .. }) => {
512+
!transit_ips.is_empty()
513+
}
514+
IpConfig::V6(Ipv6Config { transit_ips, .. }) => {
515+
!transit_ips.is_empty()
516+
}
517+
IpConfig::DualStack {
518+
v4: Ipv4Config { transit_ips: ipv4_addrs, .. },
519+
v6: Ipv6Config { transit_ips: ipv6_addrs, .. },
520+
} => !ipv4_addrs.is_empty() || !ipv6_addrs.is_empty(),
521+
}
522+
}
523+
}
524+
349525
/// A not fully constructed NetworkInterface. It may not yet have an IP
350526
/// address allocated.
351527
#[derive(Clone, Debug)]
@@ -354,10 +530,9 @@ pub struct IncompleteNetworkInterface {
354530
pub kind: NetworkInterfaceKind,
355531
pub parent_id: Uuid,
356532
pub subnet: VpcSubnet,
357-
pub ip: Option<std::net::IpAddr>,
533+
pub ip_config: IpConfig,
358534
pub mac: Option<external::MacAddr>,
359535
pub slot: Option<u8>,
360-
pub transit_ips: Vec<IpNetwork>,
361536
}
362537

363538
impl IncompleteNetworkInterface {
@@ -368,20 +543,15 @@ impl IncompleteNetworkInterface {
368543
parent_id: Uuid,
369544
subnet: VpcSubnet,
370545
identity: external::IdentityMetadataCreateParams,
371-
ip: Option<std::net::IpAddr>,
546+
ip_config: IpConfig,
372547
mac: Option<external::MacAddr>,
373548
slot: Option<u8>,
374-
transit_ips: Vec<IpNetwork>,
375549
) -> Result<Self, external::Error> {
376-
if let Some(ip) = ip {
377-
// TODO-completeness:
378-
// https://github.com/oxidecomputer/omicron/issues/9244.
379-
if ip.is_ipv6() {
380-
return Err(Error::invalid_request(
381-
"IPv6 addresses are not yet supported",
382-
));
383-
}
384-
subnet.check_requestable_addr(ip)?;
550+
if let Some(ip) = ip_config.ipv4_addr() {
551+
subnet.check_requestable_addr(IpAddr::V4(*ip))?;
552+
};
553+
if let Some(ip) = ip_config.ipv6_addr() {
554+
subnet.check_requestable_addr(IpAddr::V6(*ip))?;
385555
};
386556
if let Some(mac) = mac {
387557
match kind {
@@ -422,10 +592,9 @@ impl IncompleteNetworkInterface {
422592
kind,
423593
parent_id,
424594
subnet,
425-
ip,
595+
ip_config,
426596
mac,
427597
slot,
428-
transit_ips,
429598
})
430599
}
431600

@@ -434,19 +603,17 @@ impl IncompleteNetworkInterface {
434603
instance_id: InstanceUuid,
435604
subnet: VpcSubnet,
436605
identity: external::IdentityMetadataCreateParams,
437-
ip: Option<std::net::IpAddr>,
438-
transit_ips: Vec<IpNetwork>,
606+
ip_config: IpConfig,
439607
) -> Result<Self, external::Error> {
440608
Self::new(
441609
interface_id,
442610
NetworkInterfaceKind::Instance,
443611
instance_id.into_untyped_uuid(),
444612
subnet,
445613
identity,
446-
ip,
614+
ip_config,
447615
None,
448616
None,
449-
transit_ips,
450617
)
451618
}
452619

@@ -455,20 +622,24 @@ impl IncompleteNetworkInterface {
455622
service_id: Uuid,
456623
subnet: VpcSubnet,
457624
identity: external::IdentityMetadataCreateParams,
458-
ip: std::net::IpAddr,
625+
ip_config: IpConfig,
459626
mac: external::MacAddr,
460627
slot: u8,
461628
) -> Result<Self, external::Error> {
629+
if ip_config.has_transit_ips() {
630+
return Err(external::Error::invalid_request(
631+
"Cannot specify transit IPs for service NICs",
632+
));
633+
}
462634
Self::new(
463635
interface_id,
464636
NetworkInterfaceKind::Service,
465637
service_id,
466638
subnet,
467639
identity,
468-
Some(ip),
640+
ip_config,
469641
Some(mac),
470642
Some(slot),
471-
vec![], // Service interfaces don't use transit_ips
472643
)
473644
}
474645

@@ -477,7 +648,7 @@ impl IncompleteNetworkInterface {
477648
probe_id: Uuid,
478649
subnet: VpcSubnet,
479650
identity: external::IdentityMetadataCreateParams,
480-
ip: Option<std::net::IpAddr>,
651+
ip_config: IpConfig,
481652
mac: Option<external::MacAddr>,
482653
) -> Result<Self, external::Error> {
483654
Self::new(
@@ -486,10 +657,9 @@ impl IncompleteNetworkInterface {
486657
probe_id,
487658
subnet,
488659
identity,
489-
ip,
660+
ip_config,
490661
mac,
491662
None,
492-
vec![], // Probe interfaces don't use transit_ips
493663
)
494664
}
495665
}

nexus/db-queries/src/db/datastore/deployment/external_networking.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::db::fixed_data::vpc_subnet::NEXUS_VPC_SUBNET;
1212
use crate::db::fixed_data::vpc_subnet::NTP_VPC_SUBNET;
1313
use nexus_db_lookup::DbConnection;
1414
use nexus_db_model::IncompleteNetworkInterface;
15+
use nexus_db_model::IpConfig;
1516
use nexus_db_model::IpPool;
1617
use nexus_sled_agent_shared::inventory::ZoneKind;
1718
use nexus_types::deployment::BlueprintZoneConfig;
@@ -384,6 +385,16 @@ impl DataStore {
384385
if self.is_nic_already_allocated(conn, service_id, nic, log).await? {
385386
return Ok(());
386387
}
388+
389+
// TODO-completeness: Handle dual-stack `shared::NetworkInterface`s.
390+
// See https://github.com/oxidecomputer/omicron/issues/9246.
391+
let std::net::IpAddr::V4(ip) = nic.ip else {
392+
return Err(Error::internal_error(&format!(
393+
"Unexpectedly found a service NIC without an IPv4 \
394+
address, nic_id=\"{}\"",
395+
nic.id,
396+
)));
397+
};
387398
let nic_arg = IncompleteNetworkInterface::new_service(
388399
nic.id,
389400
service_id.into_untyped_uuid(),
@@ -392,7 +403,7 @@ impl DataStore {
392403
name: nic.name.clone(),
393404
description: format!("{} service vNIC", zone_kind.report_str()),
394405
},
395-
nic.ip,
406+
IpConfig::from_ipv4(ip),
396407
nic.mac,
397408
nic.slot,
398409
)?;

nexus/db-queries/src/db/datastore/network_interface.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,7 @@ mod tests {
943943
use crate::db::pub_test_utils::TestDatabase;
944944
use nexus_config::NUM_INITIAL_RESERVED_IP_ADDRESSES;
945945
use nexus_db_fixed_data::vpc_subnet::NEXUS_VPC_SUBNET;
946+
use nexus_db_model::IpConfig;
946947
use omicron_common::address::NEXUS_OPTE_IPV4_SUBNET;
947948
use omicron_test_utils::dev;
948949
use std::collections::BTreeSet;
@@ -994,7 +995,7 @@ mod tests {
994995
name: name.parse().unwrap(),
995996
description: name,
996997
},
997-
ip.into(),
998+
IpConfig::from_ipv4(ip),
998999
macs.next().unwrap(),
9991000
0,
10001001
)

nexus/db-queries/src/db/datastore/probe.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use nexus_db_errors::ErrorHandler;
1515
use nexus_db_errors::public_error_from_diesel;
1616
use nexus_db_lookup::LookupPath;
1717
use nexus_db_model::IncompleteNetworkInterface;
18+
use nexus_db_model::IpConfig;
1819
use nexus_db_model::Probe;
1920
use nexus_db_model::VpcSubnet;
2021
use nexus_db_model::to_db_typed_uuid;
@@ -294,7 +295,7 @@ impl super::DataStore {
294295
probe.name(),
295296
),
296297
},
297-
None, //Request IP address assignment
298+
IpConfig::auto_ipv4(),
298299
None, //Request MAC address assignment
299300
)?;
300301

0 commit comments

Comments
 (0)