1use crate::state::{
23 Ec2State, Image, InternetGateway, NetworkAcl, NetworkAclAssoc, NetworkAclEntry, Route,
24 RouteTable, 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
93type AmiSeed = (
113 &'static str,
114 &'static str,
115 &'static str,
116 &'static str,
117 &'static str,
118 Option<&'static str>,
119 &'static str,
120 &'static str,
121 Option<&'static str>,
122);
123
124pub(crate) fn seed_public_images(state: &mut Ec2State) {
125 const AMAZON: &str = "137112412989";
126 const CANONICAL: &str = "099720109477";
127 const AMAZON_WINDOWS: &str = "801119661308";
128 let seeds: &[AmiSeed] = &[
129 (
137 "ami-0a1b2c3d4e5f60001",
138 "amzn2-ami-minimal-hvm-2.0.20240306.2-x86_64-ebs",
139 "Amazon Linux 2 AMI 2.0.20240306.2 x86_64 Minimal HVM ebs",
140 "x86_64",
141 AMAZON,
142 Some("amazon"),
143 "2024-03-06T12:00:00.000Z",
144 "/dev/xvda",
145 None,
146 ),
147 (
148 "ami-0a1b2c3d4e5f60007",
149 "amzn2-ami-minimal-hvm-2.0.20240306.2-arm64-ebs",
150 "Amazon Linux 2 AMI 2.0.20240306.2 arm64 Minimal HVM ebs",
151 "arm64",
152 AMAZON,
153 Some("amazon"),
154 "2024-03-06T12:00:00.000Z",
155 "/dev/xvda",
156 None,
157 ),
158 (
159 "ami-0a1b2c3d4e5f60002",
160 "al2023-ami-2023.4.20240319.1-kernel-6.1-x86_64",
161 "Amazon Linux 2023 AMI 2023.4.20240319.1 x86_64 HVM kernel-6.1",
162 "x86_64",
163 AMAZON,
164 Some("amazon"),
165 "2024-03-19T12:00:00.000Z",
166 "/dev/xvda",
167 None,
168 ),
169 (
170 "ami-0a1b2c3d4e5f60003",
171 "al2023-ami-2023.4.20240319.1-kernel-6.1-arm64",
172 "Amazon Linux 2023 AMI 2023.4.20240319.1 arm64 HVM kernel-6.1",
173 "arm64",
174 AMAZON,
175 Some("amazon"),
176 "2024-03-19T12:00:00.000Z",
177 "/dev/xvda",
178 None,
179 ),
180 (
181 "ami-0a1b2c3d4e5f60004",
182 "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20240319",
183 "Canonical, Ubuntu, 22.04 LTS, amd64 jammy image build on 2024-03-19",
184 "x86_64",
185 CANONICAL,
186 None,
187 "2024-03-19T06:00:00.000Z",
188 "/dev/sda1",
189 None,
190 ),
191 (
192 "ami-0a1b2c3d4e5f60005",
193 "ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-20240423",
194 "Canonical, Ubuntu, 24.04 LTS, amd64 noble image build on 2024-04-23",
195 "x86_64",
196 CANONICAL,
197 None,
198 "2024-04-23T06:00:00.000Z",
199 "/dev/sda1",
200 None,
201 ),
202 (
206 "ami-0a1b2c3d4e5f60008",
207 "ubuntu/images/hvm-instance/ubuntu-jammy-22.04-amd64-server-20240319",
208 "Canonical, Ubuntu, 22.04 LTS, amd64 jammy instance-store image build on 2024-03-19",
209 "x86_64",
210 CANONICAL,
211 None,
212 "2024-03-19T05:00:00.000Z",
213 "/dev/sda1",
214 None,
215 ),
216 (
217 "ami-0a1b2c3d4e5f60006",
218 "Windows_Server-2022-English-Full-Base-2024.03.13",
219 "Microsoft Windows Server 2022 Full Locale English AMI provided by Amazon",
220 "x86_64",
221 AMAZON_WINDOWS,
222 Some("amazon"),
223 "2024-03-13T12:00:00.000Z",
224 "/dev/sda1",
225 Some("windows"),
226 ),
227 ];
228 for (id, name, desc, arch, owner, alias, created, root_dev, platform) in seeds {
229 state.images.insert(
230 (*id).to_string(),
231 Image {
232 image_id: (*id).to_string(),
233 name: (*name).to_string(),
234 description: (*desc).to_string(),
235 state: "available".to_string(),
236 architecture: (*arch).to_string(),
237 public: true,
238 source_instance_id: None,
239 in_recycle_bin: false,
240 deprecation_time: None,
241 deregistration_protection: false,
242 launch_permission_users: Vec::new(),
243 launch_permission_groups: vec!["all".to_string()],
244 boot_mode: None,
245 owner_id: Some((*owner).to_string()),
246 owner_alias: alias.map(str::to_string),
247 creation_date: Some((*created).to_string()),
248 root_device_name: Some((*root_dev).to_string()),
249 platform: platform.map(str::to_string),
250 },
251 );
252 }
253}
254
255pub(crate) fn bootstrap_default_network(state: &mut Ec2State) {
256 let account = state.account_id.clone();
257 let region = if state.region.is_empty() {
258 "us-east-1".to_string()
259 } else {
260 state.region.clone()
261 };
262
263 let vpc_id = default_vpc_id(&account);
264 let igw_id = deterministic_id("igw", &account, "default-igw");
265 let rtb_id = deterministic_id("rtb", &account, "default-rtb");
266 let acl_id = deterministic_id("acl", &account, "default-acl");
267 let sg_id = default_security_group_id(&account);
268
269 state.vpcs.insert(
271 vpc_id.clone(),
272 Vpc {
273 vpc_id: vpc_id.clone(),
274 cidr_block: DEFAULT_VPC_CIDR.to_string(),
275 state: "available".to_string(),
276 dhcp_options_id: "default".to_string(),
277 instance_tenancy: "default".to_string(),
278 is_default: true,
279 enable_dns_support: true,
280 enable_dns_hostnames: true,
281 cidr_associations: Vec::new(),
282 ipv6_cidr_block: None,
283 },
284 );
285
286 state.internet_gateways.insert(
288 igw_id.clone(),
289 InternetGateway {
290 internet_gateway_id: igw_id.clone(),
291 attachments: vec![(vpc_id.clone(), "available".to_string())],
292 },
293 );
294
295 let az_prefix = az_id_prefix(®ion);
297 let mut subnet_ids = Vec::new();
298 for (idx, suffix) in DEFAULT_AZ_SUFFIXES.iter().enumerate() {
299 let subnet_id = deterministic_id("subnet", &account, &format!("default-subnet-{suffix}"));
300 let az = format!("{region}{suffix}");
301 state.subnets.insert(
302 subnet_id.clone(),
303 Subnet {
304 subnet_id: subnet_id.clone(),
305 vpc_id: vpc_id.clone(),
306 cidr_block: format!("172.31.{}.0/20", idx * 16),
308 availability_zone: az,
309 availability_zone_id: format!("{az_prefix}-az{}", idx + 1),
310 state: "available".to_string(),
311 available_ip_address_count: 4091,
312 default_for_az: true,
313 map_public_ip_on_launch: true,
315 assign_ipv6_address_on_creation: false,
316 map_customer_owned_ip_on_launch: false,
317 enable_dns64: false,
318 private_dns_hostname_type: "ip-name".to_string(),
319 ipv6_cidr_block: None,
320 },
321 );
322 subnet_ids.push(subnet_id);
323 }
324
325 let mut associations = vec![RouteTableAssociation {
327 association_id: deterministic_id("rtbassoc", &account, "default-rtb-main"),
328 route_table_id: rtb_id.clone(),
329 subnet_id: None,
330 gateway_id: None,
331 main: true,
332 }];
333 for sid in &subnet_ids {
334 associations.push(RouteTableAssociation {
335 association_id: deterministic_id("rtbassoc", &account, &format!("default-rtb-{sid}")),
336 route_table_id: rtb_id.clone(),
337 subnet_id: Some(sid.clone()),
338 gateway_id: None,
339 main: false,
340 });
341 }
342 state.route_tables.insert(
343 rtb_id.clone(),
344 RouteTable {
345 route_table_id: rtb_id.clone(),
346 vpc_id: vpc_id.clone(),
347 routes: vec![
348 Route {
349 destination_cidr_block: Some(DEFAULT_VPC_CIDR.to_string()),
350 gateway_id: Some("local".to_string()),
351 ..Default::default()
352 },
353 Route {
354 destination_cidr_block: Some("0.0.0.0/0".to_string()),
355 gateway_id: Some(igw_id.clone()),
356 ..Default::default()
357 },
358 ],
359 associations,
360 },
361 );
362
363 state.security_groups.insert(
365 sg_id.clone(),
366 SecurityGroup {
367 group_id: sg_id.clone(),
368 group_name: "default".to_string(),
369 description: "default VPC security group".to_string(),
370 vpc_id: vpc_id.clone(),
371 rules: vec![
372 SecurityGroupRule {
373 rule_id: deterministic_id("sgr", &account, "default-sg-ingress"),
374 group_id: sg_id.clone(),
375 is_egress: false,
376 ip_protocol: "-1".to_string(),
377 from_port: -1,
378 to_port: -1,
379 cidr_ipv4: None,
380 cidr_ipv6: None,
381 prefix_list_id: None,
382 referenced_group_id: Some(sg_id.clone()),
383 referenced_group_name: None,
384 referenced_user_id: None,
385 description: String::new(),
386 },
387 SecurityGroupRule {
388 rule_id: deterministic_id("sgr", &account, "default-sg-egress"),
389 group_id: sg_id.clone(),
390 is_egress: true,
391 ip_protocol: "-1".to_string(),
392 from_port: -1,
393 to_port: -1,
394 cidr_ipv4: Some("0.0.0.0/0".to_string()),
395 cidr_ipv6: None,
396 prefix_list_id: None,
397 referenced_group_id: None,
398 referenced_group_name: None,
399 referenced_user_id: None,
400 description: String::new(),
401 },
402 ],
403 },
404 );
405
406 let nacl_associations = subnet_ids
408 .iter()
409 .map(|sid| NetworkAclAssoc {
410 association_id: deterministic_id("aclassoc", &account, &format!("default-acl-{sid}")),
411 subnet_id: sid.clone(),
412 })
413 .collect();
414 state.network_acls.insert(
415 acl_id.clone(),
416 NetworkAcl {
417 network_acl_id: acl_id.clone(),
418 vpc_id: vpc_id.clone(),
419 is_default: true,
420 entries: vec![
421 allow_all_entry(false),
422 deny_all_entry(false),
423 allow_all_entry(true),
424 deny_all_entry(true),
425 ],
426 associations: nacl_associations,
427 },
428 );
429}
430
431pub(crate) fn create_vpc_default_resources(state: &mut Ec2State, vpc_id: &str, cidr: &str) {
438 let account = state.account_id.clone();
439 let rtb_id = deterministic_id("rtb", &account, &format!("{vpc_id}-main-rtb"));
440 let acl_id = deterministic_id("acl", &account, &format!("{vpc_id}-default-acl"));
441 let sg_id = deterministic_id("sg", &account, &format!("{vpc_id}-default-sg"));
442
443 state
444 .route_tables
445 .entry(rtb_id.clone())
446 .or_insert_with(|| RouteTable {
447 route_table_id: rtb_id.clone(),
448 vpc_id: vpc_id.to_string(),
449 routes: vec![Route {
450 destination_cidr_block: Some(cidr.to_string()),
451 gateway_id: Some("local".to_string()),
452 ..Default::default()
453 }],
454 associations: vec![RouteTableAssociation {
455 association_id: deterministic_id("rtbassoc", &account, &format!("{vpc_id}-main")),
456 route_table_id: rtb_id.clone(),
457 subnet_id: None,
458 gateway_id: None,
459 main: true,
460 }],
461 });
462
463 state
464 .security_groups
465 .entry(sg_id.clone())
466 .or_insert_with(|| SecurityGroup {
467 group_id: sg_id.clone(),
468 group_name: "default".to_string(),
469 description: "default VPC security group".to_string(),
470 vpc_id: vpc_id.to_string(),
471 rules: vec![
472 SecurityGroupRule {
473 rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-ingress")),
474 group_id: sg_id.clone(),
475 is_egress: false,
476 ip_protocol: "-1".to_string(),
477 from_port: -1,
478 to_port: -1,
479 cidr_ipv4: None,
480 cidr_ipv6: None,
481 prefix_list_id: None,
482 referenced_group_id: Some(sg_id.clone()),
483 referenced_group_name: None,
484 referenced_user_id: None,
485 description: String::new(),
486 },
487 SecurityGroupRule {
488 rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-egress")),
489 group_id: sg_id.clone(),
490 is_egress: true,
491 ip_protocol: "-1".to_string(),
492 from_port: -1,
493 to_port: -1,
494 cidr_ipv4: Some("0.0.0.0/0".to_string()),
495 cidr_ipv6: None,
496 prefix_list_id: None,
497 referenced_group_id: None,
498 referenced_group_name: None,
499 referenced_user_id: None,
500 description: String::new(),
501 },
502 ],
503 });
504
505 state
506 .network_acls
507 .entry(acl_id.clone())
508 .or_insert_with(|| NetworkAcl {
509 network_acl_id: acl_id.clone(),
510 vpc_id: vpc_id.to_string(),
511 is_default: true,
512 entries: vec![
513 allow_all_entry(false),
514 deny_all_entry(false),
515 allow_all_entry(true),
516 deny_all_entry(true),
517 ],
518 associations: Vec::new(),
519 });
520}
521
522fn allow_all_entry(egress: bool) -> NetworkAclEntry {
523 NetworkAclEntry {
524 rule_number: 100,
525 protocol: "-1".to_string(),
526 rule_action: "allow".to_string(),
527 egress,
528 cidr_block: Some("0.0.0.0/0".to_string()),
529 ipv6_cidr_block: None,
530 port_range: None,
531 icmp_type_code: None,
532 }
533}
534
535fn deny_all_entry(egress: bool) -> NetworkAclEntry {
536 NetworkAclEntry {
537 rule_number: 32767,
538 protocol: "-1".to_string(),
539 rule_action: "deny".to_string(),
540 egress,
541 cidr_block: Some("0.0.0.0/0".to_string()),
542 ipv6_cidr_block: None,
543 port_range: None,
544 icmp_type_code: None,
545 }
546}
547
548pub(crate) fn subnet_is_public(state: &Ec2State, subnet_id: &str) -> bool {
556 let Some(subnet) = state.subnets.get(subnet_id) else {
557 return false;
558 };
559 let explicit = state.route_tables.values().find(|rt| {
561 rt.associations
562 .iter()
563 .any(|a| a.subnet_id.as_deref() == Some(subnet_id))
564 });
565 let main = state
566 .route_tables
567 .values()
568 .find(|rt| rt.vpc_id == subnet.vpc_id && rt.associations.iter().any(|a| a.main));
569 let rt = explicit.or(main);
570 rt.map(route_table_has_igw_default).unwrap_or(false)
571}
572
573fn route_table_has_igw_default(rt: &RouteTable) -> bool {
574 rt.routes.iter().any(|r| {
575 r.destination_cidr_block.as_deref() == Some("0.0.0.0/0")
576 && r.gateway_id
577 .as_deref()
578 .map(|g| g.starts_with("igw-"))
579 .unwrap_or(false)
580 })
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use crate::state::Ec2State;
587
588 #[test]
589 fn deterministic_id_is_stable_and_shaped() {
590 let a = deterministic_id("vpc", "123456789012", "default-vpc");
591 let b = deterministic_id("vpc", "123456789012", "default-vpc");
592 assert_eq!(a, b);
593 assert!(a.starts_with("vpc-"));
594 let hex = a.strip_prefix("vpc-").unwrap();
596 assert_eq!(hex.len(), 17);
597 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
598 }
599
600 #[test]
601 fn deterministic_id_varies_by_account_and_role() {
602 let base = deterministic_id("vpc", "111111111111", "default-vpc");
603 assert_ne!(base, deterministic_id("vpc", "222222222222", "default-vpc"));
604 assert_ne!(base, deterministic_id("vpc", "111111111111", "default-igw"));
605 }
606
607 #[test]
608 fn deterministic_id_is_region_independent() {
609 assert_eq!(
614 default_vpc_id("111111111111"),
615 deterministic_id("vpc", "111111111111", "default-vpc")
616 );
617 }
618
619 #[test]
620 fn az_id_prefix_matches_aws_shape() {
621 assert_eq!(az_id_prefix("us-east-1"), "use1");
622 assert_eq!(az_id_prefix("eu-west-2"), "euw2");
623 assert_eq!(az_id_prefix("ap-southeast-1"), "aps1");
624 }
625
626 #[test]
627 fn bootstrap_creates_full_default_topology() {
628 let state = Ec2State::new("123456789012", "us-east-1");
629 assert_eq!(state.vpcs.len(), 1);
631 let vpc = state.vpcs.values().next().unwrap();
632 assert!(vpc.is_default);
633 assert_eq!(vpc.cidr_block, "172.31.0.0/16");
634 assert_eq!(state.subnets.len(), DEFAULT_AZ_SUFFIXES.len());
636 assert!(state.subnets.values().all(|s| s.default_for_az));
637 assert!(state.subnets.values().all(|s| s.map_public_ip_on_launch));
638 assert_eq!(state.internet_gateways.len(), 1);
640 let igw = state.internet_gateways.values().next().unwrap();
641 assert_eq!(igw.attachments[0].0, vpc.vpc_id);
642 let sg = state.security_groups.values().next().unwrap();
644 assert_eq!(sg.group_name, "default");
645 assert!(state.network_acls.values().next().unwrap().is_default);
646 }
647
648 #[test]
649 fn default_subnets_are_public() {
650 let state = Ec2State::new("123456789012", "us-east-1");
651 for sid in state.subnets.keys() {
652 assert!(
653 subnet_is_public(&state, sid),
654 "subnet {sid} should be public"
655 );
656 }
657 }
658
659 #[test]
660 fn ids_match_across_fresh_states() {
661 let a = Ec2State::new("123456789012", "us-east-1");
664 let b = Ec2State::new("123456789012", "us-east-1");
665 let a_vpc: Vec<_> = a.vpcs.keys().collect();
666 let b_vpc: Vec<_> = b.vpcs.keys().collect();
667 assert_eq!(a_vpc, b_vpc);
668 assert_eq!(a_vpc[0], &default_vpc_id("123456789012"));
669 }
670
671 #[test]
672 fn default_vpc_id_agrees_across_regions() {
673 let read_path = Ec2State::new("123456789012", "eu-west-1");
677 let persisted = Ec2State::new("123456789012", "us-east-1");
678 let read_vpc = read_path.vpcs.keys().next().unwrap();
679 let persisted_vpc = persisted.vpcs.keys().next().unwrap();
680 assert_eq!(read_vpc, persisted_vpc);
681 let read_subnets: std::collections::BTreeSet<_> = read_path.subnets.keys().collect();
684 let persisted_subnets: std::collections::BTreeSet<_> = persisted.subnets.keys().collect();
685 assert_eq!(read_subnets, persisted_subnets);
686 }
687}