Skip to content

Commit b276b93

Browse files
committed
Support dual-stack NICs in the public API
- Move IP traits and config types to common modules so they can be used in the public API - Add private IP stack types to external API, used when fetching NICs - Add new `IpConfig` type to the parameters for creating NICs. This gives a more flexible scheme for selecting an IP address automatically or explicitly, and independently for each IP stack. - Add a new version of the external API for the new types, and implement conversions from the previous - Ensure external IPs are allocated for NICs with a corresponding private IP stack, checking at both IP creation and attachment.
1 parent 06c0808 commit b276b93

File tree

77 files changed

+32645
-826
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+32645
-826
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/address.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,51 @@ impl Iterator for IpRangeIter {
878878
}
879879
}
880880

881+
/// Trait for any IP address type.
882+
pub trait Ip:
883+
Clone
884+
+ Copy
885+
+ std::fmt::Debug
886+
+ Diffable
887+
+ Eq
888+
+ JsonSchema
889+
+ std::hash::Hash
890+
+ PartialOrd
891+
+ PartialEq
892+
+ Ord
893+
+ Serialize
894+
{
895+
}
896+
impl Ip for Ipv4Addr {}
897+
impl Ip for Ipv6Addr {}
898+
impl Ip for IpAddr {}
899+
900+
/// An IP address of a specific version, IPv4 or IPv6.
901+
pub trait ConcreteIp: Ip {
902+
fn into_ipaddr(self) -> IpAddr;
903+
fn into_ipnet(self) -> ipnetwork::IpNetwork;
904+
}
905+
906+
impl ConcreteIp for Ipv4Addr {
907+
fn into_ipaddr(self) -> IpAddr {
908+
IpAddr::V4(self)
909+
}
910+
911+
fn into_ipnet(self) -> ipnetwork::IpNetwork {
912+
ipnetwork::IpNetwork::V4(ipnetwork::Ipv4Network::from(self))
913+
}
914+
}
915+
916+
impl ConcreteIp for Ipv6Addr {
917+
fn into_ipaddr(self) -> IpAddr {
918+
IpAddr::V6(self)
919+
}
920+
921+
fn into_ipnet(self) -> ipnetwork::IpNetwork {
922+
ipnetwork::IpNetwork::V6(ipnetwork::Ipv6Network::from(self))
923+
}
924+
}
925+
881926
#[cfg(test)]
882927
mod test {
883928
use serde_json::json;

common/src/api/external/mod.rs

Lines changed: 109 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ use omicron_uuid_kinds::InstanceUuid;
2727
use omicron_uuid_kinds::SledUuid;
2828
use oxnet::IpNet;
2929
use oxnet::Ipv4Net;
30+
use oxnet::Ipv6Net;
3031
use parse_display::Display;
3132
use parse_display::FromStr;
3233
use rand::Rng;
@@ -43,6 +44,7 @@ use std::fmt::Formatter;
4344
use std::fmt::Result as FormatResult;
4445
use std::net::IpAddr;
4546
use std::net::Ipv4Addr;
47+
use std::net::Ipv6Addr;
4648
use std::num::ParseIntError;
4749
use std::num::{NonZeroU16, NonZeroU32};
4850
use std::ops::Deref;
@@ -2608,18 +2610,118 @@ pub struct InstanceNetworkInterface {
26082610
/// The MAC address assigned to this interface.
26092611
pub mac: MacAddr,
26102612

2611-
/// The IP address assigned to this interface.
2612-
// TODO-correctness: We need to split this into an optional V4 and optional
2613-
// V6 address, at least one of which must be specified.
2614-
pub ip: IpAddr,
26152613
/// True if this interface is the primary for the instance to which it's
26162614
/// attached.
26172615
pub primary: bool,
26182616

2619-
/// A set of additional networks that this interface may send and
2617+
/// The VPC-private IP stack for this interface.
2618+
pub ip_stack: PrivateIpStack,
2619+
}
2620+
2621+
/// The VPC-private IP stack for a network interface.
2622+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
2623+
#[serde(rename_all = "snake_case", tag = "type", content = "value")]
2624+
pub enum PrivateIpStack {
2625+
/// The interface has only an IPv4 stack.
2626+
V4(PrivateIpv4Stack),
2627+
/// The interface has only an IPv6 stack.
2628+
V6(PrivateIpv6Stack),
2629+
/// The interface is dual-stack IPv4 and IPv6.
2630+
DualStack { v4: PrivateIpv4Stack, v6: PrivateIpv6Stack },
2631+
}
2632+
2633+
impl PrivateIpStack {
2634+
/// Return the IPv4 stack, if it exists.
2635+
pub fn ipv4_stack(&self) -> Option<&PrivateIpv4Stack> {
2636+
match self {
2637+
PrivateIpStack::V4(v4) | PrivateIpStack::DualStack { v4, .. } => {
2638+
Some(v4)
2639+
}
2640+
PrivateIpStack::V6(_) => None,
2641+
}
2642+
}
2643+
2644+
/// Return the VPC-private IPv4 address, if it exists.
2645+
pub fn ipv4_addr(&self) -> Option<&Ipv4Addr> {
2646+
self.ipv4_stack().map(|s| &s.ip)
2647+
}
2648+
2649+
/// Return the IPv6 stack, if it exists.
2650+
pub fn ipv6_stack(&self) -> Option<&PrivateIpv6Stack> {
2651+
match self {
2652+
PrivateIpStack::V6(v6) | PrivateIpStack::DualStack { v6, .. } => {
2653+
Some(v6)
2654+
}
2655+
PrivateIpStack::V4(_) => None,
2656+
}
2657+
}
2658+
2659+
/// Return the VPC-private IPv6 address, if it exists.
2660+
pub fn ipv6_addr(&self) -> Option<&Ipv6Addr> {
2661+
self.ipv6_stack().map(|s| &s.ip)
2662+
}
2663+
2664+
/// Return true if this is an IPv4-only stack, and false otherwise.
2665+
pub fn is_ipv4_only(&self) -> bool {
2666+
matches!(self, PrivateIpStack::V4(_))
2667+
}
2668+
2669+
/// Return true if this is an IPv6-only stack, and false otherwise.
2670+
pub fn is_ipv6_only(&self) -> bool {
2671+
matches!(self, PrivateIpStack::V6(_))
2672+
}
2673+
2674+
/// Return true if this is dual-stack, and false otherwise.
2675+
pub fn is_dual_stack(&self) -> bool {
2676+
matches!(self, PrivateIpStack::DualStack { .. })
2677+
}
2678+
2679+
/// Return the IPv4 transit IPs, if they exist.
2680+
pub fn ipv4_transit_ips(&self) -> Option<&[Ipv4Net]> {
2681+
self.ipv4_stack().map(|c| c.transit_ips.as_slice())
2682+
}
2683+
2684+
/// Return the IPv6 transit IPs, if they exist.
2685+
pub fn ipv6_transit_ips(&self) -> Option<&[Ipv6Net]> {
2686+
self.ipv6_stack().map(|c| c.transit_ips.as_slice())
2687+
}
2688+
2689+
/// Return all transit IPs, of any IP version.
2690+
pub fn all_transit_ips(&self) -> impl Iterator<Item = IpNet> + '_ {
2691+
let v4 = self
2692+
.ipv4_transit_ips()
2693+
.into_iter()
2694+
.flatten()
2695+
.copied()
2696+
.map(Into::into);
2697+
let v6 = self
2698+
.ipv6_transit_ips()
2699+
.into_iter()
2700+
.flatten()
2701+
.copied()
2702+
.map(Into::into);
2703+
v4.chain(v6)
2704+
}
2705+
}
2706+
2707+
/// The VPC-private IPv4 stack for a network interface
2708+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
2709+
pub struct PrivateIpv4Stack {
2710+
/// The VPC-private IPv4 address for the interface.
2711+
pub ip: Ipv4Addr,
2712+
/// A set of additional IPv4 networks that this interface may send and
26202713
/// receive traffic on.
2621-
#[serde(default)]
2622-
pub transit_ips: Vec<IpNet>,
2714+
pub transit_ips: Vec<Ipv4Net>,
2715+
}
2716+
2717+
/// The VPC-private IPv6 stack for a network interface
2718+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
2719+
pub struct PrivateIpv6Stack {
2720+
/// The VPC-private IPv6 address for the interface.
2721+
pub ip: Ipv6Addr,
2722+
/// A set of additional IPv6 networks that this interface may send and
2723+
/// receive traffic on.
2724+
pub transit_ips: Vec<Ipv6Net>,
26232725
}
26242726

