Skip to content

Commit b574dcd

Browse files
committed
Add support for ICMPv6 protocol filter in firewall rules
This splits out the pre-existing, shared firewall protocol enum from `omicron-common` into local versions for Nexus and the sled-agent. This reflects the fact that the type was present from the initial version anyway, and lets make normal updates to the API. This also adds new versions to the Nexus and sled-agent APIs with type conversions and endpoint handlers. Fixes #9908
1 parent af9dd12 commit b574dcd

26 files changed

Lines changed: 693 additions & 32 deletions

File tree

clients/sled-agent-client/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ impl From<omicron_common::api::external::VpcFirewallRuleProtocol>
270270
Tcp => Self::Tcp,
271271
Udp => Self::Udp,
272272
Icmp(v) => Self::Icmp(v),
273+
Icmp6(v) => Self::Icmp6(v),
273274
}
274275
}
275276
}

common/src/api/external/mod.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,8 +1877,7 @@ pub enum VpcFirewallRuleProtocol {
18771877
Tcp,
18781878
Udp,
18791879
Icmp(Option<VpcFirewallIcmpFilter>),
1880-
// TODO: IPv6 not supported by instances.
1881-
// Icmpv6(Option<VpcFirewallIcmpFilter>),
1880+
Icmp6(Option<VpcFirewallIcmpFilter>),
18821881
// TODO: OPTE does not yet permit further L4 protocols. (opte#609)
18831882
// Other(u16),
18841883
}
@@ -1901,6 +1900,12 @@ impl FromStr for VpcFirewallRuleProtocol {
19011900
(lhs, Some(rhs)) if lhs.eq_ignore_ascii_case("icmp") => {
19021901
Ok(Self::Icmp(Some(rhs.parse()?)))
19031902
}
1903+
(lhs, None) if lhs.eq_ignore_ascii_case("icmp6") => {
1904+
Ok(Self::Icmp6(None))
1905+
}
1906+
(lhs, Some(rhs)) if lhs.eq_ignore_ascii_case("icmp6") => {
1907+
Ok(Self::Icmp6(Some(rhs.parse()?)))
1908+
}
19041909
(lhs, None) => Err(Error::invalid_value(
19051910
"vpc_firewall_rule_protocol",
19061911
format!("unrecognized protocol: {lhs}"),
@@ -1930,6 +1935,8 @@ impl Display for VpcFirewallRuleProtocol {
19301935
VpcFirewallRuleProtocol::Udp => write!(f, "udp"),
19311936
VpcFirewallRuleProtocol::Icmp(None) => write!(f, "icmp"),
19321937
VpcFirewallRuleProtocol::Icmp(Some(v)) => write!(f, "icmp:{v}"),
1938+
VpcFirewallRuleProtocol::Icmp6(None) => write!(f, "icmp6"),
1939+
VpcFirewallRuleProtocol::Icmp6(Some(v)) => write!(f, "icmp6:{v}"),
19331940
}
19341941
}
19351942
}

illumos-utils/src/opte/firewall_rules.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ impl FromVpcFirewallRule for ResolvedVpcFirewallRule {
110110
}
111111
}))
112112
}
113+
VpcFirewallRuleProtocol::Icmp6(v) => {
114+
ProtoFilter::Icmpv6(v.map(|v| {
115+
oxide_vpc::api::IcmpFilter {
116+
ty: v.icmp_type,
117+
codes: v.code.map(Into::into),
118+
}
119+
}))
120+
}
113121
})
114122
.collect(),
115123
_ => vec![ProtoFilter::Any],

nexus/external-api/src/lib.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ use openapiv3::OpenAPI;
4848
mod v2025_11_20_00_local;
4949
mod v2026_01_01_00_local;
5050
mod v2026_01_30_00_local;
51+
mod v2026_03_14_00_local;
5152

