Skip to main content

fakecloud_ec2/
defaults.rs

1//! Default-VPC bootstrap.
2//!
3//! Every real AWS account has, per region, a *default VPC* (`172.31.0.0/16`)
4//! with an attached internet gateway, a main route table that sends
5//! `0.0.0.0/0` at the gateway, one default subnet per Availability Zone, a
6//! `default` security group, and a default network ACL. Callers that never
7//! touch the VPC APIs (the common case — `RunInstances` with no `SubnetId`)
8//! still expect their instances to land in that default VPC and come back from
9//! `DescribeInstances` with a real `vpc-…` / `subnet-…`.
10//!
11//! fakecloud builds the same fixtures the first time an account's EC2 state is
12//! constructed ([`Ec2State::new`](crate::state::Ec2State::new)). The resource
13//! ids are **deterministic** functions of the account id and a role string
14//! (region-independent — see [`deterministic_id`]), so the throwaway empty
15//! states that the read paths synthesize as a "not found" fallback report the
16//! *same* ids as the persisted account state regardless of the caller's region.
17//!
18//! Per-VPC packet isolation (issue #1745 phase 2+) keys off this topology: a
19//! subnet whose route table has a `0.0.0.0/0 -> igw-…` route is public and gets
20//! a routable backing network; a subnet without one is private (`internal`).
21
22use crate::state::{
23    Ec2State, InternetGateway, NetworkAcl, NetworkAclAssoc, NetworkAclEntry, Route, RouteTable,
24    RouteTableAssociation, SecurityGroup, SecurityGroupRule, Subnet, Vpc,
25};
26
27/// CIDR of the default VPC, matching AWS.
28const DEFAULT_VPC_CIDR: &str = "172.31.0.0/16";
29
30/// The Availability Zone suffixes that receive a default subnet. AWS creates a
31/// default subnet in every AZ; three covers the cardinality every realistic
32/// test exercises (and keeps the deterministic CIDR layout simple).
33const DEFAULT_AZ_SUFFIXES: [&str; 3] = ["a", "b", "c"];
34
35/// Deterministic EC2 resource id: `<prefix>-<17 hex>` derived from the account
36/// and a per-resource `role`. Deliberately **region-independent**: a
37/// `MultiAccountState` partitions by account and pins a single region per
38/// server, but read handlers build a throwaway `Ec2State::new(account,
39/// req.region)` for accounts that don't exist yet — where `req.region` is the
40/// caller's SigV4 scope, not the server's region. Seeding the id on the region
41/// made those throwaway-derived ids disagree with the persisted account's ids
42/// whenever the client region differed from the server's, so a no-subnet launch
43/// stamped the instance with a subnet/VPC id that didn't exist in its own
44/// account (bug-hunt 2026-06-18 finding 1.1). Dropping region from the seed
45/// makes both paths agree; the AZ/CIDR cosmetics below still use the region.
46pub(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    // 16 hex from the first hash + 1 nibble from the second = the 17 hex chars
51    // a modern EC2 long-id carries.
52    format!("{prefix}-{:016x}{:01x}", h1, h2 & 0xf)
53}
54
55/// FNV-1a 64-bit. A tiny, dependency-free, stable hash — we only need
56/// determinism, not cryptographic strength.
57fn 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
66/// AWS-style AZ-id prefix for a region: `us-east-1 -> use1`. Falls back to the
67/// region with dashes stripped for non-`a-b-N` shapes.
68fn 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
82/// The default VPC id for an account (also exposed so request handlers can
83/// resolve the implicit default without re-deriving the seed by hand).
84pub(crate) fn default_vpc_id(account: &str) -> String {
85    deterministic_id("vpc", account, "default-vpc")
86}
87
88/// The default security-group id for an account.
89pub(crate) fn default_security_group_id(account: &str) -> String {
90    deterministic_id("sg", account, "default-sg")
91}
92
93/// Populate `state` with the default VPC topology. Called once at state
94/// construction; idempotent in practice because the deterministic ids collide
95/// on re-entry.
96pub(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    // --- default VPC ---
111    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    // --- internet gateway, attached to the default VPC ---
128    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    // --- default subnets, one per AZ ---
137    let az_prefix = az_id_prefix(&region);
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                // /20 blocks carved from 172.31.0.0/16: .0, .16, .32 …
148                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                // Default subnets auto-assign public IPs, matching AWS.
155                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    // --- main route table: local + default route at the IGW (public) ---
167    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    // --- default security group: allow all from self, allow all egress ---
205    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    // --- default network ACL: allow-all, associated with every default subnet ---
244    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
268/// Create the implicit resources AWS provisions for every newly-created VPC: a
269/// `default` security group, a default network ACL, and a main route table
270/// (with the `local` route). The `aws_vpc` resource reads back
271/// `default_security_group_id`, `default_network_acl_id`,
272/// `default_route_table_id`, and `main_route_table_id`, all derived from these.
273/// Ids are deterministic functions of the VPC id so read-path fallbacks agree.
274pub(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
381/// True when `subnet_id` resolves to a subnet whose route table carries a
382/// `0.0.0.0/0` route at an internet gateway — i.e. a *public* subnet. A subnet
383/// without such a route is private and (phase 2) backs onto an `internal`
384/// network. Subnets default to their VPC's main route table when not
385/// explicitly associated.
386// Drives per-subnet networking (chooses `internal` vs routable backing
387// networks) from `service/mod.rs` and `instance.rs`.
388pub(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    // An explicit association wins; otherwise fall back to the VPC's main table.
393    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        // 17 hex chars after the prefix, matching EC2 long-ids.
428        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        // The id seed deliberately excludes region so read-path (req.region)
443        // and persisted (server region) states agree (finding 1.1). Region is
444        // not a parameter anymore — this documents the contract by asserting
445        // default_vpc_id depends only on the account.
446        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        // exactly one VPC, marked default
463        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        // one subnet per AZ suffix, all default_for_az + public
468        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        // IGW attached to the default VPC
472        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        // default SG + default NACL
476        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        // The throwaway "empty" states read paths build must agree with the
495        // persisted account state on the default VPC id.
496        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        // The crux of finding 1.1: a read-path empty built with the caller's
507        // region must derive the SAME default VPC id as the persisted account
508        // state built with the server's region.
509        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        // subnets too (so a no-subnet launch resolves a subnet that exists in
515        // the persisted account regardless of the caller's region).
516        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}