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 },
124 );
125
126 state.internet_gateways.insert(
128 igw_id.clone(),
129 InternetGateway {
130 internet_gateway_id: igw_id.clone(),
131 attachments: vec![(vpc_id.clone(), "available".to_string())],
132 },
133 );
134
135 let az_prefix = az_id_prefix(®ion);
137 let mut subnet_ids = Vec::new();
138 for (idx, suffix) in DEFAULT_AZ_SUFFIXES.iter().enumerate() {
139 let subnet_id = deterministic_id("subnet", &account, &format!("default-subnet-{suffix}"));
140 let az = format!("{region}{suffix}");
141 state.subnets.insert(
142 subnet_id.clone(),
143 Subnet {
144 subnet_id: subnet_id.clone(),
145 vpc_id: vpc_id.clone(),
146 cidr_block: format!("172.31.{}.0/20", idx * 16),
148 availability_zone: az,
149 availability_zone_id: format!("{az_prefix}-az{}", idx + 1),
150 state: "available".to_string(),
151 available_ip_address_count: 4091,
152 default_for_az: true,
153 map_public_ip_on_launch: true,
155 assign_ipv6_address_on_creation: false,
156 map_customer_owned_ip_on_launch: false,
157 enable_dns64: false,
158 private_dns_hostname_type: "ip-name".to_string(),
159 },
160 );
161 subnet_ids.push(subnet_id);
162 }
163
164 let mut associations = vec![RouteTableAssociation {
166 association_id: deterministic_id("rtbassoc", &account, "default-rtb-main"),
167 route_table_id: rtb_id.clone(),
168 subnet_id: None,
169 gateway_id: None,
170 main: true,
171 }];
172 for sid in &subnet_ids {
173 associations.push(RouteTableAssociation {
174 association_id: deterministic_id("rtbassoc", &account, &format!("default-rtb-{sid}")),
175 route_table_id: rtb_id.clone(),
176 subnet_id: Some(sid.clone()),
177 gateway_id: None,
178 main: false,
179 });
180 }
181 state.route_tables.insert(
182 rtb_id.clone(),
183 RouteTable {
184 route_table_id: rtb_id.clone(),
185 vpc_id: vpc_id.clone(),
186 routes: vec![
187 Route {
188 destination_cidr_block: Some(DEFAULT_VPC_CIDR.to_string()),
189 gateway_id: Some("local".to_string()),
190 ..Default::default()
191 },
192 Route {
193 destination_cidr_block: Some("0.0.0.0/0".to_string()),
194 gateway_id: Some(igw_id.clone()),
195 ..Default::default()
196 },
197 ],
198 associations,
199 },
200 );
201
202 state.security_groups.insert(
204 sg_id.clone(),
205 SecurityGroup {
206 group_id: sg_id.clone(),
207 group_name: "default".to_string(),
208 description: "default VPC security group".to_string(),
209 vpc_id: vpc_id.clone(),
210 rules: vec![
211 SecurityGroupRule {
212 rule_id: deterministic_id("sgr", &account, "default-sg-ingress"),
213 group_id: sg_id.clone(),
214 is_egress: false,
215 ip_protocol: "-1".to_string(),
216 from_port: -1,
217 to_port: -1,
218 cidr_ipv4: None,
219 cidr_ipv6: None,
220 prefix_list_id: None,
221 referenced_group_id: Some(sg_id.clone()),
222 description: String::new(),
223 },
224 SecurityGroupRule {
225 rule_id: deterministic_id("sgr", &account, "default-sg-egress"),
226 group_id: sg_id.clone(),
227 is_egress: true,
228 ip_protocol: "-1".to_string(),
229 from_port: -1,
230 to_port: -1,
231 cidr_ipv4: Some("0.0.0.0/0".to_string()),
232 cidr_ipv6: None,
233 prefix_list_id: None,
234 referenced_group_id: None,
235 description: String::new(),
236 },
237 ],
238 },
239 );
240
241 let nacl_associations = subnet_ids
243 .iter()
244 .map(|sid| NetworkAclAssoc {
245 association_id: deterministic_id("aclassoc", &account, &format!("default-acl-{sid}")),
246 subnet_id: sid.clone(),
247 })
248 .collect();
249 state.network_acls.insert(
250 acl_id.clone(),
251 NetworkAcl {
252 network_acl_id: acl_id.clone(),
253 vpc_id: vpc_id.clone(),
254 is_default: true,
255 entries: vec![
256 allow_all_entry(false),
257 deny_all_entry(false),
258 allow_all_entry(true),
259 deny_all_entry(true),
260 ],
261 associations: nacl_associations,
262 },
263 );
264}
265
266fn allow_all_entry(egress: bool) -> NetworkAclEntry {
267 NetworkAclEntry {
268 rule_number: 100,
269 protocol: "-1".to_string(),
270 rule_action: "allow".to_string(),
271 egress,
272 cidr_block: Some("0.0.0.0/0".to_string()),
273 ipv6_cidr_block: None,
274 port_range: None,
275 icmp_type_code: None,
276 }
277}
278
279fn deny_all_entry(egress: bool) -> NetworkAclEntry {
280 NetworkAclEntry {
281 rule_number: 32767,
282 protocol: "-1".to_string(),
283 rule_action: "deny".to_string(),
284 egress,
285 cidr_block: Some("0.0.0.0/0".to_string()),
286 ipv6_cidr_block: None,
287 port_range: None,
288 icmp_type_code: None,
289 }
290}
291
292pub(crate) fn subnet_is_public(state: &Ec2State, subnet_id: &str) -> bool {
300 let Some(subnet) = state.subnets.get(subnet_id) else {
301 return false;
302 };
303 let explicit = state.route_tables.values().find(|rt| {
305 rt.associations
306 .iter()
307 .any(|a| a.subnet_id.as_deref() == Some(subnet_id))
308 });
309 let main = state
310 .route_tables
311 .values()
312 .find(|rt| rt.vpc_id == subnet.vpc_id && rt.associations.iter().any(|a| a.main));
313 let rt = explicit.or(main);
314 rt.map(route_table_has_igw_default).unwrap_or(false)
315}
316
317fn route_table_has_igw_default(rt: &RouteTable) -> bool {
318 rt.routes.iter().any(|r| {
319 r.destination_cidr_block.as_deref() == Some("0.0.0.0/0")
320 && r.gateway_id
321 .as_deref()
322 .map(|g| g.starts_with("igw-"))
323 .unwrap_or(false)
324 })
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use crate::state::Ec2State;
331
332 #[test]
333 fn deterministic_id_is_stable_and_shaped() {
334 let a = deterministic_id("vpc", "123456789012", "default-vpc");
335 let b = deterministic_id("vpc", "123456789012", "default-vpc");
336 assert_eq!(a, b);
337 assert!(a.starts_with("vpc-"));
338 let hex = a.strip_prefix("vpc-").unwrap();
340 assert_eq!(hex.len(), 17);
341 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
342 }
343
344 #[test]
345 fn deterministic_id_varies_by_account_and_role() {
346 let base = deterministic_id("vpc", "111111111111", "default-vpc");
347 assert_ne!(base, deterministic_id("vpc", "222222222222", "default-vpc"));
348 assert_ne!(base, deterministic_id("vpc", "111111111111", "default-igw"));
349 }
350
351 #[test]
352 fn deterministic_id_is_region_independent() {
353 assert_eq!(
358 default_vpc_id("111111111111"),
359 deterministic_id("vpc", "111111111111", "default-vpc")
360 );
361 }
362
363 #[test]
364 fn az_id_prefix_matches_aws_shape() {
365 assert_eq!(az_id_prefix("us-east-1"), "use1");
366 assert_eq!(az_id_prefix("eu-west-2"), "euw2");
367 assert_eq!(az_id_prefix("ap-southeast-1"), "aps1");
368 }
369
370 #[test]
371 fn bootstrap_creates_full_default_topology() {
372 let state = Ec2State::new("123456789012", "us-east-1");
373 assert_eq!(state.vpcs.len(), 1);
375 let vpc = state.vpcs.values().next().unwrap();
376 assert!(vpc.is_default);
377 assert_eq!(vpc.cidr_block, "172.31.0.0/16");
378 assert_eq!(state.subnets.len(), DEFAULT_AZ_SUFFIXES.len());
380 assert!(state.subnets.values().all(|s| s.default_for_az));
381 assert!(state.subnets.values().all(|s| s.map_public_ip_on_launch));
382 assert_eq!(state.internet_gateways.len(), 1);
384 let igw = state.internet_gateways.values().next().unwrap();
385 assert_eq!(igw.attachments[0].0, vpc.vpc_id);
386 let sg = state.security_groups.values().next().unwrap();
388 assert_eq!(sg.group_name, "default");
389 assert!(state.network_acls.values().next().unwrap().is_default);
390 }
391
392 #[test]
393 fn default_subnets_are_public() {
394 let state = Ec2State::new("123456789012", "us-east-1");
395 for sid in state.subnets.keys() {
396 assert!(
397 subnet_is_public(&state, sid),
398 "subnet {sid} should be public"
399 );
400 }
401 }
402
403 #[test]
404 fn ids_match_across_fresh_states() {
405 let a = Ec2State::new("123456789012", "us-east-1");
408 let b = Ec2State::new("123456789012", "us-east-1");
409 let a_vpc: Vec<_> = a.vpcs.keys().collect();
410 let b_vpc: Vec<_> = b.vpcs.keys().collect();
411 assert_eq!(a_vpc, b_vpc);
412 assert_eq!(a_vpc[0], &default_vpc_id("123456789012"));
413 }
414
415 #[test]
416 fn default_vpc_id_agrees_across_regions() {
417 let read_path = Ec2State::new("123456789012", "eu-west-1");
421 let persisted = Ec2State::new("123456789012", "us-east-1");
422 let read_vpc = read_path.vpcs.keys().next().unwrap();
423 let persisted_vpc = persisted.vpcs.keys().next().unwrap();
424 assert_eq!(read_vpc, persisted_vpc);
425 let read_subnets: std::collections::BTreeSet<_> = read_path.subnets.keys().collect();
428 let persisted_subnets: std::collections::BTreeSet<_> = persisted.subnets.keys().collect();
429 assert_eq!(read_subnets, persisted_subnets);
430 }
431}