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 description: String::new(),
384 },
385 SecurityGroupRule {
386 rule_id: deterministic_id("sgr", &account, "default-sg-egress"),
387 group_id: sg_id.clone(),
388 is_egress: true,
389 ip_protocol: "-1".to_string(),
390 from_port: -1,
391 to_port: -1,
392 cidr_ipv4: Some("0.0.0.0/0".to_string()),
393 cidr_ipv6: None,
394 prefix_list_id: None,
395 referenced_group_id: None,
396 description: String::new(),
397 },
398 ],
399 },
400 );
401
402 let nacl_associations = subnet_ids
404 .iter()
405 .map(|sid| NetworkAclAssoc {
406 association_id: deterministic_id("aclassoc", &account, &format!("default-acl-{sid}")),
407 subnet_id: sid.clone(),
408 })
409 .collect();
410 state.network_acls.insert(
411 acl_id.clone(),
412 NetworkAcl {
413 network_acl_id: acl_id.clone(),
414 vpc_id: vpc_id.clone(),
415 is_default: true,
416 entries: vec![
417 allow_all_entry(false),
418 deny_all_entry(false),
419 allow_all_entry(true),
420 deny_all_entry(true),
421 ],
422 associations: nacl_associations,
423 },
424 );
425}
426
427pub(crate) fn create_vpc_default_resources(state: &mut Ec2State, vpc_id: &str, cidr: &str) {
434 let account = state.account_id.clone();
435 let rtb_id = deterministic_id("rtb", &account, &format!("{vpc_id}-main-rtb"));
436 let acl_id = deterministic_id("acl", &account, &format!("{vpc_id}-default-acl"));
437 let sg_id = deterministic_id("sg", &account, &format!("{vpc_id}-default-sg"));
438
439 state
440 .route_tables
441 .entry(rtb_id.clone())
442 .or_insert_with(|| RouteTable {
443 route_table_id: rtb_id.clone(),
444 vpc_id: vpc_id.to_string(),
445 routes: vec![Route {
446 destination_cidr_block: Some(cidr.to_string()),
447 gateway_id: Some("local".to_string()),
448 ..Default::default()
449 }],
450 associations: vec![RouteTableAssociation {
451 association_id: deterministic_id("rtbassoc", &account, &format!("{vpc_id}-main")),
452 route_table_id: rtb_id.clone(),
453 subnet_id: None,
454 gateway_id: None,
455 main: true,
456 }],
457 });
458
459 state
460 .security_groups
461 .entry(sg_id.clone())
462 .or_insert_with(|| SecurityGroup {
463 group_id: sg_id.clone(),
464 group_name: "default".to_string(),
465 description: "default VPC security group".to_string(),
466 vpc_id: vpc_id.to_string(),
467 rules: vec![
468 SecurityGroupRule {
469 rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-ingress")),
470 group_id: sg_id.clone(),
471 is_egress: false,
472 ip_protocol: "-1".to_string(),
473 from_port: -1,
474 to_port: -1,
475 cidr_ipv4: None,
476 cidr_ipv6: None,
477 prefix_list_id: None,
478 referenced_group_id: Some(sg_id.clone()),
479 description: String::new(),
480 },
481 SecurityGroupRule {
482 rule_id: deterministic_id("sgr", &account, &format!("{vpc_id}-sg-egress")),
483 group_id: sg_id.clone(),
484 is_egress: true,
485 ip_protocol: "-1".to_string(),
486 from_port: -1,
487 to_port: -1,
488 cidr_ipv4: Some("0.0.0.0/0".to_string()),
489 cidr_ipv6: None,
490 prefix_list_id: None,
491 referenced_group_id: None,
492 description: String::new(),
493 },
494 ],
495 });
496
497 state
498 .network_acls
499 .entry(acl_id.clone())
500 .or_insert_with(|| NetworkAcl {
501 network_acl_id: acl_id.clone(),
502 vpc_id: vpc_id.to_string(),
503 is_default: true,
504 entries: vec![
505 allow_all_entry(false),
506 deny_all_entry(false),
507 allow_all_entry(true),
508 deny_all_entry(true),
509 ],
510 associations: Vec::new(),
511 });
512}
513
514fn allow_all_entry(egress: bool) -> NetworkAclEntry {
515 NetworkAclEntry {
516 rule_number: 100,
517 protocol: "-1".to_string(),
518 rule_action: "allow".to_string(),
519 egress,
520 cidr_block: Some("0.0.0.0/0".to_string()),
521 ipv6_cidr_block: None,
522 port_range: None,
523 icmp_type_code: None,
524 }
525}
526
527fn deny_all_entry(egress: bool) -> NetworkAclEntry {
528 NetworkAclEntry {
529 rule_number: 32767,
530 protocol: "-1".to_string(),
531 rule_action: "deny".to_string(),
532 egress,
533 cidr_block: Some("0.0.0.0/0".to_string()),
534 ipv6_cidr_block: None,
535 port_range: None,
536 icmp_type_code: None,
537 }
538}
539
540pub(crate) fn subnet_is_public(state: &Ec2State, subnet_id: &str) -> bool {
548 let Some(subnet) = state.subnets.get(subnet_id) else {
549 return false;
550 };
551 let explicit = state.route_tables.values().find(|rt| {
553 rt.associations
554 .iter()
555 .any(|a| a.subnet_id.as_deref() == Some(subnet_id))
556 });
557 let main = state
558 .route_tables
559 .values()
560 .find(|rt| rt.vpc_id == subnet.vpc_id && rt.associations.iter().any(|a| a.main));
561 let rt = explicit.or(main);
562 rt.map(route_table_has_igw_default).unwrap_or(false)
563}
564
565fn route_table_has_igw_default(rt: &RouteTable) -> bool {
566 rt.routes.iter().any(|r| {
567 r.destination_cidr_block.as_deref() == Some("0.0.0.0/0")
568 && r.gateway_id
569 .as_deref()
570 .map(|g| g.starts_with("igw-"))
571 .unwrap_or(false)
572 })
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use crate::state::Ec2State;
579
580 #[test]
581 fn deterministic_id_is_stable_and_shaped() {
582 let a = deterministic_id("vpc", "123456789012", "default-vpc");
583 let b = deterministic_id("vpc", "123456789012", "default-vpc");
584 assert_eq!(a, b);
585 assert!(a.starts_with("vpc-"));
586 let hex = a.strip_prefix("vpc-").unwrap();
588 assert_eq!(hex.len(), 17);
589 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
590 }
591
592 #[test]
593 fn deterministic_id_varies_by_account_and_role() {
594 let base = deterministic_id("vpc", "111111111111", "default-vpc");
595 assert_ne!(base, deterministic_id("vpc", "222222222222", "default-vpc"));
596 assert_ne!(base, deterministic_id("vpc", "111111111111", "default-igw"));
597 }
598
599 #[test]
600 fn deterministic_id_is_region_independent() {
601 assert_eq!(
606 default_vpc_id("111111111111"),
607 deterministic_id("vpc", "111111111111", "default-vpc")
608 );
609 }
610
611 #[test]
612 fn az_id_prefix_matches_aws_shape() {
613 assert_eq!(az_id_prefix("us-east-1"), "use1");
614 assert_eq!(az_id_prefix("eu-west-2"), "euw2");
615 assert_eq!(az_id_prefix("ap-southeast-1"), "aps1");
616 }
617
618 #[test]
619 fn bootstrap_creates_full_default_topology() {
620 let state = Ec2State::new("123456789012", "us-east-1");
621 assert_eq!(state.vpcs.len(), 1);
623 let vpc = state.vpcs.values().next().unwrap();
624 assert!(vpc.is_default);
625 assert_eq!(vpc.cidr_block, "172.31.0.0/16");
626 assert_eq!(state.subnets.len(), DEFAULT_AZ_SUFFIXES.len());
628 assert!(state.subnets.values().all(|s| s.default_for_az));
629 assert!(state.subnets.values().all(|s| s.map_public_ip_on_launch));
630 assert_eq!(state.internet_gateways.len(), 1);
632 let igw = state.internet_gateways.values().next().unwrap();
633 assert_eq!(igw.attachments[0].0, vpc.vpc_id);
634 let sg = state.security_groups.values().next().unwrap();
636 assert_eq!(sg.group_name, "default");
637 assert!(state.network_acls.values().next().unwrap().is_default);
638 }
639
640 #[test]
641 fn default_subnets_are_public() {
642 let state = Ec2State::new("123456789012", "us-east-1");
643 for sid in state.subnets.keys() {
644 assert!(
645 subnet_is_public(&state, sid),
646 "subnet {sid} should be public"
647 );
648 }
649 }
650
651 #[test]
652 fn ids_match_across_fresh_states() {
653 let a = Ec2State::new("123456789012", "us-east-1");
656 let b = Ec2State::new("123456789012", "us-east-1");
657 let a_vpc: Vec<_> = a.vpcs.keys().collect();
658 let b_vpc: Vec<_> = b.vpcs.keys().collect();
659 assert_eq!(a_vpc, b_vpc);
660 assert_eq!(a_vpc[0], &default_vpc_id("123456789012"));
661 }
662
663 #[test]
664 fn default_vpc_id_agrees_across_regions() {
665 let read_path = Ec2State::new("123456789012", "eu-west-1");
669 let persisted = Ec2State::new("123456789012", "us-east-1");
670 let read_vpc = read_path.vpcs.keys().next().unwrap();
671 let persisted_vpc = persisted.vpcs.keys().next().unwrap();
672 assert_eq!(read_vpc, persisted_vpc);
673 let read_subnets: std::collections::BTreeSet<_> = read_path.subnets.keys().collect();
676 let persisted_subnets: std::collections::BTreeSet<_> = persisted.subnets.keys().collect();
677 assert_eq!(read_subnets, persisted_subnets);
678 }
679}