26252727
#[derive(

common/src/api/internal/shared/external_ip/mod.rs

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
77
pub mod v1;
88

9+
use crate::address::ConcreteIp;
10+
use crate::address::Ip;
911
use crate::address::NUM_SOURCE_NAT_PORTS;
1012
use daft::Diffable;
1113
use itertools::Either;
@@ -17,28 +19,6 @@ use std::net::IpAddr;
1719
use std::net::Ipv4Addr;
1820
use std::net::Ipv6Addr;
1921

20-
/// Trait for any IP address type.
21-
///
22-
/// This is used to constrain the external addressing types below.
23-
pub trait Ip:
24-
Clone
25-
+ Copy
26-
+ std::fmt::Debug
27-
+ Diffable
28-
+ Eq
29-
+ JsonSchema
30-
+ std::hash::Hash
31-
+ PartialOrd
32-
+ PartialEq
33-
+ Ord
34-
+ Serialize
35-
+ SnatSchema
36-
{
37-
}
38-
impl Ip for Ipv4Addr {}
39-
impl Ip for Ipv6Addr {}
40-
impl Ip for IpAddr {}
41-
4222
/// Helper trait specifying the name of the JSON Schema for a `SourceNatConfig`.
4323
///
4424
/// This exists so we can use a generic type and have the names of the concrete
@@ -65,23 +45,6 @@ impl SnatSchema for IpAddr {
6545
}
6646
}
6747

