Skip to content

Commit 71da009

Browse files
authored
Validate transit IPs on network interface update (#7559)
This PR adds in some checks when setting transit IPs on a NIC to ensure that we have: * Only unicast addresses. * No loopback addresses. * No duplicates. As discussed in the ticket, violation of any of these isn't going to leave OPTE or CRDB in a broken state -- just a confusing one for end users. This should make things unambiguous in terms of how the transit IPs will be handled. Closes #7530.
1 parent 665cf83 commit 71da009

File tree

2 files changed

+276
-2
lines changed

2 files changed

+276
-2
lines changed

nexus/src/app/network_interface.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ use nexus_db_queries::authz::ApiResource;
22
use nexus_db_queries::context::OpContext;
33
use nexus_db_queries::db::queries::network_interface;
44
use nexus_types::external_api::params;
5+
use omicron_common::api::external::http_pagination::PaginatedBy;
56
use omicron_common::api::external::CreateResult;
67
use omicron_common::api::external::DeleteResult;
78
use omicron_common::api::external::Error;
89
use omicron_common::api::external::ListResultVec;
910
use omicron_common::api::external::LookupResult;
1011
use omicron_common::api::external::NameOrId;
11-
12-
use omicron_common::api::external::http_pagination::PaginatedBy;
1312
use omicron_common::api::external::UpdateResult;
1413
use omicron_uuid_kinds::GenericUuid;
1514
use omicron_uuid_kinds::InstanceUuid;
15+
use oxnet::IpNet;
1616
use uuid::Uuid;
1717

1818
use nexus_db_queries::authz;
@@ -148,6 +148,7 @@ impl super::Nexus {
148148
) -> UpdateResult<db::model::InstanceNetworkInterface> {
149149
let (.., authz_instance, authz_interface) =
150150
network_interface_lookup.lookup_for(authz::Action::Modify).await?;
151+
validate_transit_ips(updates.transit_ips.as_slice())?;
151152
self.db_datastore
152153
.instance_update_network_interface(
153154
opctx,
@@ -213,3 +214,45 @@ impl super::Nexus {
213214
}
214215
}
215216
}
217+
218+
fn validate_transit_ips(ips: &[IpNet]) -> Result<(), Error> {
219+
for (i, ip) in ips.iter().enumerate() {
220+
let (count, ty) = if ip.is_host_net() {
221+
("", "address")
222+
} else {
223+
(" block", "network")
224+
};
225+
226+
if !ip.is_network_address() {
227+
return Err(Error::invalid_request(format!(
228+
"transit IP{count} {ip} has a non-zero host identifier"
229+
)));
230+
}
231+
232+
if ip.is_multicast() {
233+
return Err(Error::invalid_request(format!(
234+
"transit IP{count} {ip} is a multicast {ty}"
235+
)));
236+
}
237+
238+
if ip.is_loopback() {
239+
return Err(Error::invalid_request(format!(
240+
"transit IP{count} {ip} is a loopback {ty}"
241+
)));
242+
}
243+
244+
// Checking for overlapping CIDRs using all prior ips is O(n^2). This
245+
// is an infrequent check, and we can bound n if desired.
246+
// The fastest way to catch overlaps would be to make use of a trie for
247+
// representing past `IpNet`s per address family.
248+
let overlap = &ips[..i].iter().find(|el| ip.overlaps(el));
249+
250+
if let Some(past) = overlap {
251+
return Err(Error::invalid_request(format!(
252+
"transit IP{count} {ip} overlaps with {past}"
253+
)));
254+
}
255+
}
256+
257+
Ok(())
258+
}

nexus/tests/integration_tests/instances.rs

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ use nexus_test_utils::resource_helpers::object_create;
3232
use nexus_test_utils::resource_helpers::object_create_error;
3333
use nexus_test_utils::resource_helpers::object_delete;
3434
use nexus_test_utils::resource_helpers::object_delete_error;
35+
use nexus_test_utils::resource_helpers::object_get;
3536
use nexus_test_utils::resource_helpers::object_put;
37+
use nexus_test_utils::resource_helpers::object_put_error;
3638
use nexus_test_utils::resource_helpers::objects_list_page_authz;
3739
use nexus_test_utils::resource_helpers::DiskTest;
3840
use nexus_test_utils::wait_for_producer;
@@ -3116,6 +3118,235 @@ async fn test_instance_update_network_interfaces(
31163118
assert_eq!(iface.identity.name, if_params[0].identity.name);
31173119
}
31183120

3121+
#[nexus_test]
3122+
async fn test_instance_update_network_interface_transit_ips(
3123+
cptestctx: &ControlPlaneTestContext,
3124+
) {
3125+
let client = &cptestctx.external_client;
3126+
let instance_name = "transit-ips-test";
3127+
let nic_name = "net0";
3128+
3129+
create_project_and_pool(&client).await;
3130+
3131+
// Create a stopped instance with a single network interface.
3132+
let instance = create_instance_with(
3133+
&client,
3134+
PROJECT_NAME,
3135+
instance_name,
3136+
&params::InstanceNetworkInterfaceAttachment::Default,
3137+
vec![],
3138+
vec![],
3139+
false,
3140+
Default::default(),
3141+
)
3142+
.await;
3143+
3144+
let url_interface = format!(
3145+
"/v1/network-interfaces/{nic_name}?instance={}",
3146+
instance.identity.id
3147+
);
3148+
3149+
let base_update = params::InstanceNetworkInterfaceUpdate {
3150+
identity: IdentityMetadataUpdateParams {
3151+
name: None,
3152+
description: None,
3153+
},
3154+
primary: false,
3155+
transit_ips: vec![
3156+
"10.0.0.0/9".parse().unwrap(),
3157+
"10.128.0.0/9".parse().unwrap(),
3158+
"1.1.1.1/32".parse().unwrap(),
3159+
],
3160+
};
3161+
3162+
// Verify that a selection of transit IPs (mixture of private and global
3163+
// unicast, no overlaps) is accepted.
3164+
let updated_nic: InstanceNetworkInterface =
3165+
object_put(client, &url_interface, &base_update).await;
3166+
3167+
assert_eq!(base_update.transit_ips, updated_nic.transit_ips);
3168+
3169+
// Non-canonical form (e.g., host identifier is nonzero) subnets should
3170+
// be rejected.
3171+
let with_extra_bits = params::InstanceNetworkInterfaceUpdate {
3172+
transit_ips: vec![
3173+
"10.0.0.0/9".parse().unwrap(),
3174+
"10.128.0.0/9".parse().unwrap(),
3175+
// Invalid vvv
3176+
"172.30.255.255/24".parse().unwrap(),
3177+
],
3178+
..base_update.clone()
3179+
};
3180+
let err = object_put_error(
3181+
client,
3182+
&url_interface,
3183+
&with_extra_bits,
3184+
StatusCode::BAD_REQUEST,
3185+
)
3186+
.await;
3187+
assert_eq!(
3188+
err.message,
3189+
"transit IP block 172.30.255.255/24 has a non-zero host identifier",
3190+
);
3191+
3192+
// Multicast IP blocks should be rejected.
3193+
let with_mc1 = params::InstanceNetworkInterfaceUpdate {
3194+
transit_ips: vec![
3195+
"10.0.0.0/9".parse().unwrap(),
3196+
"10.128.0.0/9".parse().unwrap(),
3197+
"224.0.0.0/4".parse().unwrap(),
3198+
],
3199+
..base_update.clone()
3200+
};
3201+
let err = object_put_error(
3202+
client,
3203+
&url_interface,
3204+
&with_mc1,
3205+
StatusCode::BAD_REQUEST,
3206+
)
3207+
.await;
3208+
assert_eq!(
3209+
err.message,
3210+
"transit IP block 224.0.0.0/4 is a multicast network",
3211+
);
3212+
3213+
let with_mc2 = params::InstanceNetworkInterfaceUpdate {
3214+
transit_ips: vec![
3215+
"10.0.0.0/9".parse().unwrap(),
3216+
"10.128.0.0/9".parse().unwrap(),
3217+
"230.20.20.128/32".parse().unwrap(),
3218+
],
3219+
..base_update.clone()
3220+
};
3221+
let err = object_put_error(
3222+
client,
3223+
&url_interface,
3224+
&with_mc2,
3225+
StatusCode::BAD_REQUEST,
3226+
)
3227+
.await;
3228+
assert_eq!(
3229+
err.message,
3230+
"transit IP 230.20.20.128/32 is a multicast address",
3231+
);
3232+
3233+
// Loopback ranges.
3234+
let with_lo1 = params::InstanceNetworkInterfaceUpdate {
3235+
transit_ips: vec![
3236+
"10.0.0.0/9".parse().unwrap(),
3237+
"10.128.0.0/9".parse().unwrap(),
3238+
"127.42.77.0/24".parse().unwrap(),
3239+
],
3240+
..base_update.clone()
3241+
};
3242+
let err = object_put_error(
3243+
client,
3244+
&url_interface,
3245+
&with_lo1,
3246+
StatusCode::BAD_REQUEST,
3247+
)
3248+
.await;
3249+
assert_eq!(
3250+
err.message,
3251+
"transit IP block 127.42.77.0/24 is a loopback network",
3252+
);
3253+
3254+
let with_lo2 = params::InstanceNetworkInterfaceUpdate {
3255+
transit_ips: vec![
3256+
"10.0.0.0/9".parse().unwrap(),
3257+
"10.128.0.0/9".parse().unwrap(),
3258+
"127.0.0.1/32".parse().unwrap(),
3259+
],
3260+
..base_update.clone()
3261+
};
3262+
let err = object_put_error(
3263+
client,
3264+
&url_interface,
3265+
&with_lo2,
3266+
StatusCode::BAD_REQUEST,
3267+
)
3268+
.await;
3269+
assert_eq!(err.message, "transit IP 127.0.0.1/32 is a loopback address");
3270+
3271+
// Overlapping IP ranges should be rejected, as should identical ranges.
3272+
let with_dup1 = params::InstanceNetworkInterfaceUpdate {
3273+
transit_ips: vec![
3274+
"10.0.0.0/9".parse().unwrap(),
3275+
"10.128.0.0/9".parse().unwrap(),
3276+
"10.0.0.0/9".parse().unwrap(),
3277+
],
3278+
..base_update.clone()
3279+
};
3280+
let err = object_put_error(
3281+
client,
3282+
&url_interface,
3283+
&with_dup1,
3284+
StatusCode::BAD_REQUEST,
3285+
)
3286+
.await;
3287+
assert_eq!(
3288+
err.message,
3289+
"transit IP block 10.0.0.0/9 overlaps with 10.0.0.0/9",
3290+
);
3291+
3292+
let with_dup2 = params::InstanceNetworkInterfaceUpdate {
3293+
transit_ips: vec![
3294+
"10.0.0.0/9".parse().unwrap(),
3295+
"10.128.0.0/9".parse().unwrap(),
3296+
"10.128.32.0/24".parse().unwrap(),
3297+
],
3298+
..base_update.clone()
3299+
};
3300+
let err = object_put_error(
3301+
client,
3302+
&url_interface,
3303+
&with_dup2,
3304+
StatusCode::BAD_REQUEST,
3305+
)
3306+
.await;
3307+
assert_eq!(
3308+
err.message,
3309+
"transit IP block 10.128.32.0/24 overlaps with 10.128.0.0/9",
3310+
);
3311+
3312+
// Verify that we also catch more specific CIDRs appearing sooner in the list.
3313+
let with_dup3 = params::InstanceNetworkInterfaceUpdate {
3314+
transit_ips: vec![
3315+
"10.20.20.0/30".parse().unwrap(),
3316+
"10.0.0.0/8".parse().unwrap(),
3317+
],
3318+
..base_update.clone()
3319+
};
3320+
let err = object_put_error(
3321+
client,
3322+
&url_interface,
3323+
&with_dup3,
3324+
StatusCode::BAD_REQUEST,
3325+
)
3326+
.await;
3327+
assert_eq!(
3328+
err.message,
3329+
"transit IP block 10.0.0.0/8 overlaps with 10.20.20.0/30",
3330+
);
3331+
3332+
// ...and in the end, no changes have applied.
3333+
let final_nic: InstanceNetworkInterface =
3334+
object_get(client, &url_interface).await;
3335+
assert_eq!(updated_nic.transit_ips, final_nic.transit_ips);
3336+
3337+
// As a final sanity test, we can still effectively remove spoof checking
3338+
// using the unspecified network address.
3339+
let allow_all = params::InstanceNetworkInterfaceUpdate {
3340+
transit_ips: vec!["0.0.0.0/0".parse().unwrap()],
3341+
..base_update.clone()
3342+
};
3343+
3344+
let updated_nic: InstanceNetworkInterface =
3345+
object_put(client, &url_interface, &allow_all).await;
3346+
3347+
assert_eq!(allow_all.transit_ips, updated_nic.transit_ips);
3348+
}
3349+
31193350
/// This test specifically creates two NICs, the second of which will fail the
31203351
/// saga on purpose, since its IP address is the same. This is to verify that
31213352
/// the initial NIC is also deleted.

0 commit comments

Comments
 (0)