Skip to content
Merged
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions common/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,51 @@ impl Iterator for IpRangeIter {
}
}

/// Trait for any IP address type.
pub trait Ip:
Clone
+ Copy
+ std::fmt::Debug
+ Diffable
+ Eq
+ JsonSchema
+ std::hash::Hash
+ PartialOrd
+ PartialEq
+ Ord
+ Serialize
{
}
impl Ip for Ipv4Addr {}
impl Ip for Ipv6Addr {}
impl Ip for IpAddr {}

/// An IP address of a specific version, IPv4 or IPv6.
pub trait ConcreteIp: Ip {
fn into_ipaddr(self) -> IpAddr;
fn into_ipnet(self) -> ipnetwork::IpNetwork;
}

impl ConcreteIp for Ipv4Addr {
fn into_ipaddr(self) -> IpAddr {
IpAddr::V4(self)
}

fn into_ipnet(self) -> ipnetwork::IpNetwork {
ipnetwork::IpNetwork::V4(ipnetwork::Ipv4Network::from(self))
}
}

impl ConcreteIp for Ipv6Addr {
fn into_ipaddr(self) -> IpAddr {
IpAddr::V6(self)
}

fn into_ipnet(self) -> ipnetwork::IpNetwork {
ipnetwork::IpNetwork::V6(ipnetwork::Ipv6Network::from(self))
}
}

#[cfg(test)]
mod test {
use serde_json::json;
Expand Down
116 changes: 109 additions & 7 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use omicron_uuid_kinds::InstanceUuid;
use omicron_uuid_kinds::SledUuid;
use oxnet::IpNet;
use oxnet::Ipv4Net;
use oxnet::Ipv6Net;
use parse_display::Display;
use parse_display::FromStr;
use rand::Rng;
Expand All @@ -43,6 +44,7 @@ use std::fmt::Formatter;
use std::fmt::Result as FormatResult;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::num::ParseIntError;
use std::num::{NonZeroU16, NonZeroU32};
use std::ops::Deref;
Expand Down Expand Up @@ -2608,18 +2610,118 @@ pub struct InstanceNetworkInterface {
/// The MAC address assigned to this interface.
pub mac: MacAddr,

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

/// A set of additional networks that this interface may send and
/// The VPC-private IP stack for this interface.
pub ip_stack: PrivateIpStack,
}

/// The VPC-private IP stack for a network interface.
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
#[serde(rename_all = "snake_case", tag = "type", content = "value")]
pub enum PrivateIpStack {
/// The interface has only an IPv4 stack.
V4(PrivateIpv4Stack),
/// The interface has only an IPv6 stack.
V6(PrivateIpv6Stack),
/// The interface is dual-stack IPv4 and IPv6.
DualStack { v4: PrivateIpv4Stack, v6: PrivateIpv6Stack },
}

impl PrivateIpStack {
/// Return the IPv4 stack, if it exists.
pub fn ipv4_stack(&self) -> Option<&PrivateIpv4Stack> {
match self {
PrivateIpStack::V4(v4) | PrivateIpStack::DualStack { v4, .. } => {
Some(v4)
}
PrivateIpStack::V6(_) => None,
}
}

/// Return the VPC-private IPv4 address, if it exists.
pub fn ipv4_addr(&self) -> Option<&Ipv4Addr> {
self.ipv4_stack().map(|s| &s.ip)
}

/// Return the IPv6 stack, if it exists.
pub fn ipv6_stack(&self) -> Option<&PrivateIpv6Stack> {
match self {
PrivateIpStack::V6(v6) | PrivateIpStack::DualStack { v6, .. } => {
Some(v6)
}
PrivateIpStack::V4(_) => None,
}
}

/// Return the VPC-private IPv6 address, if it exists.
pub fn ipv6_addr(&self) -> Option<&Ipv6Addr> {
self.ipv6_stack().map(|s| &s.ip)
}

/// Return true if this is an IPv4-only stack, and false otherwise.
pub fn is_ipv4_only(&self) -> bool {
matches!(self, PrivateIpStack::V4(_))
}

/// Return true if this is an IPv6-only stack, and false otherwise.
pub fn is_ipv6_only(&self) -> bool {
matches!(self, PrivateIpStack::V6(_))
}

/// Return true if this is dual-stack, and false otherwise.
pub fn is_dual_stack(&self) -> bool {
matches!(self, PrivateIpStack::DualStack { .. })
}

/// Return the IPv4 transit IPs, if they exist.
pub fn ipv4_transit_ips(&self) -> Option<&[Ipv4Net]> {
self.ipv4_stack().map(|c| c.transit_ips.as_slice())
}

/// Return the IPv6 transit IPs, if they exist.
pub fn ipv6_transit_ips(&self) -> Option<&[Ipv6Net]> {
self.ipv6_stack().map(|c| c.transit_ips.as_slice())
}

/// Return all transit IPs, of any IP version.
pub fn all_transit_ips(&self) -> impl Iterator<Item = IpNet> + '_ {
let v4 = self
.ipv4_transit_ips()
.into_iter()
.flatten()
.copied()
.map(Into::into);
let v6 = self
.ipv6_transit_ips()
.into_iter()
.flatten()
.copied()
.map(Into::into);
v4.chain(v6)
}
}

/// The VPC-private IPv4 stack for a network interface
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
pub struct PrivateIpv4Stack {
/// The VPC-private IPv4 address for the interface.
pub ip: Ipv4Addr,
/// A set of additional IPv4 networks that this interface may send and
/// receive traffic on.
#[serde(default)]
pub transit_ips: Vec<IpNet>,
pub transit_ips: Vec<Ipv4Net>,
}