5253
api_versions!([
5354
// API versions are in the format YYYY_MM_DD_NN.0.0, defined below as
@@ -78,6 +79,7 @@ api_versions!([
7879
// | date-based version should be at the top of the list.
7980
// v
8081
// (next_yyyy_mm_dd_nn, IDENT),
82+
(2026_03_18_00, ADD_ICMPV6_FIREWALL_SUPPORT),
8183
(2026_03_14_00, MULTICAST_DROP_MVLAN),
8284
(2026_03_12_00, CAPITALIZE_DESCRIPTIONS),
8385
(2026_03_06_01, SWITCH_SLOT_ENUM),
@@ -6080,12 +6082,31 @@ pub trait NexusExternalApi {
60806082
method = GET,
60816083
path = "/v1/vpc-firewall-rules",
60826084
tags = ["vpcs"],
6085+
versions = VERSION_ADD_ICMPV6_FIREWALL_SUPPORT..,
60836086
}]
60846087
async fn vpc_firewall_rules_view(
60856088
rqctx: RequestContext<Self::Context>,
60866089
query_params: Query<latest::vpc::VpcSelector>,
60876090
) -> Result<HttpResponseOk<VpcFirewallRules>, HttpError>;
60886091

6092+
/// List firewall rules
6093+
#[endpoint {
6094+
operation_id = "vpc_firewall_rules_view",
6095+
method = GET,
6096+
path = "/v1/vpc-firewall-rules",
6097+
tags = ["vpcs"],
6098+
versions = ..VERSION_ADD_ICMPV6_FIREWALL_SUPPORT,
6099+
}]
6100+
async fn vpc_firewall_rules_view_v2026_03_14_00(
6101+
rqctx: RequestContext<Self::Context>,
6102+
query_params: Query<latest::vpc::VpcSelector>,
6103+
) -> Result<HttpResponseOk<v2026_03_14_00_local::VpcFirewallRules>, HttpError>
6104+
{
6105+
Self::vpc_firewall_rules_view(rqctx, query_params).await.and_then(
6106+
|resp| resp.try_map(TryInto::try_into).map_err(HttpError::from),
6107+
)
6108+
}
6109+
60896110
// Note: the limits in the below comment come from the firewall rules model
60906111
// file, nexus/db-model/src/vpc_firewall_rule.rs.
60916112

@@ -6107,13 +6128,36 @@ pub trait NexusExternalApi {
61076128
method = PUT,
61086129
path = "/v1/vpc-firewall-rules",
61096130
tags = ["vpcs"],
6131+
versions = VERSION_ADD_ICMPV6_FIREWALL_SUPPORT..,
61106132
}]
61116133
async fn vpc_firewall_rules_update(
61126134
rqctx: RequestContext<Self::Context>,
61136135
query_params: Query<latest::vpc::VpcSelector>,
6114-
router_params: TypedBody<VpcFirewallRuleUpdateParams>,
6136+
update: TypedBody<VpcFirewallRuleUpdateParams>,
61156137
) -> Result<HttpResponseOk<VpcFirewallRules>, HttpError>;
61166138

6139+
/// Replace firewall rules
6140+
#[endpoint {
6141+
operation_id = "vpc_firewall_rules_update",
6142+
method = PUT,
6143+
path = "/v1/vpc-firewall-rules",
6144+
tags = ["vpcs"],
6145+
versions = ..VERSION_ADD_ICMPV6_FIREWALL_SUPPORT,
6146+
}]
6147+
async fn vpc_firewall_rules_update_v2026_03_14_00(
6148+
rqctx: RequestContext<Self::Context>,
6149+
query_params: Query<latest::vpc::VpcSelector>,
6150+
update: TypedBody<v2026_03_14_00_local::VpcFirewallRuleUpdateParams>,
6151+
) -> Result<HttpResponseOk<v2026_03_14_00_local::VpcFirewallRules>, HttpError>
6152+
{
6153+
let body = update.map(Into::into);
6154+
Self::vpc_firewall_rules_update(rqctx, query_params, body)
6155+
.await
6156+
.and_then(|resp| {
6157+
resp.try_map(TryInto::try_into).map_err(HttpError::from)
6158+
})
6159+
}
6160+
61176161
// VPC Routers
61186162

