@@ -32,7 +32,9 @@ use nexus_test_utils::resource_helpers::object_create;
3232use nexus_test_utils:: resource_helpers:: object_create_error;
3333use nexus_test_utils:: resource_helpers:: object_delete;
3434use nexus_test_utils:: resource_helpers:: object_delete_error;
35+ use nexus_test_utils:: resource_helpers:: object_get;
3536use nexus_test_utils:: resource_helpers:: object_put;
37+ use nexus_test_utils:: resource_helpers:: object_put_error;
3638use nexus_test_utils:: resource_helpers:: objects_list_page_authz;
3739use nexus_test_utils:: resource_helpers:: DiskTest ;
3840use 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