/// The VPC-private IPv6 stack for a network interface
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
pub struct PrivateIpv6Stack {
/// The VPC-private IPv6 address for the interface.
pub ip: Ipv6Addr,
/// A set of additional IPv6 networks that this interface may send and
/// receive traffic on.
pub transit_ips: Vec<Ipv6Net>,
}

#[derive(
Expand Down
54 changes: 10 additions & 44 deletions common/src/api/internal/shared/external_ip/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

pub mod v1;

use crate::address::ConcreteIp;
use crate::address::Ip;
use crate::address::NUM_SOURCE_NAT_PORTS;
use daft::Diffable;
use itertools::Either;
Expand All @@ -17,28 +19,6 @@ use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;

/// Trait for any IP address type.
///
/// This is used to constrain the external addressing types below.
pub trait Ip:
Clone
+ Copy
+ std::fmt::Debug
+ Diffable
+ Eq
+ JsonSchema
+ std::hash::Hash
+ PartialOrd
+ PartialEq
+ Ord
+ Serialize
+ SnatSchema
{
}
impl Ip for Ipv4Addr {}
impl Ip for Ipv6Addr {}
impl Ip for IpAddr {}

/// Helper trait specifying the name of the JSON Schema for a `SourceNatConfig`.
///
/// This exists so we can use a generic type and have the names of the concrete
Expand All @@ -65,23 +45,6 @@ impl SnatSchema for IpAddr {
}
}

/// An IP address of a specific version, IPv4 or IPv6.
pub trait ConcreteIp: Ip {
fn into_ipaddr(self) -> IpAddr;
}

impl ConcreteIp for Ipv4Addr {
fn into_ipaddr(self) -> IpAddr {
IpAddr::V4(self)
}
}

impl ConcreteIp for Ipv6Addr {
fn into_ipaddr(self) -> IpAddr {
IpAddr::V6(self)
}
}

/// Helper trait specifying the name of the JSON Schema for an
/// `ExternalIpConfig` object.
///
Expand Down Expand Up @@ -140,7 +103,7 @@ pub struct SourceNatConfig<T: Ip> {
// should be fine as long as we're using JSON or similar formats.)
#[derive(Deserialize, JsonSchema)]
#[serde(remote = "SourceNatConfig")]
struct SourceNatConfigShadow<T: Ip> {
struct SourceNatConfigShadow<T: Ip + SnatSchema> {
/// The external address provided to the instance or service.
ip: T,
/// The first port used for source NAT, inclusive.
Expand All @@ -155,7 +118,7 @@ pub type SourceNatConfigGeneric = SourceNatConfig<IpAddr>;

impl<T> JsonSchema for SourceNatConfig<T>
where
T: Ip,
T: Ip + SnatSchema,
{
fn schema_name() -> String {
<T as SnatSchema>::json_schema_name()
Expand All @@ -170,7 +133,10 @@ where

// We implement `Deserialize` manually to add validity checking on the port
// range.
impl<'de, T: Ip + Deserialize<'de>> Deserialize<'de> for SourceNatConfig<T> {
impl<'de, T> Deserialize<'de> for SourceNatConfig<T>
where
T: Ip + SnatSchema + Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down Expand Up @@ -346,7 +312,7 @@ pub type ExternalIpv6Config = ExternalIps<Ipv6Addr>;
#[serde(remote = "ExternalIps")]
struct ExternalIpsShadow<T>
where
T: ConcreteIp,
T: ConcreteIp + SnatSchema + ExternalIpSchema,
{
/// Source NAT configuration, for outbound-only connectivity.
source_nat: Option<SourceNatConfig<T>>,
Expand Down Expand Up @@ -384,7 +350,7 @@ impl JsonSchema for ExternalIpv6Config {
// SNAT, ephemeral, and floating IPs in the input data.
impl<'de, T> Deserialize<'de> for ExternalIps<T>
where
T: ConcreteIp + Deserialize<'de>,
T: ConcreteIp + SnatSchema + ExternalIpSchema + Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand Down
7 changes: 4 additions & 3 deletions end-to-end-tests/src/instance_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use omicron_test_utils::dev::poll::{CondCheckError, wait_for_condition};
use oxide_client::types::{
ByteCount, DiskBackend, DiskCreate, DiskSource, ExternalIp,
ExternalIpCreate, InstanceCpuCount, InstanceCreate, InstanceDiskAttachment,
InstanceNetworkInterfaceAttachment, InstanceState, SshKeyCreate,
InstanceNetworkInterfaceAttachment, InstanceState, IpVersion, SshKeyCreate,
};
use oxide_client::{ClientCurrentUserExt, ClientDisksExt, ClientInstancesExt};
use russh::{ChannelMsg, Disconnect};
Expand Down Expand Up @@ -70,10 +70,11 @@ async fn instance_launch() -> Result<()> {
name: disk_name.clone(),
}),
disks: Vec::new(),
network_interfaces: InstanceNetworkInterfaceAttachment::Default,
network_interfaces:
InstanceNetworkInterfaceAttachment::DefaultDualStack,
external_ips: vec![ExternalIpCreate::Ephemeral {
pool: None,
ip_version: None,
ip_version: Some(IpVersion::V4),
}],
user_data: String::new(),
ssh_public_keys: Some(vec![oxide_client::types::NameOrId::Name(
Expand Down
Loading
Loading