61196163
/// List routers
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
//! Types from API version 2026_03_14_00 that cannot live in `nexus-types-versions`
6+
//! because they convert to/from `omicron-common` types (orphan rule).
7+
8+
use api_identity::ObjectIdentity;
9+
use omicron_common::api::external;
10+
use omicron_common::api::external::Error;
11+
use omicron_common::api::external::IdentityMetadata;
12+
use omicron_common::api::external::L4PortRange;
13+
use omicron_common::api::external::Name;
14+
use omicron_common::api::external::ObjectIdentity;
15+
use omicron_common::api::external::VpcFirewallIcmpFilter;
16+
use omicron_common::api::external::VpcFirewallRuleAction;
17+
use omicron_common::api::external::VpcFirewallRuleDirection;
18+
use omicron_common::api::external::VpcFirewallRuleHostFilter;
19+
use omicron_common::api::external::VpcFirewallRulePriority;
20+
use omicron_common::api::external::VpcFirewallRuleStatus;
21+
use omicron_common::api::external::VpcFirewallRuleTarget;
22+
use schemars::JsonSchema;
23+
use serde::Deserialize;
24+
use serde::Serialize;
25+
use uuid::Uuid;
26+
27+
/// The protocols that may be specified in a firewall rule's filter.
28+
//
29+
// This is the version of the enum without `Icmp6`, for API versions up
30+
// through `MULTICAST_DROP_MVLAN`.
31+
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
32+
#[serde(rename_all = "snake_case")]
33+
#[serde(tag = "type", content = "value")]
34+
pub enum VpcFirewallRuleProtocol {
35+
Tcp,
36+
Udp,
37+
Icmp(Option<VpcFirewallIcmpFilter>),
38+
}
39+
40+
impl From<VpcFirewallRuleProtocol> for external::VpcFirewallRuleProtocol {
41+
fn from(p: VpcFirewallRuleProtocol) -> Self {
42+
match p {
43+
VpcFirewallRuleProtocol::Tcp => Self::Tcp,
44+
VpcFirewallRuleProtocol::Udp => Self::Udp,
45+
VpcFirewallRuleProtocol::Icmp(v) => Self::Icmp(v),
46+
}
47+
}
48+
}
49+
50+
/// Filters reduce the scope of a firewall rule.
51+
//
52+
// This is the version of the filter without `Icmp6` protocol support, for API
53+
// versions up through `MULTICAST_DROP_MVLAN`.
54+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
55+
pub struct VpcFirewallRuleFilter {
56+
/// If present, host filters match the "other end" of traffic from the
57+
/// target's perspective: for an inbound rule, they match the source of
58+
/// traffic. For an outbound rule, they match the destination.
59+
#[schemars(length(max = 256))]
60+
pub hosts: Option<Vec<VpcFirewallRuleHostFilter>>,
61+
62+
/// If present, the networking protocols this rule applies to.
63+
#[schemars(length(max = 256))]
64+
pub protocols: Option<Vec<VpcFirewallRuleProtocol>>,
65+
66+
/// If present, the destination ports or port ranges this rule applies to.
67+
#[schemars(length(max = 256))]
68+
pub ports: Option<Vec<L4PortRange>>,
69+
}
70+
71+
impl TryFrom<external::VpcFirewallRuleFilter> for VpcFirewallRuleFilter {
72+
type Error = Error;
73+
74+
fn try_from(f: external::VpcFirewallRuleFilter) -> Result<Self, Error> {
75+
let protocols = f
76+
.protocols
77+
.map(|ps| {
78+
ps.into_iter()
79+
.map(|p| match p {
80+
external::VpcFirewallRuleProtocol::Tcp => {
81+
Ok(VpcFirewallRuleProtocol::Tcp)
82+
}
83+
external::VpcFirewallRuleProtocol::Udp => {
84+
Ok(VpcFirewallRuleProtocol::Udp)
85+
}
86+
external::VpcFirewallRuleProtocol::Icmp(v) => {
87+
Ok(VpcFirewallRuleProtocol::Icmp(v))
88+
}
89+
external::VpcFirewallRuleProtocol::Icmp6(_) => {
90+
Err(Error::invalid_value(
91+
"vpc_firewall_rule_protocol",
92+
format!("unrecognized protocol: {p}"),
93+
))
94+
}
95+
})
96+
.collect::<Result<Vec<_>, _>>()
97+
})
98+
.transpose()?;
99+
Ok(Self { hosts: f.hosts, protocols, ports: f.ports })
100+
}
101+
}
102+
103+
impl From<VpcFirewallRuleFilter> for external::VpcFirewallRuleFilter {
104+
fn from(f: VpcFirewallRuleFilter) -> Self {
105+
Self {
106+
hosts: f.hosts,
107+
protocols: f
108+
.protocols
109+
.map(|ps| ps.into_iter().map(Into::into).collect()),
110+
ports: f.ports,
111+
}
112+
}
113+
}
114+
115+
/// A single rule in a VPC firewall.
116+
//
117+
// This is the version of the rule without `Icmp6` protocol support, for API
118+
// versions up through `MULTICAST_DROP_MVLAN`.
119+
#[derive(ObjectIdentity, Clone, Debug, Deserialize, Serialize, JsonSchema)]
120+
pub struct VpcFirewallRule {
121+
/// Common identifying metadata
122+
#[serde(flatten)]
123+
pub identity: IdentityMetadata,
124+
/// Whether this rule is in effect
125+
pub status: VpcFirewallRuleStatus,
126+
/// Whether this rule is for incoming or outgoing traffic
127+
pub direction: VpcFirewallRuleDirection,
128+
/// Determine the set of instances that the rule applies to
129+
pub targets: Vec<VpcFirewallRuleTarget>,
130+
/// Reductions on the scope of the rule
131+
pub filters: VpcFirewallRuleFilter,
132+
/// Whether traffic matching the rule should be allowed or dropped
133+
pub action: VpcFirewallRuleAction,
134+
/// The relative priority of this rule
135+
pub priority: VpcFirewallRulePriority,
136+
/// The VPC to which this rule belongs
137+
pub vpc_id: Uuid,
138+
}
139+
140+
impl TryFrom<external::VpcFirewallRule> for VpcFirewallRule {
141+
type Error = Error;
142+
143+
fn try_from(r: external::VpcFirewallRule) -> Result<Self, Error> {
144+
Ok(Self {
145+
identity: r.identity,
146+
status: r.status,
147+
direction: r.direction,
148+
targets: r.targets,
149+
filters: r.filters.try_into()?,
150+
action: r.action,
151+
priority: r.priority,
152+
vpc_id: r.vpc_id,
153+
})
154+
}
155+
}
156+
157+
/// Collection of a VPC's firewall rules.
158+
//
159+
// This is the version of the collection without `Icmp6` protocol support, for
160+
// API versions up through `MULTICAST_DROP_MVLAN`.
161+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
162+
pub struct VpcFirewallRules {
163+
pub rules: Vec<VpcFirewallRule>,
164+
}
165+
166+
impl TryFrom<external::VpcFirewallRules> for VpcFirewallRules {
167+
type Error = Error;
168+
169+
fn try_from(r: external::VpcFirewallRules) -> Result<Self, Error> {
170+
let rules = r
171+
.rules
172+
.into_iter()
173+
.map(VpcFirewallRule::try_from)
174+
.collect::<Result<Vec<_>, _>>()?;
175+
Ok(Self { rules })
176+
}
177+
}
178+
179+
/// A single rule in a VPC firewall update request.
180+
//
181+
// This is the version of the update without `Icmp6` protocol support, for API
182+
// versions up through `MULTICAST_DROP_MVLAN`.
183+
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
184+
pub struct VpcFirewallRuleUpdate {
185+
/// Name of the rule, unique to this VPC
186+
pub name: Name,
187+
/// Human-readable free-form text about a resource
188+
pub description: String,
189+
/// Whether this rule is in effect
190+
pub status: VpcFirewallRuleStatus,
191+
/// Whether this rule is for incoming or outgoing traffic
192+
pub direction: VpcFirewallRuleDirection,
193+
/// Determine the set of instances that the rule applies to
194+
#[schemars(length(max = 256))]
195+
pub targets: Vec<VpcFirewallRuleTarget>,
196+
/// Reductions on the scope of the rule
197+
pub filters: VpcFirewallRuleFilter,
198+
/// Whether traffic matching the rule should be allowed or dropped
199+
pub action: VpcFirewallRuleAction,
200+
/// The relative priority of this rule
201+
pub priority: VpcFirewallRulePriority,
202+
}
203+
204+
impl From<VpcFirewallRuleUpdate> for external::VpcFirewallRuleUpdate {
205+
fn from(u: VpcFirewallRuleUpdate) -> Self {
206+
Self {
207+
name: u.name,
208+
description: u.description,
209+
status: u.status,
210+
direction: u.direction,
211+
targets: u.targets,
212+
filters: u.filters.into(),
213+
action: u.action,
214+
priority: u.priority,
215+
}
216+
}
217+
}
218+
219+
/// Updated list of firewall rules. Will replace all existing rules.
220+
//
221+
// This is the version of the params without `Icmp6` protocol support, for API
222+
// versions up through `MULTICAST_DROP_MVLAN`.
223+
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
224+
pub struct VpcFirewallRuleUpdateParams {
225+
#[schemars(length(max = 1024))]
226+
#[serde(default)]
227+
pub rules: Vec<VpcFirewallRuleUpdate>,
228+
}
229+
230+
impl From<VpcFirewallRuleUpdateParams>
231+
for external::VpcFirewallRuleUpdateParams
232+
{
233+
fn from(p: VpcFirewallRuleUpdateParams) -> Self {
234+
Self { rules: p.rules.into_iter().map(Into::into).collect() }
235+
}
236+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
7bb5c322905252aa53b36256c313356d056278c3:openapi/nexus/nexus-2026031400.0.0-203a86.json

0 commit comments

Comments
 (0)