diff --git a/common/src/api/external/mod.rs b/common/src/api/external/mod.rs index 5c4799fd926..821588c8f41 100644 --- a/common/src/api/external/mod.rs +++ b/common/src/api/external/mod.rs @@ -1341,6 +1341,15 @@ impl From for IpNet { } } +impl From for IpNet { + fn from(n: IpAddr) -> IpNet { + match n { + IpAddr::V4(v4) => IpNet::from(v4), + IpAddr::V6(v6) => IpNet::from(v6), + } + } +} + impl std::fmt::Display for IpNet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/common/src/api/internal/nexus.rs b/common/src/api/internal/nexus.rs index cb007b211ad..9e71a40b934 100644 --- a/common/src/api/internal/nexus.rs +++ b/common/src/api/internal/nexus.rs @@ -5,7 +5,8 @@ //! APIs exposed by Nexus. use crate::api::external::{ - ByteCount, DiskState, Generation, InstanceCpuCount, InstanceState, + ByteCount, DiskState, Generation, InstanceCpuCount, InstanceState, IpNet, + Vni, }; use chrono::{DateTime, Utc}; use parse_display::{Display, FromStr}; @@ -180,3 +181,13 @@ mod tests { } } } + +/// A `HostIdentifier` represents either an IP host or network (v4 or v6), +/// or an entire VPC (identified by its VNI). It is used in firewall rule +/// host filters. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[serde(tag = "type", content = "value", rename_all = "snake_case")] +pub enum HostIdentifier { + Ip(IpNet), + Vpc(Vni), +} diff --git a/nexus/src/app/vpc.rs b/nexus/src/app/vpc.rs index 3fc64efc5c7..ee6a81643c6 100644 --- a/nexus/src/app/vpc.rs +++ b/nexus/src/app/vpc.rs @@ -22,14 +22,16 @@ use omicron_common::api::external::CreateResult; use omicron_common::api::external::DeleteResult; use omicron_common::api::external::Error; use omicron_common::api::external::InternalContext; +use omicron_common::api::external::IpNet; use omicron_common::api::external::ListResultVec; use omicron_common::api::external::LookupResult; use omicron_common::api::external::LookupType; use omicron_common::api::external::NameOrId; use omicron_common::api::external::UpdateResult; +use omicron_common::api::external::Vni; use omicron_common::api::external::VpcFirewallRuleUpdateParams; +use omicron_common::api::internal::nexus::HostIdentifier; use ref_cast::RefCast; -use sled_agent_client::types::IpNet; use sled_agent_client::types::NetworkInterface; use futures::future::join_all; @@ -532,10 +534,10 @@ impl super::Nexus { .unwrap_or(&no_interfaces) .iter() .filter(|nic| match (net, nic.ip) { - (external::IpNet::V4(net), IpAddr::V4(ip)) => { + (IpNet::V4(net), IpAddr::V4(ip)) => { net.contains(ip) } - (external::IpNet::V6(net), IpAddr::V6(ip)) => { + (IpNet::V6(net), IpAddr::V6(ip)) => { net.contains(ip) } (_, _) => false, @@ -562,7 +564,12 @@ impl super::Nexus { .get(&name) .unwrap_or(&no_interfaces) { - host_addrs.push(IpNet::from(interface.ip)) + host_addrs.push( + HostIdentifier::Ip(IpNet::from( + interface.ip, + )) + .into(), + ) } } external::VpcFirewallRuleHostFilter::Subnet( @@ -572,21 +579,34 @@ impl super::Nexus { .get(&name) .unwrap_or(&no_networks) { - host_addrs.push(IpNet::from(*subnet)); + host_addrs.push( + HostIdentifier::Ip(IpNet::from( + *subnet, + )) + .into(), + ); } } external::VpcFirewallRuleHostFilter::Ip(addr) => { - host_addrs.push(IpNet::from(*addr)) + host_addrs.push( + HostIdentifier::Ip(IpNet::from(*addr)) + .into(), + ) } external::VpcFirewallRuleHostFilter::IpNet(net) => { - host_addrs.push(IpNet::from(*net)) + host_addrs.push(HostIdentifier::Ip(*net).into()) } external::VpcFirewallRuleHostFilter::Vpc(name) => { for interface in vpc_interfaces .get(&name) .unwrap_or(&no_interfaces) { - host_addrs.push(IpNet::from(interface.ip)) + host_addrs.push( + HostIdentifier::Vpc(Vni::try_from( + *interface.vni, + )?) + .into(), + ) } } } diff --git a/openapi/sled-agent.json b/openapi/sled-agent.json index 8372db204a5..07686bb9442 100644 --- a/openapi/sled-agent.json +++ b/openapi/sled-agent.json @@ -840,6 +840,47 @@ "format": "uint64", "minimum": 0 }, + "HostIdentifier": { + "description": "A `HostIdentifier` represents either an IP host or network (v4 or v6), or an entire VPC (identified by its VNI). It is used in firewall rule host filters.", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "ip" + ] + }, + "value": { + "$ref": "#/components/schemas/IpNet" + } + }, + "required": [ + "type", + "value" + ] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "vpc" + ] + }, + "value": { + "$ref": "#/components/schemas/Vni" + } + }, + "required": [ + "type", + "value" + ] + } + ] + }, "InstanceCpuCount": { "description": "The number of CPUs in an Instance", "type": "integer", @@ -1769,7 +1810,7 @@ "nullable": true, "type": "array", "items": { - "$ref": "#/components/schemas/IpNet" + "$ref": "#/components/schemas/HostIdentifier" } }, "filter_ports": { diff --git a/sled-agent-client/src/lib.rs b/sled-agent-client/src/lib.rs index cf36967cafe..7452e92e477 100644 --- a/sled-agent-client/src/lib.rs +++ b/sled-agent-client/src/lib.rs @@ -208,6 +208,12 @@ impl From for types::Vni { } } +impl From for omicron_common::api::external::Vni { + fn from(s: types::Vni) -> Self { + Self::try_from(s.0 as u32).unwrap() + } +} + impl From for types::MacAddr { fn from(s: omicron_common::api::external::MacAddr) -> Self { Self::try_from(s.0.to_string()) @@ -327,6 +333,18 @@ impl From } } +impl From + for types::HostIdentifier +{ + fn from(s: omicron_common::api::internal::nexus::HostIdentifier) -> Self { + use omicron_common::api::internal::nexus::HostIdentifier::*; + match s { + Ip(net) => Self::Ip(net.into()), + Vpc(vni) => Self::Vpc(vni.into()), + } + } +} + impl From for types::VpcFirewallRuleAction { diff --git a/sled-agent/src/opte/illumos/firewall_rules.rs b/sled-agent/src/opte/illumos/firewall_rules.rs index 4a1cd8011b4..e8fa9bde59d 100644 --- a/sled-agent/src/opte/illumos/firewall_rules.rs +++ b/sled-agent/src/opte/illumos/firewall_rules.rs @@ -12,6 +12,7 @@ use omicron_common::api::external::VpcFirewallRuleAction; use omicron_common::api::external::VpcFirewallRuleDirection; use omicron_common::api::external::VpcFirewallRuleProtocol; use omicron_common::api::external::VpcFirewallRuleStatus; +use omicron_common::api::internal::nexus::HostIdentifier; use oxide_vpc::api::Address; use oxide_vpc::api::Direction; use oxide_vpc::api::Filters; @@ -62,16 +63,23 @@ impl FromVpcFirewallRule for VpcFirewallRule { hosts .iter() .map(|host| match host { - IpNet::V4(net) if net.prefix() == 32 => { + HostIdentifier::Ip(IpNet::V4(net)) + if net.prefix() == 32 => + { Address::Ip(net.ip().into()) } - IpNet::V4(net) => Address::Subnet(Ipv4Cidr::new( - net.ip().into(), - Ipv4PrefixLen::new(net.prefix()).unwrap(), - )), - IpNet::V6(_net) => { + HostIdentifier::Ip(IpNet::V4(net)) => { + Address::Subnet(Ipv4Cidr::new( + net.ip().into(), + Ipv4PrefixLen::new(net.prefix()).unwrap(), + )) + } + HostIdentifier::Ip(IpNet::V6(_net)) => { todo!("IPv6 host filters") } + HostIdentifier::Vpc(vni) => { + Address::Vni(Vni::new(u32::from(*vni)).unwrap()) + } }) .collect::>() }, diff --git a/sled-agent/src/params.rs b/sled-agent/src/params.rs index 3afd431f520..fa8b41f407c 100644 --- a/sled-agent/src/params.rs +++ b/sled-agent/src/params.rs @@ -9,7 +9,7 @@ use omicron_common::address::{ }; use omicron_common::api::external; use omicron_common::api::internal::nexus::{ - DiskRuntimeState, InstanceRuntimeState, + DiskRuntimeState, HostIdentifier, InstanceRuntimeState, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -53,7 +53,7 @@ pub struct VpcFirewallRule { pub status: external::VpcFirewallRuleStatus, pub direction: external::VpcFirewallRuleDirection, pub targets: Vec, - pub filter_hosts: Option>, + pub filter_hosts: Option>, pub filter_ports: Option>, pub filter_protocols: Option>, pub action: external::VpcFirewallRuleAction,