@@ -98,8 +98,7 @@ const MAX_PORT: i32 = u16::MAX as _;
9898/// FROM
9999/// ip_pool_range
100100/// WHERE
101- /// project_id = <project_id> OR
102- /// project_id IS NULL AND
101+ /// <pool restriction clause> AND
103102/// time_deleted IS NULL
104103/// )
105104/// CROSS JOIN
@@ -183,9 +182,36 @@ const MAX_PORT: i32 = u16::MAX as _;
183182/// (even though we send the start/stop to the database as `i32`s). We need to
184183/// cast it on the way out of the database, so that Diesel can correctly
185184/// deserialize it into an `i32`.
185+ ///
186+ /// Pool restriction
187+ /// ----------------
188+ ///
189+ /// Clients may optionally request an external address from a specific IP Pool.
190+ /// If they don't provide a pool, the query is restricted to all IP Pools
191+ /// available to their project (not reserved for a different project). In that
192+ /// case, the pool restriction looks like:
193+ ///
194+ /// ```sql
195+ /// (project_id = <project_id> OR project_id IS NULL)
196+ /// ```
197+ ///
198+ /// That is, we search for all pools that are unreserved for reserved for _this
199+ /// instance's_ project.
200+ ///
201+ /// If the client does provide a pool, an additional filtering clause is added,
202+ /// so that the whole filter looks like:
203+ ///
204+ /// ```sql
205+ /// pool_id = <pool_id> AND
206+ /// (project_id = <project_id> OR project_id IS NULL)
207+ /// ```
208+ ///
209+ /// That filter on `project_id` is a bit redundant, as the caller should be
210+ /// looking up the pool's ID by name, among those available to the instance's
211+ /// project. However, it helps provides safety in the case that the caller
212+ /// violates that expectation.
186213#[ derive( Debug , Clone ) ]
187214pub struct NextExternalIp {
188- // TODO-completeness: Add `ip_pool_id`, and restrict search to it.
189215 ip : IncompleteInstanceExternalIp ,
190216 // Number of ports reserved per IP address. Only applicable if the IP kind
191217 // is snat.
@@ -390,7 +416,7 @@ impl NextExternalIp {
390416 // FROM
391417 // ip_pool_range
392418 // WHERE
393- // (project_id = <project_id> OR project_id IS NULL) AND
419+ // <pool_restriction> AND
394420 // time_deleted IS NULL
395421 // ```
396422 fn push_address_sequence_subquery < ' a > (
@@ -410,13 +436,21 @@ impl NextExternalIp {
410436 out. push_identifier ( dsl:: first_address:: NAME ) ?;
411437 out. push_sql ( ") AS candidate_ip FROM " ) ;
412438 IP_POOL_RANGE_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
413- out. push_sql ( " WHERE (" ) ;
414- out. push_identifier ( dsl:: project_id:: NAME ) ?;
415- out. push_sql ( " = " ) ;
416- out. push_bind_param :: < sql_types:: Uuid , Uuid > ( self . ip . project_id ( ) ) ?;
417- out. push_sql ( " OR " ) ;
418- out. push_identifier ( dsl:: project_id:: NAME ) ?;
419- out. push_sql ( " IS NULL) AND " ) ;
439+ out. push_sql ( " WHERE " ) ;
440+ if let Some ( ref pool_id) = self . ip . pool_id ( ) {
441+ out. push_identifier ( dsl:: ip_pool_id:: NAME ) ?;
442+ out. push_sql ( " = " ) ;
443+ out. push_bind_param :: < sql_types:: Uuid , Uuid > ( pool_id) ?;
444+ } else {
445+ out. push_sql ( "(" ) ;
446+ out. push_identifier ( dsl:: project_id:: NAME ) ?;
447+ out. push_sql ( " = " ) ;
448+ out. push_bind_param :: < sql_types:: Uuid , Uuid > ( self . ip . project_id ( ) ) ?;
449+ out. push_sql ( " OR " ) ;
450+ out. push_identifier ( dsl:: project_id:: NAME ) ?;
451+ out. push_sql ( " IS NULL)" ) ;
452+ }
453+ out. push_sql ( " AND " ) ;
420454 out. push_identifier ( dsl:: time_deleted:: NAME ) ?;
421455 out. push_sql ( " IS NULL" ) ;
422456 Ok ( ( ) )
@@ -581,6 +615,7 @@ mod tests {
581615 use crate :: db:: model:: IpKind ;
582616 use crate :: db:: model:: IpPool ;
583617 use crate :: db:: model:: IpPoolRange ;
618+ use crate :: db:: model:: Name ;
584619 use crate :: external_api:: shared:: IpRange ;
585620 use async_bb8_diesel:: AsyncRunQueryDsl ;
586621 use dropshot:: test_util:: LogContext ;
@@ -589,6 +624,7 @@ mod tests {
589624 use omicron_common:: api:: external:: IdentityMetadataCreateParams ;
590625 use omicron_test_utils:: dev;
591626 use omicron_test_utils:: dev:: db:: CockroachInstance ;
627+ use std:: net:: IpAddr ;
592628 use std:: net:: Ipv4Addr ;
593629 use std:: sync:: Arc ;
594630 use uuid:: Uuid ;
@@ -999,4 +1035,113 @@ mod tests {
9991035
10001036 context. success ( ) . await ;
10011037 }
1038+
1039+ #[ tokio:: test]
1040+ async fn test_next_external_ip_is_restricted_to_pools ( ) {
1041+ let context =
1042+ TestContext :: new ( "test_next_external_ip_is_restricted_to_pools" )
1043+ . await ;
1044+
1045+ // Create two pools, neither project-restricted.
1046+ let first_range = IpRange :: try_from ( (
1047+ Ipv4Addr :: new ( 10 , 0 , 0 , 1 ) ,
1048+ Ipv4Addr :: new ( 10 , 0 , 0 , 3 ) ,
1049+ ) )
1050+ . unwrap ( ) ;
1051+ context. create_ip_pool ( "p0" , first_range, None ) . await ;
1052+ let second_range = IpRange :: try_from ( (
1053+ Ipv4Addr :: new ( 10 , 0 , 0 , 4 ) ,
1054+ Ipv4Addr :: new ( 10 , 0 , 0 , 6 ) ,
1055+ ) )
1056+ . unwrap ( ) ;
1057+ context. create_ip_pool ( "p1" , second_range, None ) . await ;
1058+
1059+ // Allocating an address on an instance in the second pool should be
1060+ // respected, even though there are IPs available in the first.
1061+ let instance_id = Uuid :: new_v4 ( ) ;
1062+ let project_id = Uuid :: new_v4 ( ) ;
1063+ let id = Uuid :: new_v4 ( ) ;
1064+ let pool_name = Some ( Name ( "p1" . parse ( ) . unwrap ( ) ) ) ;
1065+
1066+ let ip = context
1067+ . db_datastore
1068+ . allocate_instance_ephemeral_ip (
1069+ & context. opctx ,
1070+ id,
1071+ project_id,
1072+ instance_id,
1073+ pool_name,
1074+ )
1075+ . await
1076+ . expect ( "Failed to allocate instance ephemeral IP address" ) ;
1077+ assert_eq ! ( ip. kind, IpKind :: Ephemeral ) ;
1078+ assert_eq ! ( ip. ip. ip( ) , second_range. first_address( ) ) ;
1079+ assert_eq ! ( ip. first_port. 0 , 0 ) ;
1080+ assert_eq ! ( ip. last_port. 0 , u16 :: MAX ) ;
1081+ assert_eq ! ( ip. project_id, project_id) ;
1082+
1083+ context. success ( ) . await ;
1084+ }
1085+
1086+ #[ tokio:: test]
1087+ async fn test_ensure_pool_exhaustion_does_not_use_other_pool ( ) {
1088+ let context = TestContext :: new (
1089+ "test_ensure_pool_exhaustion_does_not_use_other_pool" ,
1090+ )
1091+ . await ;
1092+
1093+ // Create two pools, neither project-restricted.
1094+ let first_range = IpRange :: try_from ( (
1095+ Ipv4Addr :: new ( 10 , 0 , 0 , 1 ) ,
1096+ Ipv4Addr :: new ( 10 , 0 , 0 , 3 ) ,
1097+ ) )
1098+ . unwrap ( ) ;
1099+ context. create_ip_pool ( "p0" , first_range, None ) . await ;
1100+ let first_address = Ipv4Addr :: new ( 10 , 0 , 0 , 4 ) ;
1101+ let last_address = Ipv4Addr :: new ( 10 , 0 , 0 , 6 ) ;
1102+ let second_range =
1103+ IpRange :: try_from ( ( first_address, last_address) ) . unwrap ( ) ;
1104+ context. create_ip_pool ( "p1" , second_range, None ) . await ;
1105+
1106+ // Allocate all available addresses in the second pool.
1107+ let instance_id = Uuid :: new_v4 ( ) ;
1108+ let project_id = Uuid :: new_v4 ( ) ;
1109+ let pool_name = Some ( Name ( "p1" . parse ( ) . unwrap ( ) ) ) ;
1110+ let first_octet = first_address. octets ( ) [ 3 ] ;
1111+ let last_octet = last_address. octets ( ) [ 3 ] ;
1112+ for octet in first_octet..=last_octet {
1113+ let ip = context
1114+ . db_datastore
1115+ . allocate_instance_ephemeral_ip (
1116+ & context. opctx ,
1117+ Uuid :: new_v4 ( ) ,
1118+ project_id,
1119+ instance_id,
1120+ pool_name. clone ( ) ,
1121+ )
1122+ . await
1123+ . expect ( "Failed to allocate instance ephemeral IP address" ) ;
1124+ println ! ( "{ip:#?}" ) ;
1125+ if let IpAddr :: V4 ( addr) = ip. ip . ip ( ) {
1126+ assert_eq ! ( addr. octets( ) [ 3 ] , octet) ;
1127+ } else {
1128+ panic ! ( "Expected an IPv4 address" ) ;
1129+ }
1130+ }
1131+
1132+ // Allocating another address should _fail_, and not use the first pool.
1133+ context
1134+ . db_datastore
1135+ . allocate_instance_ephemeral_ip (
1136+ & context. opctx ,
1137+ Uuid :: new_v4 ( ) ,
1138+ project_id,
1139+ instance_id,
1140+ pool_name,
1141+ )
1142+ . await
1143+ . expect_err ( "Should not use IP addresses from a different pool" ) ;
1144+
1145+ context. success ( ) . await ;
1146+ }
10021147}
0 commit comments