1use crate::state::{
23 Ec2State, InternetGateway, NetworkAcl, NetworkAclAssoc, NetworkAclEntry, Route, RouteTable,
24 RouteTableAssociation, SecurityGroup, SecurityGroupRule, Subnet, Vpc,
25};
26
27const DEFAULT_VPC_CIDR: &str = "172.31.0.0/16";
29
30const DEFAULT_AZ_SUFFIXES: [&str; 3] = ["a", "b", "c"];
34
35pub(crate) fn deterministic_id(prefix: &str, account: &str, role: &str) -> String {
47 let seed = format!("{account}/{role}");
48 let h1 = fnv1a64(seed.as_bytes());
49 let h2 = fnv1a64(format!("{seed}/salt").as_bytes());
50 format!("{prefix}-{:016x}{:01x}", h1, h2 & 0xf)
53}
54
55fn fnv1a64(bytes: &[u8]) -> u64 {
58 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
59 for b in bytes {
60 h ^= u64::from(*b);
61 h = h.wrapping_mul(0x0000_0100_0000_01b3);
62 }
63 h
64}
65
66fn az_id_prefix(region: &str) -> String {
69 let parts: Vec<&str> = region.split('-').collect();
70 if parts.len() == 3 && !parts[1].is_empty() {
71 format!(
72 "{}{}{}",
73 parts[0],
74 parts[1].chars().next().unwrap_or('x'),
75 parts[2]
76 )
77 } else {
78 region.replace('-', "")
79 }
80}
81
82pub(crate) fn default_vpc_id(account: &str) -> String {
85 deterministic_id("vpc", account, "default-vpc")
86}
87
88pub(crate) fn default_security_group_id(account: &str) -> String {
90 deterministic_id("sg", account, "default-sg")
91}
92
93pub(crate) fn bootstrap_default_network(state: &mut Ec2State) {
97 let account = state.account_id.clone();
98 let region = if state.region.is_empty() {
99 "us-east-1".to_string()
100 } else {
101 state.region.clone()
102 };
103
104 let vpc_id = default_vpc_id(&account);
105 let igw_id = deterministic_id("igw", &account, "default-igw");
106 let rtb_id = deterministic_id("rtb", &account, "default-rtb");
107 let acl_id = deterministic_id("acl", &account, "default-acl");
108 let sg_id = default_security_group_id(&account);
109
110 state.vpcs.insert(
112 vpc_id.clone(),
113 Vpc {
114 vpc_id: vpc_id.clone(),
115 cidr_block: DEFAULT_VPC_CIDR.to_string(),
116 state: "available".to_string(),
117 dhcp_options_id: "default".to_string(),
118 instance_tenancy: "default".to_string(),
119 is_default: true,
120 enable_dns_support: true,
121 enable_dns_hostnames: true,
122 cidr_associations: Vec::new(),
123 ipv6_cidr_block: None,
124 },
125 );
126
127 state.internet_gateways.insert(
129 igw_id.clone(),
130 InternetGateway {
131 internet_gateway_id: igw_id.clone(),
132 attachments: vec![(vpc_id.clone(), "available".to_string())],
133 },
134 );
135
136 let az_prefix = az_id_prefix(®ion);
138 let mut subnet_ids = Vec::new();
139 for (idx, suffix) in DEFAULT_AZ_SUFFIXES.iter().enumerate() {
140 let subnet_id = deterministic_id("subnet", &account, &format!("default-subnet-{suffix}"));
141 let az = format!("{region}{suffix}");
142 state.subnets.insert(
143 subnet_id.clone(),
144 Subnet {
145 subnet_id: subnet_id.clone(),
146 vpc_id: vpc_id.clone(),
147 cidr_block: format!("172.31.{}.0/20", idx * 16),
149 availability_zone: az,
150 availability_zone_id: format!("{az_prefix}-az{}", idx + 1),
151 state: "available".to_string(),
152 available_ip_address_count: 4091,
153 default_for_az: true,
154 map_public_ip_on_launch: true,
156 assign_ipv6_address_on_creation: false,
157 map_customer_owned_ip_on_launch: false,
158 enable_dns64: false,
159 private_dns_hostname_type: "ip-name".to_string(),
160 ipv6_cidr_block: None,
161 },
162 );
163 subnet_ids.push(subnet_id);
164 }
165
166 let mut associations = vec![RouteTableAssociation {
168 association_id: deterministic_id("rtbassoc", &account, "default-rtb-main"),
169 route_table_id: rtb_id.clone(),
170 subnet_id: None,
171 gateway_id: None,
172 main: true,
173 }];
174 for sid in &subnet_ids {
175 associations.push(RouteTableAssociation {
176 association_id: deterministic_id("rtbassoc", &account, &format!("default-rtb-{sid}")),
177 route_table_id: rtb_id.clone(),
178 subnet_id: Some(sid.clone()),
179 gateway_id: None,
180 main: false,
181 });
182 }
183 state.route_tables.insert(
184 rtb_id.clone(),
185 RouteTable {
186 route_table_id: rtb_id.clone(),
187 vpc_id: vpc_id.clone(),
188 routes: vec![
189 Route {
190 destination_cidr_block: Some(DEFAULT_VPC_CIDR.to_string()),
191 gateway_id: Some("local".to_string()),
192 ..Default::default()
193 },
194 Route {
195 destination_cidr_block: Some("0.0.0.0/0".to_string()),
196 gateway_id: Some(igw_id.clone()),
197 ..Default::default()
198 },
199 ],
200 associations,
201 },
202 );
203
204 state.security_groups.insert(
206 sg_id.clone(),
207 SecurityGroup {
208 group_id: sg_id.clone(),
209 group_name: "default".to_string(),
210 description: "default VPC security group".to_string(),
211 vpc_id: vpc_id.clone(),
212 rules: vec![
213 SecurityGroupRule {
214 rule_id: deterministic_id("sgr", &account, "default-sg-ingress"),
215 group_id: sg_id.clone(),
216 is_egress: false,
217 ip_protocol: "-1".to_string(),
218 from_port: -1,
219 to_port: -1,
220 cidr_ipv4: None,
221 cidr_ipv6: None,
222 prefix_list_id: None,
223 referenced_group_id: Some(sg_id.clone()),
224 description: String::new(),
225 },
226 SecurityGroupRule {
227 rule_id: deterministic_id("sgr", &account, "default-sg-egress"),
228 group_id: sg_id.clone(),
229 is_egress: true,
230 ip_protocol: "-1".to_string(),
231 from_port: -1,
232 to_port: -1,
233 cidr_ipv4: Some("0.0.0.0/0".to_string()),
234 cidr_ipv6: None,
235 prefix_list_id: None,
236 referenced_group_id: None,
237 description: String::new(),
238 },
239 ],
240 },
241 );
242
243 let nacl_associations = subnet_ids
245 .iter()
246 .map(|sid| NetworkAclAssoc {
247 association_id: deterministic_id("aclassoc", &account, &format!("default-acl-{sid}")),
248 subnet_id: sid.clone(),
249 })
250 .collect();
251 state.network_acls.insert(
252 acl_id.clone(),
253 NetworkAcl {
254 network_acl_id: acl_id.clone(),
255 vpc_id: vpc_id.clone(),
256 is_default: true,
257 entries: vec![
258 allow_all_entry(false),
259 deny_all_entry(false),
260 allow_all_entry(true),
261 deny_all_entry(true),
262 ],
263 associations: nacl_associations,
264 },
265 );
266}
267
268pub(crate) fn create_vpc_default_resources(state: &mut Ec2State, vpc_id: &str, cidr: &str) {
275 let account = state.account_id.clone();
276 let rtb_id = deterministic_id("rtb", &account, &format!("{vpc_id}-main-rtb"));
277 let acl_id = deterministic_id("acl", &account, &format!("{vpc_id}-default-acl"));
278 let sg_id = deterministic_id("sg", &account, &format!("{vpc_id}-default-sg"));
279
280 state
281 .route_tables
282 .entry(rtb_id.clone())
283 .or_insert_with(|| RouteTable {
284 route_table_id: rtb_id.clone(),
285 vpc_id: vpc_id.to_string(),
286 routes: vec![Route {
287 destination_cidr_block: Some(cidr.to_string()),
288 gateway_id: Some("local".to_string()),
289 ..Default::default()
290 }],
291 associations: vec![RouteTableAssociation {
292 association_id: deterministic_id("rtbassoc", &account, &format!("{vpc_id}-main")),
293 route_table_id: rtb_id.clone(),
294 subnet_id: None,
295 gateway_id: None,
296 main: true,
297 }],
298 });
299
300 state
301 .security_groups
302 .entry(sg_id.clone())
303 .or_insert_with(|| SecurityGroup {
304 group_id: sg_id.clone(),
305 group_name: "default".to_string(),
306 description: "default VPC security group".to_string(),
307 vpc_id: vpc_id.to_string(),
308 rules: vec![
309 SecurityGroupRule {
310 rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-ingress")),
311 group_id: sg_id.clone(),
312 is_egress: false,
313 ip_protocol: "-1".to_string(),
314 from_port: -1,
315 to_port: -1,
316 cidr_ipv4: None,
317 cidr_ipv6: None,
318 prefix_list_id: None,
319 referenced_group_id: Some(sg_id.clone()),
320 description: String::new(),
321 },
322 SecurityGroupRule {
323 rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-egress")),
324 group_id: sg_id.clone(),
325 is_egress: true,
326 ip_protocol: "-1".to_string(),
327 from_port: -1,
328 to_port: -1,
329 cidr_ipv4: Some("0.0.0.0/0".to_string()),
330 cidr_ipv6: None,
331 prefix_list_id: None,
332 referenced_group_id: None,
333 description: String::new(),
334 },
335 ],
336 });
337
338 state
339 .network_acls
340 .entry(acl_id.clone())
341 .or_insert_with(|| NetworkAcl {
342 network_acl_id: acl_id.clone(),
343 vpc_id: vpc_id.to_string(),
344 is_default: true,
345 entries: vec![
346 allow_all_entry(false),
347 deny_all_entry(false),
348 allow_all_entry(true),
349 deny_all_entry(true),
350 ],
351 associations: Vec::new(),
352 });
353}
354
355fn allow_all_entry(egress: bool) -> NetworkAclEntry {
356 NetworkAclEntry {
357 rule_number: 100,
358 protocol: "-1".to_string(),
359 rule_action: "allow".to_string(),
360 egress,
361 cidr_block: Some("0.0.0.0/0".to_string()),
362 ipv6_cidr_block: None,
363 port_range: None,
364 icmp_type_code: None,
365 }
366}
367
368fn deny_all_entry(egress: bool) -> NetworkAclEntry {
369 NetworkAclEntry {
370 rule_number: 32767,
371 protocol: "-1".to_string(),
372 rule_action: "deny".to_string(),
373 egress,
374 cidr_block: Some("0.0.0.0/0".to_string()),
375 ipv6_cidr_block: None,
376 port_range: None,
377 icmp_type_code: None,
378 }
379}
380
381pub(crate) fn subnet_is_public(state: &Ec2State, subnet_id: &str) -> bool {
389 let Some(subnet) = state.subnets.get(subnet_id) else {
390 return false;
391 };
392 let explicit = state.route_tables.values().find(|rt| {
394 rt.associations
395 .iter()
396 .any(|a| a.subnet_id.as_deref() == Some(subnet_id))
397 });
398 let main = state
399 .route_tables
400 .values()
401 .find(|rt| rt.vpc_id == subnet.vpc_id && rt.associations.iter().any(|a| a.main));
402 let rt = explicit.or(main);
403 rt.map(route_table_has_igw_default).unwrap_or(false)
404}
405
406fn route_table_has_igw_default(rt: &RouteTable) -> bool {
407 rt.routes.iter().any(|r| {
408 r.destination_cidr_block.as_deref() == Some("0.0.0.0/0")
409 && r.gateway_id
410 .as_deref()
411 .map(|g| g.starts_with("igw-"))
412 .unwrap_or(false)
413 })
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::state::Ec2State;
420
421 #[test]
422 fn deterministic_id_is_stable_and_shaped() {
423 let a = deterministic_id("vpc", "123456789012", "default-vpc");
424 let b = deterministic_id("vpc", "123456789012", "default-vpc");
425 assert_eq!(a, b);
426 assert!(a.starts_with("vpc-"));
427 let hex = a.strip_prefix("vpc-").unwrap();
429 assert_eq!(hex.len(), 17);
430 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
431 }
432
433 #[test]
434 fn deterministic_id_varies_by_account_and_role() {
435 let base = deterministic_id("vpc", "111111111111", "default-vpc");
436 assert_ne!(base, deterministic_id("vpc", "222222222222", "default-vpc"));
437 assert_ne!(base, deterministic_id("vpc", "111111111111", "default-igw"));
438 }
439
440 #[test]
441 fn deterministic_id_is_region_independent() {
442 assert_eq!(
447 default_vpc_id("111111111111"),
448 deterministic_id("vpc", "111111111111", "default-vpc")
449 );
450 }
451
452 #[test]
453 fn az_id_prefix_matches_aws_shape() {
454 assert_eq!(az_id_prefix("us-east-1"), "use1");
455 assert_eq!(az_id_prefix("eu-west-2"), "euw2");
456 assert_eq!(az_id_prefix("ap-southeast-1"), "aps1");
457 }
458
459 #[test]
460 fn bootstrap_creates_full_default_topology() {
461 let state = Ec2State::new("123456789012", "us-east-1");
462 assert_eq!(state.vpcs.len(), 1);
464 let vpc = state.vpcs.values().next().unwrap();
465 assert!(vpc.is_default);
466 assert_eq!(vpc.cidr_block, "172.31.0.0/16");
467 assert_eq!(state.subnets.len(), DEFAULT_AZ_SUFFIXES.len());
469 assert!(state.subnets.values().all(|s| s.default_for_az));
470 assert!(state.subnets.values().all(|s| s.map_public_ip_on_launch));
471 assert_eq!(state.internet_gateways.len(), 1);
473 let igw = state.internet_gateways.values().next().unwrap();
474 assert_eq!(igw.attachments[0].0, vpc.vpc_id);
475 let sg = state.security_groups.values().next().unwrap();
477 assert_eq!(sg.group_name, "default");
478 assert!(state.network_acls.values().next().unwrap().is_default);
479 }
480
481 #[test]
482 fn default_subnets_are_public() {
483 let state = Ec2State::new("123456789012", "us-east-1");
484 for sid in state.subnets.keys() {
485 assert!(
486 subnet_is_public(&state, sid),
487 "subnet {sid} should be public"
488 );
489 }
490 }
491
492 #[test]
493 fn ids_match_across_fresh_states() {
494 let a = Ec2State::new("123456789012", "us-east-1");
497 let b = Ec2State::new("123456789012", "us-east-1");
498 let a_vpc: Vec<_> = a.vpcs.keys().collect();
499 let b_vpc: Vec<_> = b.vpcs.keys().collect();
500 assert_eq!(a_vpc, b_vpc);
501 assert_eq!(a_vpc[0], &default_vpc_id("123456789012"));
502 }
503
504 #[test]
505 fn default_vpc_id_agrees_across_regions() {
506 let read_path = Ec2State::new("123456789012", "eu-west-1");
510 let persisted = Ec2State::new("123456789012", "us-east-1");
511 let read_vpc = read_path.vpcs.keys().next().unwrap();
512 let persisted_vpc = persisted.vpcs.keys().next().unwrap();
513 assert_eq!(read_vpc, persisted_vpc);
514 let read_subnets: std::collections::BTreeSet<_> = read_path.subnets.keys().collect();
517 let persisted_subnets: std::collections::BTreeSet<_> = persisted.subnets.keys().collect();
518 assert_eq!(read_subnets, persisted_subnets);
519 }
520}