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
292#[allow(dead_code)]
300pub(crate) fn subnet_is_public(state: &Ec2State, subnet_id: &str) -> bool {
301 let Some(subnet) = state.subnets.get(subnet_id) else {
302 return false;
303 };
304 let explicit = state.route_tables.values().find(|rt| {
306 rt.associations
307 .iter()
308 .any(|a| a.subnet_id.as_deref() == Some(subnet_id))
309 });
310 let main = state
311 .route_tables
312 .values()
313 .find(|rt| rt.vpc_id == subnet.vpc_id && rt.associations.iter().any(|a| a.main));
314 let rt = explicit.or(main);
315 rt.map(route_table_has_igw_default).unwrap_or(false)
316}
317
318fn route_table_has_igw_default(rt: &RouteTable) -> bool {
319 rt.routes.iter().any(|r| {
320 r.destination_cidr_block.as_deref() == Some("0.0.0.0/0")
321 && r.gateway_id
322 .as_deref()
323 .map(|g| g.starts_with("igw-"))
324 .unwrap_or(false)
325 })
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use crate::state::Ec2State;
332
333 #[test]
334 fn deterministic_id_is_stable_and_shaped() {
335 let a = deterministic_id("vpc", "123456789012", "default-vpc");
336 let b = deterministic_id("vpc", "123456789012", "default-vpc");
337 assert_eq!(a, b);
338 assert!(a.starts_with("vpc-"));
339 let hex = a.strip_prefix("vpc-").unwrap();
341 assert_eq!(hex.len(), 17);
342 assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
343 }
344
345 #[test]
346 fn deterministic_id_varies_by_account_and_role() {
347 let base = deterministic_id("vpc", "111111111111", "default-vpc");
348 assert_ne!(base, deterministic_id("vpc", "222222222222", "default-vpc"));
349 assert_ne!(base, deterministic_id("vpc", "111111111111", "default-igw"));
350 }
351
352 #[test]
353 fn deterministic_id_is_region_independent() {
354 assert_eq!(
359 default_vpc_id("111111111111"),
360 deterministic_id("vpc", "111111111111", "default-vpc")
361 );
362 }
363
364 #[test]
365 fn az_id_prefix_matches_aws_shape() {
366 assert_eq!(az_id_prefix("us-east-1"), "use1");
367 assert_eq!(az_id_prefix("eu-west-2"), "euw2");
368 assert_eq!(az_id_prefix("ap-southeast-1"), "aps1");
369 }
370
371 #[test]
372 fn bootstrap_creates_full_default_topology() {
373 let state = Ec2State::new("123456789012", "us-east-1");
374 assert_eq!(state.vpcs.len(), 1);
376 let vpc = state.vpcs.values().next().unwrap();
377 assert!(vpc.is_default);
378 assert_eq!(vpc.cidr_block, "172.31.0.0/16");
379 assert_eq!(state.subnets.len(), DEFAULT_AZ_SUFFIXES.len());
381 assert!(state.subnets.values().all(|s| s.default_for_az));
382 assert!(state.subnets.values().all(|s| s.map_public_ip_on_launch));
383 assert_eq!(state.internet_gateways.len(), 1);
385 let igw = state.internet_gateways.values().next().unwrap();
386 assert_eq!(igw.attachments[0].0, vpc.vpc_id);
387 let sg = state.security_groups.values().next().unwrap();
389 assert_eq!(sg.group_name, "default");
390 assert!(state.network_acls.values().next().unwrap().is_default);
391 }
392
393 #[test]
394 fn default_subnets_are_public() {
395 let state = Ec2State::new("123456789012", "us-east-1");
396 for sid in state.subnets.keys() {
397 assert!(
398 subnet_is_public(&state, sid),
399 "subnet {sid} should be public"
400 );
401 }
402 }
403
404 #[test]
405 fn ids_match_across_fresh_states() {
406 let a = Ec2State::new("123456789012", "us-east-1");
409 let b = Ec2State::new("123456789012", "us-east-1");
410 let a_vpc: Vec<_> = a.vpcs.keys().collect();
411 let b_vpc: Vec<_> = b.vpcs.keys().collect();
412 assert_eq!(a_vpc, b_vpc);
413 assert_eq!(a_vpc[0], &default_vpc_id("123456789012"));
414 }
415
416 #[test]
417 fn default_vpc_id_agrees_across_regions() {
418 let read_path = Ec2State::new("123456789012", "eu-west-1");
422 let persisted = Ec2State::new("123456789012", "us-east-1");
423 let read_vpc = read_path.vpcs.keys().next().unwrap();
424 let persisted_vpc = persisted.vpcs.keys().next().unwrap();
425 assert_eq!(read_vpc, persisted_vpc);
426 let read_subnets: std::collections::BTreeSet<_> = read_path.subnets.keys().collect();
429 let persisted_subnets: std::collections::BTreeSet<_> = persisted.subnets.keys().collect();
430 assert_eq!(read_subnets, persisted_subnets);
431 }
432}