68-
/// An IP address of a specific version, IPv4 or IPv6.
69-
pub trait ConcreteIp: Ip {
70-
fn into_ipaddr(self) -> IpAddr;
71-
}
72-
73-
impl ConcreteIp for Ipv4Addr {
74-
fn into_ipaddr(self) -> IpAddr {
75-
IpAddr::V4(self)
76-
}
77-
}
78-
79-
impl ConcreteIp for Ipv6Addr {
80-
fn into_ipaddr(self) -> IpAddr {
81-
IpAddr::V6(self)
82-
}
83-
}
84-
8548
/// Helper trait specifying the name of the JSON Schema for an
8649
/// `ExternalIpConfig` object.
8750
///
@@ -140,7 +103,7 @@ pub struct SourceNatConfig<T: Ip> {
140103
// should be fine as long as we're using JSON or similar formats.)
141104
#[derive(Deserialize, JsonSchema)]
142105
#[serde(remote = "SourceNatConfig")]
143-
struct SourceNatConfigShadow<T: Ip> {
106+
struct SourceNatConfigShadow<T: Ip + SnatSchema> {
144107
/// The external address provided to the instance or service.
145108
ip: T,
146109
/// The first port used for source NAT, inclusive.
@@ -155,7 +118,7 @@ pub type SourceNatConfigGeneric = SourceNatConfig<IpAddr>;
155118

156119
impl<T> JsonSchema for SourceNatConfig<T>
157120
where
158-
T: Ip,
121+
T: Ip + SnatSchema,
159122
{
160123
fn schema_name() -> String {
161124
<T as SnatSchema>::json_schema_name()
@@ -170,7 +133,10 @@ where
170133

171134
// We implement `Deserialize` manually to add validity checking on the port
172135
// range.
173-
impl<'de, T: Ip + Deserialize<'de>> Deserialize<'de> for SourceNatConfig<T> {
136+
impl<'de, T> Deserialize<'de> for SourceNatConfig<T>
137+
where
138+
T: Ip + SnatSchema + Deserialize<'de>,
139+
{
174140
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175141
where
176142
D: serde::Deserializer<'de>,
@@ -346,7 +312,7 @@ pub type ExternalIpv6Config = ExternalIps<Ipv6Addr>;
346312
#[serde(remote = "ExternalIps")]
347313
struct ExternalIpsShadow<T>
348314
where
349-
T: ConcreteIp,
315+
T: ConcreteIp + SnatSchema + ExternalIpSchema,
350316
{
351317
/// Source NAT configuration, for outbound-only connectivity.
352318
source_nat: Option<SourceNatConfig<T>>,
@@ -384,7 +350,7 @@ impl JsonSchema for ExternalIpv6Config {
384350
// SNAT, ephemeral, and floating IPs in the input data.
385351
impl<'de, T> Deserialize<'de> for ExternalIps<T>
386352
where
387-
T: ConcreteIp + Deserialize<'de>,
353+
T: ConcreteIp + SnatSchema + ExternalIpSchema + Deserialize<'de>,
388354
{
389355
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
390356
where

end-to-end-tests/src/instance_launch.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ async fn instance_launch() -> Result<()> {
7070
name: disk_name.clone(),
7171
}),
7272
disks: Vec::new(),
73-
network_interfaces: InstanceNetworkInterfaceAttachment::Default,
73+
network_interfaces: InstanceNetworkInterfaceAttachment::DefaultIpv4,
7474
external_ips: vec![ExternalIpCreate::Ephemeral { pool: None }],
7575
user_data: String::new(),
7676
ssh_public_keys: Some(vec![oxide_client::types::NameOrId::Name(

0 commit comments

Comments
 (0)