Skip to main content

microvms_core/control/
connector.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Network connectors: a closed intent enum, and the ARN each one derives (TRAP-4).
3//!
4//! # Why an enum rather than a string
5//!
6//! Two reasons, and both are measurements.
7//!
8//! The API takes a **fully-qualified ARN** and rejects the bare name with "Malformed
9//! network connector ARN" (measured 2026-08-05). `NetworkConnector` in the service model
10//! is just `{type: string, max: 2048, min: 1}` — no pattern, no enum — so a free-form
11//! parameter passes every check the model states and fails on the wire, and the value
12//! that reads most natural to write (`"ALL_INGRESS"`) is exactly the one that fails.
13//! Deriving the ARN from an intent means the caller states what they want and the
14//! spelling is not theirs to get wrong.
15//!
16//! # TRAP-11, revised: the shell connector is a variant, on measured ground
17//!
18//! `SHELL_INGRESS` **is** a variant here, and it was not always. This module used to
19//! omit it on the claim that it gated a console-only debugging flow — "not a
20//! programmatic exec path despite the name" — and that omission made requesting it
21//! unwriteable. `docs/PLATFORM.md` (measured 2026-08-15) refutes the claim: the shell
22//! endpoint is a real PTY over a WebSocket, and it is programmatically drivable. The
23//! ground that actually holds is narrower — **one interactive session is not
24//! programmatic exec**: no exec ids, no idempotency, no separated stdout/stderr, no exit
25//! codes. So the variant exists for callers that want the PTY, and the exec path never
26//! requests it; the lifecycle test in [`crate::control::microvm`] asserts a launch
27//! carries exactly the connectors its caller asked for.
28//!
29//! Two measured constraints travel with the variant (both from `docs/PLATFORM.md`):
30//! `ALL_INGRESS` cannot combine with any other ingress connector, and the platform says
31//! so only at token-mint time — `RunMicrovm` accepts the invalid set, the VM reaches
32//! RUNNING, and it bills until something asks for a shell token.
33//! [`crate::control::ControlPlane::run_microvm`] refuses the combination locally
34//! instead. The pair that works is `[HTTP_INGRESS, SHELL_INGRESS]`.
35//!
36//! The sibling half of TRAP-11 — the shell-auth operation — is still closed by the
37//! absence of a method on [`crate::control::ControlPlane`]; see that module's docs.
38
39use crate::region::Region;
40
41/// The Lambda-managed connectors this client will name, and no others.
42///
43/// Named for the *intent* rather than for the wire value, because the intent is what a
44/// caller has: "let the proxy reach the VM" and "let the VM reach the internet". The
45/// wire spellings ([`ConnectorIntent::wire_name`]) are an implementation detail of the
46/// ARN.
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
48pub enum ConnectorIntent {
49    /// Lets the endpoint proxy reach the VM. Required for any session to work.
50    ///
51    /// The union that cannot be intersected: the platform refuses to combine it with
52    /// any other ingress connector, and only at token-mint time — see the module docs.
53    /// A VM that needs a shell requests [`ConnectorIntent::HttpIngress`] plus
54    /// [`ConnectorIntent::ShellIngress`] instead of this.
55    AllIngress,
56    /// Lets the endpoint proxy reach the VM's HTTP surface, without the shell.
57    ///
58    /// The finer-grained sibling of [`ConnectorIntent::AllIngress`], and the half of the
59    /// measured pair `[HTTP_INGRESS, SHELL_INGRESS]` that keeps the daemon reachable
60    /// (`docs/PLATFORM.md`, measured 2026-08-15).
61    HttpIngress,
62    /// Lets the VM mint shell tokens and serve its PTY WebSocket.
63    ///
64    /// One interactive session, not programmatic exec — the module docs carry the
65    /// revision of TRAP-11 that admitted this variant. Never combined with
66    /// [`ConnectorIntent::AllIngress`]; the pair that launches and mints is with
67    /// [`ConnectorIntent::HttpIngress`].
68    ShellIngress,
69    /// Lets the VM reach the internet.
70    ///
71    /// Omitted by default — the right default for a daemon that needs none. Omitting it
72    /// omits the connector from the request; measured 2026-09-11 and 2026-09-12 the
73    /// platform still gave such a VM outbound network (`docs/PLATFORM.md`, "A VM launched
74    /// without the egress connector still has outbound network").
75    Egress,
76}
77
78impl ConnectorIntent {
79    /// Every intent, so a test can enumerate the complete set.
80    ///
81    /// Maintained by hand; the tests below assert its length, so an edit here is a
82    /// deliberate one. The set grew from two to four when TRAP-11 was revised — see the
83    /// module docs.
84    pub const ALL: [ConnectorIntent; 4] = [
85        ConnectorIntent::AllIngress,
86        ConnectorIntent::HttpIngress,
87        ConnectorIntent::ShellIngress,
88        ConnectorIntent::Egress,
89    ];
90
91    /// The connector's name as the ARN spells it.
92    ///
93    /// `INTERNET_EGRESS` rather than `EGRESS`: the wire name is not the intent's name,
94    /// which is the second reason this is a table rather than a `Display` derive.
95    pub fn wire_name(self) -> &'static str {
96        match self {
97            ConnectorIntent::AllIngress => "ALL_INGRESS",
98            ConnectorIntent::HttpIngress => "HTTP_INGRESS",
99            ConnectorIntent::ShellIngress => "SHELL_INGRESS",
100            ConnectorIntent::Egress => "INTERNET_EGRESS",
101        }
102    }
103
104    /// The fully-qualified ARN for this connector in `region`.
105    ///
106    /// One interpolation for both directions. The Python client's earlier shape derived
107    /// the egress ARN by string-replacing `ALL_INGRESS` inside the ingress one, which
108    /// produced a valid ARN only as long as the two names never became substrings of
109    /// each other (`sandbox.py:391-398`).
110    ///
111    /// The doubled `aws-network-connector` segment is not a typo: the resource type and
112    /// the resource name are both present, and the format is copied from
113    /// `sandbox.py:398` rather than reconstructed from the ARN grammar.
114    pub fn arn(self, region: &Region) -> String {
115        format!(
116            "arn:aws:lambda:{}:aws:network-connector:aws-network-connector:{}",
117            region.as_str(),
118            self.wire_name()
119        )
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    /// The exact ARN, as a literal, for the region the measurements were taken in. The
128    /// format came from a measurement rather than from the ARN grammar, so a
129    /// reconstruction of it in the test would agree with a reconstruction in the code.
130    #[test]
131    fn the_ingress_arn_is_the_measured_literal() {
132        assert_eq!(
133            ConnectorIntent::AllIngress.arn(&Region::UsEast1),
134            "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:ALL_INGRESS"
135        );
136    }
137
138    /// The egress ARN, likewise — and note the wire name is `INTERNET_EGRESS` while the
139    /// variant is `Egress`, which is the thing a `Display` derive would have got wrong.
140    #[test]
141    fn the_egress_arn_uses_the_internet_egress_wire_name() {
142        assert_eq!(
143            ConnectorIntent::Egress.arn(&Region::UsEast1),
144            "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
145        );
146    }
147
148    /// The region is interpolated rather than fixed, for every region including the
149    /// escape hatch. TRAP-4 is "for the request region", and a hardcoded `us-east-1`
150    /// would pass both tests above.
151    #[test]
152    fn the_arn_carries_the_request_region_for_every_region() {
153        for region in crate::region::MICROVM_REGIONS {
154            let arn = ConnectorIntent::AllIngress.arn(&region);
155            assert!(
156                arn.contains(&format!(":lambda:{}:aws:", region.as_str())),
157                "{arn}"
158            );
159        }
160        let unlisted = Region::unlisted("me-south-1");
161        assert!(
162            ConnectorIntent::Egress
163                .arn(&unlisted)
164                .contains(":lambda:me-south-1:aws:"),
165            "the escape hatch still derives an ARN for its own region"
166        );
167    }
168
169    /// TRAP-11, rewritten on the ground that holds. This test used to assert no intent
170    /// names `SHELL_INGRESS`, standing on the claim that the shell was a console-only
171    /// debugging path — a claim `docs/PLATFORM.md` (measured 2026-08-15) refutes: the
172    /// shell endpoint is a real PTY and programmatically drivable. What holds instead is
173    /// that **one interactive session is not programmatic exec**, so the variant exists,
174    /// exactly one intent renders it, and the check on the rendered ARNs stays — a
175    /// variant named something else must not render `SHELL_INGRESS` either.
176    ///
177    /// The other half of the revised guard — a launch carries exactly the connectors its
178    /// caller asked for — lives with the lifecycle test in `microvm.rs`.
179    ///
180    /// **Falsification** — add a second variant whose wire name contains `SHELL`, or
181    /// rename [`ConnectorIntent::ShellIngress`]'s wire name, and this fails.
182    #[test]
183    fn shell_ingress_is_one_deliberate_intent() {
184        assert_eq!(
185            ConnectorIntent::ALL.len(),
186            4,
187            "four intents, and shell is deliberately one of them"
188        );
189        let shells: Vec<ConnectorIntent> = ConnectorIntent::ALL
190            .into_iter()
191            .filter(|intent| intent.wire_name().contains("SHELL"))
192            .collect();
193        assert_eq!(
194            shells,
195            vec![ConnectorIntent::ShellIngress],
196            "exactly one intent names the shell, and it is the one that says so"
197        );
198        assert_eq!(
199            ConnectorIntent::ShellIngress.arn(&Region::UsEast1),
200            "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:SHELL_INGRESS"
201        );
202    }
203
204    /// `HTTP_INGRESS`, the measured literal — the finer-grained ingress that pairs with
205    /// the shell connector, since `ALL_INGRESS` cannot combine with either
206    /// (`docs/PLATFORM.md`, measured 2026-08-15).
207    #[test]
208    fn the_http_ingress_arn_is_the_measured_literal() {
209        assert_eq!(
210            ConnectorIntent::HttpIngress.arn(&Region::UsEast1),
211            "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:HTTP_INGRESS"
212        );
213    }
214
215    /// No two intents render the same ARN. The string-replace shape this replaced
216    /// could produce two identical ARNs if the names ever became substrings of one
217    /// another, and that is the failure this asserts against — now across the whole
218    /// set rather than the original pair.
219    #[test]
220    fn no_two_intents_render_the_same_arn() {
221        let arns: Vec<String> = ConnectorIntent::ALL
222            .iter()
223            .map(|intent| intent.arn(&Region::EuWest1))
224            .collect();
225        for (i, a) in arns.iter().enumerate() {
226            for b in &arns[i + 1..] {
227                assert_ne!(a, b);
228            }
229        }
230        assert!(!arns[0].contains("INTERNET"), "{}", arns[0]);
231    }
232
233    /// Every derived ARN clears the model's `NetworkConnector` bounds (min 1, max 2048),
234    /// including for the longest region name. The model states no pattern, so length is
235    /// the only constraint there is to check — and it is checked here rather than
236    /// trusted because a 2048-character ARN would be rejected on the wire with the same
237    /// "malformed" message a bare name gets.
238    #[test]
239    fn every_derived_arn_fits_the_models_connector_bounds() {
240        for region in crate::region::MICROVM_REGIONS {
241            for intent in ConnectorIntent::ALL {
242                let arn = intent.arn(&region);
243                assert!((1..=2048).contains(&arn.len()), "{arn}");
244                assert!(arn.starts_with("arn:aws:lambda:"), "{arn}");
245            }
246        }
247    }
248}