microvms_core/control/connector.rs
1// SPDX-License-Identifier: Apache-2.0
2//! Managed connector intents and customer-managed VPC egress connector validation.
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/// Validates a customer-managed connector ARN before launching a billable VM.
42pub(super) fn require_egress_connector_arn(arn: &str, region: &Region) -> Result<(), crate::Error> {
43 if !is_connector_arn_in(arn, region.as_str()) {
44 return Err(connector_arn_error(region.as_str()));
45 }
46 Ok(())
47}
48
49/// The refusal for a string that is not a customer-managed connector ARN in `region`.
50fn connector_arn_error(region: &str) -> crate::Error {
51 crate::Error::invalid_arg(format!(
52 "egressNetworkConnectors requires a customer-managed Lambda network connector ARN \
53 in {region} (arn:aws:lambda:{region}:<12-digit-account>:network-connector:<id>[:<version>]); \
54 create the VPC egress connector through lambda-core first",
55 ))
56}
57
58/// Whether `arn` is a customer-managed connector ARN in the region spelled `region`.
59fn is_connector_arn_in(arn: &str, region: &str) -> bool {
60 let parts: Vec<&str> = arn.splitn(6, ':').collect();
61 parts.len() == 6
62 && (1..=crate::constants::MAX_NETWORK_CONNECTOR_LEN).contains(&arn.len())
63 && parts[0] == "arn"
64 && parts[1] == "aws"
65 && parts[2] == "lambda"
66 && parts[3] == region
67 && parts[4].len() == 12
68 && parts[4].bytes().all(|byte| byte.is_ascii_digit())
69 && parts[5]
70 .strip_prefix("network-connector:")
71 .is_some_and(|resource| {
72 let mut parts = resource.split(':');
73 let name = parts.next().unwrap_or_default();
74 let version = parts.next();
75 !name.is_empty()
76 && name
77 .bytes()
78 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
79 && version.is_none_or(|version| {
80 !version.is_empty()
81 && !version.starts_with('0')
82 && version.bytes().all(|byte| byte.is_ascii_digit())
83 })
84 && parts.next().is_none()
85 })
86}
87
88/// The refusal for managed internet egress beside customer-managed connectors.
89pub(crate) fn egress_with_connectors() -> crate::Error {
90 crate::Error::invalid_arg(
91 "INTERNET_EGRESS cannot be combined with customer-managed egress connectors: \
92 choose managed internet egress or VPC routing",
93 )
94}
95
96/// The refusal for managed internet egress beside the advisory deny.
97pub(crate) fn egress_with_deny() -> crate::Error {
98 crate::Error::invalid_arg(
99 "egress and deny_egress ask for opposite things: egress puts the \
100 INTERNET_EGRESS connector on the launch, and deny_egress sets the guest's \
101 proxy variables to a black hole so a well-behaved client refuses to leave \
102 the VM. Pick one. Neither seals the VM — omitting the connector is measured \
103 not to (docs/PLATFORM.md), and the deny is advisory.",
104 )
105}
106
107/// The refusal for a `NetworkConnectorList` member over its ceiling, or `None` within it.
108pub(crate) fn over_connector_ceiling(member: &str, count: usize) -> Option<crate::Error> {
109 (count > crate::constants::MAX_NETWORK_CONNECTORS).then(|| {
110 crate::Error::invalid_arg(format!(
111 "{member} has {count} network connectors, over the NetworkConnectorList \
112 ceiling of {} (service model {}).",
113 crate::constants::MAX_NETWORK_CONNECTORS,
114 crate::constants::MODEL_API_VERSION,
115 ))
116 })
117}
118
119/// The egress posture a launch with these options reports, or the refusal it raises
120/// (BIND-11, BIND-13). Pure: no AWS call, no credentials.
121///
122/// A harness decides with this, before it pays for a build, whether a task that must not
123/// reach the network can run: only [`EgressPosture::Sealed`] is internet isolation, and this
124/// never answers it, because no launch option carries the evidence `sealed` needs (a VPC
125/// egress connector **and** separately verified VPC routing without an internet gateway or
126/// NAT gateway, `docs/NETWORKING.md`). [`crate::sandbox::Sandbox::run`] refuses and derives
127/// through this same function, and the session it builds carries the answer
128/// ([`crate::session::Session::egress_posture`]), which is also what the CLI envelope's
129/// `egressPosture` reports.
130///
131/// The refusals, in the order a launch raises them: `egress` with `deny_egress`, `egress`
132/// with any connector, a connector that is not a customer-managed connector ARN in `region`,
133/// and more connectors than `NetworkConnectorList` allows. Without a `region`, each ARN is
134/// checked against the region it names; a launch also requires that region to be its own.
135pub fn egress_posture_for(
136 egress: bool,
137 egress_network_connectors: &[String],
138 deny_egress: bool,
139 region: Option<&Region>,
140) -> Result<EgressPosture, crate::Error> {
141 if egress && deny_egress {
142 return Err(egress_with_deny());
143 }
144 if egress && !egress_network_connectors.is_empty() {
145 return Err(egress_with_connectors());
146 }
147 for arn in egress_network_connectors {
148 match region {
149 Some(region) => require_egress_connector_arn(arn, region)?,
150 None => {
151 // The region the ARN names, when it names a plausible one.
152 let named = arn.split(':').nth(3).unwrap_or_default();
153 let plausible = !named.is_empty()
154 && named.bytes().all(|byte| {
155 byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'
156 });
157 if !(plausible && is_connector_arn_in(arn, named)) {
158 return Err(connector_arn_error(if plausible {
159 named
160 } else {
161 "<region>"
162 }));
163 }
164 }
165 }
166 }
167 if let Some(refusal) =
168 over_connector_ceiling("egressNetworkConnectors", egress_network_connectors.len())
169 {
170 return Err(refusal);
171 }
172 Ok(EgressPosture::for_launch(egress, deny_egress))
173}
174
175/// Compatibility constant: omitting an egress connector does not block internet access.
176/// Isolation requires a VPC egress connector and a VPC without an internet gateway
177/// or NAT gateway. A launch flag cannot establish the VPC's routing configuration.
178pub const PLATFORM_HONOURS_OMITTED_EGRESS: bool = false;
179
180/// Conservative network posture inferred from launch options.
181///
182/// Customer-managed connector ARNs do not prove internet isolation: their VPC routing
183/// must be checked separately. No combination of flags is automatically `Sealed`.
184#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
185pub enum EgressPosture {
186 /// `INTERNET_EGRESS` is on the request: the VM has outbound network by design.
187 Open,
188 /// Internet isolation has not been verified. This is the default launch posture.
189 Unsealed,
190 /// Advisory in-guest proxy denial ([`crate::sandbox::RunRequest::deny_egress`]).
191 /// A workload can bypass it by ignoring its environment.
192 BestEffort,
193 /// Internet isolation verified separately through VPC routing configuration.
194 /// Retained for stored labels; never inferred by [`EgressPosture::for_launch`].
195 Sealed,
196}
197
198impl EgressPosture {
199 /// The posture of a launch that requested `egress` and asked for `deny_egress`.
200 ///
201 /// These flags cannot verify VPC routing, so this never returns `Sealed`.
202 pub fn for_launch(egress: bool, deny_egress: bool) -> Self {
203 match (egress, deny_egress) {
204 (true, _) => EgressPosture::Open,
205 (false, true) => EgressPosture::BestEffort,
206 (false, false) => EgressPosture::Unsealed,
207 }
208 }
209
210 /// The wire spelling, which is what an envelope and a `--json` consumer branch on.
211 ///
212 /// Four values and no `Option`: a consumer never has to guard against a missing
213 /// posture, and none of the four is the empty string a `bool` degrades into.
214 pub fn as_str(self) -> &'static str {
215 match self {
216 EgressPosture::Open => "open",
217 EgressPosture::Unsealed => "unsealed",
218 EgressPosture::BestEffort => "best-effort",
219 EgressPosture::Sealed => "sealed",
220 }
221 }
222
223 /// Whether outbound traffic is refused by something other than the workload's goodwill.
224 ///
225 /// Only [`EgressPosture::Sealed`] answers `true`. [`EgressPosture::BestEffort`] does
226 /// not, and that is the whole reason this predicate exists rather than a
227 /// `posture != Open` test at each call site.
228 pub fn is_sealed(self) -> bool {
229 matches!(self, EgressPosture::Sealed)
230 }
231
232 /// The posture a stored label names, or `None` for a string this version does not know.
233 ///
234 /// The inverse of [`EgressPosture::as_str`], and it exists for one caller: the CLI's
235 /// name registry persists the label of the launch a name was registered for, and
236 /// `agent-up`'s refresh path reads it back in a later process rather than assuming the
237 /// posture of the launch it did not perform. `None` rather than a default, so the caller
238 /// decides what an unreadable record means instead of inheriting a claim.
239 pub fn from_label(label: &str) -> Option<Self> {
240 [
241 EgressPosture::Open,
242 EgressPosture::Unsealed,
243 EgressPosture::BestEffort,
244 EgressPosture::Sealed,
245 ]
246 .into_iter()
247 .find(|posture| posture.as_str() == label)
248 }
249
250 /// One sentence a human can act on, printed beside the label.
251 ///
252 /// Each names the mechanism rather than a verdict, because "no egress" read as a
253 /// verdict is the defect.
254 pub fn describe(self) -> &'static str {
255 match self {
256 EgressPosture::Open => {
257 "the INTERNET_EGRESS connector is on the request; the VM reaches the \
258 internet by design"
259 }
260 EgressPosture::Unsealed => {
261 "internet isolation is unverified. Use a VPC egress connector with a VPC \
262 without an internet gateway or NAT gateway; connector omission does not \
263 block internet access. Not a seal"
264 }
265 EgressPosture::BestEffort => {
266 "advisory proxy-deny environment variables affect clients that honor them; \
267 workloads can bypass them. Not a seal"
268 }
269 EgressPosture::Sealed => {
270 "internet isolation requires separately verified VPC routing without an \
271 internet gateway or NAT gateway; launch flags alone do not establish it"
272 }
273 }
274 }
275}
276
277/// Defaults to the posture of a launch with no network flags.
278impl Default for EgressPosture {
279 fn default() -> Self {
280 EgressPosture::for_launch(false, false)
281 }
282}
283
284impl std::fmt::Display for EgressPosture {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.write_str(self.as_str())
287 }
288}
289
290/// Lambda-managed connector intents. Customer-managed VPC connectors use explicit ARNs.
291///
292/// Named for the *intent* rather than for the wire value, because the intent is what a
293/// caller has: "let the proxy reach the VM" and "let the VM reach the internet". The
294/// wire spellings ([`ConnectorIntent::wire_name`]) are an implementation detail of the
295/// ARN.
296#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
297pub enum ConnectorIntent {
298 /// Lets the endpoint proxy reach the VM. Required for any session to work.
299 ///
300 /// The union that cannot be intersected: the platform refuses to combine it with
301 /// any other ingress connector, and only at token-mint time — see the module docs.
302 /// A VM that needs a shell requests [`ConnectorIntent::HttpIngress`] plus
303 /// [`ConnectorIntent::ShellIngress`] instead of this.
304 AllIngress,
305 /// Lets the endpoint proxy reach the VM's HTTP surface, without the shell.
306 ///
307 /// The finer-grained sibling of [`ConnectorIntent::AllIngress`], and the half of the
308 /// measured pair `[HTTP_INGRESS, SHELL_INGRESS]` that keeps the daemon reachable
309 /// (`docs/PLATFORM.md`, measured 2026-08-15).
310 HttpIngress,
311 /// Lets the VM mint shell tokens and serve its PTY WebSocket.
312 ///
313 /// One interactive session, not programmatic exec — the module docs carry the
314 /// revision of TRAP-11 that admitted this variant. Never combined with
315 /// [`ConnectorIntent::AllIngress`]; the pair that launches and mints is with
316 /// [`ConnectorIntent::HttpIngress`].
317 ShellIngress,
318 /// Lets the VM reach the internet.
319 ///
320 /// Omitted by default — the right default for a daemon that needs none. Omitting it
321 /// omits the connector from the request; measured 2026-09-11 and 2026-09-12 the
322 /// platform still gave such a VM outbound network (`docs/PLATFORM.md`, "A VM launched
323 /// without the egress connector still has outbound network").
324 Egress,
325}
326
327impl ConnectorIntent {
328 /// Every intent, so a test can enumerate the complete set.
329 ///
330 /// Maintained by hand; the tests below assert its length, so an edit here is a
331 /// deliberate one. The set grew from two to four when TRAP-11 was revised — see the
332 /// module docs.
333 pub const ALL: [ConnectorIntent; 4] = [
334 ConnectorIntent::AllIngress,
335 ConnectorIntent::HttpIngress,
336 ConnectorIntent::ShellIngress,
337 ConnectorIntent::Egress,
338 ];
339
340 /// The connector's name as the ARN spells it.
341 ///
342 /// `INTERNET_EGRESS` rather than `EGRESS`: the wire name is not the intent's name,
343 /// which is the second reason this is a table rather than a `Display` derive.
344 pub fn wire_name(self) -> &'static str {
345 match self {
346 ConnectorIntent::AllIngress => "ALL_INGRESS",
347 ConnectorIntent::HttpIngress => "HTTP_INGRESS",
348 ConnectorIntent::ShellIngress => "SHELL_INGRESS",
349 ConnectorIntent::Egress => "INTERNET_EGRESS",
350 }
351 }
352
353 /// The fully-qualified ARN for this connector in `region`.
354 ///
355 /// One interpolation for both directions. The Python client's earlier shape derived
356 /// the egress ARN by string-replacing `ALL_INGRESS` inside the ingress one, which
357 /// produced a valid ARN only as long as the two names never became substrings of
358 /// each other (`sandbox.py:391-398`).
359 ///
360 /// The doubled `aws-network-connector` segment is not a typo: the resource type and
361 /// the resource name are both present, and the format is copied from
362 /// `sandbox.py:398` rather than reconstructed from the ARN grammar.
363 pub fn arn(self, region: &Region) -> String {
364 format!(
365 "arn:aws:lambda:{}:aws:network-connector:aws-network-connector:{}",
366 region.as_str(),
367 self.wire_name()
368 )
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn customer_connector_arn_length_uses_the_run_microvm_limit() {
378 let prefix = "arn:aws:lambda:us-east-1:123456789012:network-connector:";
379 let maximum = crate::constants::MAX_NETWORK_CONNECTOR_LEN;
380 let arn = format!("{prefix}{}", "x".repeat(maximum - prefix.len()));
381 require_egress_connector_arn(&arn, &Region::UsEast1).expect("at the request limit");
382 require_egress_connector_arn(&format!("{arn}x"), &Region::UsEast1)
383 .expect_err("one over the request limit");
384 }
385
386 /// An omitted connector cannot establish internet isolation.
387 #[test]
388 fn a_launch_with_no_connector_is_unsealed_and_not_sealed() {
389 let posture = EgressPosture::for_launch(false, false);
390 assert_eq!(posture, EgressPosture::Unsealed);
391 assert_eq!(posture.as_str(), "unsealed");
392 assert!(
393 !posture.is_sealed(),
394 "omitting the connector does not seal the VM; it is measured not to"
395 );
396 }
397
398 /// The other three postures, and the two rules that order them: `--egress` is `open`
399 /// whatever else was asked for, and proxy denial is only advisory.
400 #[test]
401 fn the_posture_table_is_the_whole_domain() {
402 assert_eq!(EgressPosture::for_launch(true, false), EgressPosture::Open);
403 assert_eq!(
404 EgressPosture::for_launch(true, true),
405 EgressPosture::Open,
406 "asking for egress is asking for egress; the advisory deny cannot dress it as \
407 anything narrower"
408 );
409 assert_eq!(
410 EgressPosture::for_launch(false, true),
411 EgressPosture::BestEffort
412 );
413 assert_eq!(EgressPosture::BestEffort.as_str(), "best-effort");
414 assert!(
415 !EgressPosture::BestEffort.is_sealed(),
416 "best effort is not a seal, and this predicate is the only place that decides it"
417 );
418 assert!(EgressPosture::Sealed.is_sealed());
419 }
420
421 /// Every posture renders a distinct non-empty label and a sentence that says what the
422 /// mechanism is, because a consumer branches on the label and a human reads the line.
423 #[test]
424 fn every_posture_has_a_distinct_label_and_a_description() {
425 let postures = [
426 EgressPosture::Open,
427 EgressPosture::Unsealed,
428 EgressPosture::BestEffort,
429 EgressPosture::Sealed,
430 ];
431 let labels: Vec<&str> = postures.iter().map(|posture| posture.as_str()).collect();
432 for (i, a) in labels.iter().enumerate() {
433 assert!(!a.is_empty());
434 for b in &labels[i + 1..] {
435 assert_ne!(a, b);
436 }
437 }
438 for posture in postures {
439 assert!(posture.describe().len() > 40, "{posture}");
440 }
441 // The two postures that are not seals must say so in words, not only in the
442 // predicate: the human line is what a reviewer reads.
443 for posture in [EgressPosture::Unsealed, EgressPosture::BestEffort] {
444 assert!(
445 posture.describe().contains("Not a seal"),
446 "{posture} must state that it is not a seal: {}",
447 posture.describe()
448 );
449 }
450 }
451
452 /// Defaults and explicitly empty launch options report the same posture.
453 #[test]
454 fn the_default_posture_is_the_derived_one() {
455 assert_eq!(
456 EgressPosture::default(),
457 EgressPosture::for_launch(false, false),
458 "a defaulted posture must be the posture of a launch that asks for nothing, \
459 derived from the same launch options"
460 );
461 }
462
463 /// Every label round-trips, and an unknown one is `None` rather than a guess.
464 ///
465 /// The registry stores the label, so a record written by a later version — or corrupted
466 /// — must not be read as a posture this version would then report as measured.
467 #[test]
468 fn every_label_round_trips_and_an_unknown_one_is_none() {
469 for posture in [
470 EgressPosture::Open,
471 EgressPosture::Unsealed,
472 EgressPosture::BestEffort,
473 EgressPosture::Sealed,
474 ] {
475 assert_eq!(EgressPosture::from_label(posture.as_str()), Some(posture));
476 }
477 assert_eq!(EgressPosture::from_label("sealed-ish"), None);
478 assert_eq!(EgressPosture::from_label(""), None);
479 assert_eq!(
480 EgressPosture::from_label("Open"),
481 None,
482 "the labels are the wire spelling, and case is part of it"
483 );
484 }
485
486 /// A connector ARN in `region`.
487 fn vpc(region: &str, index: usize) -> String {
488 format!("arn:aws:lambda:{region}:123456789012:network-connector:vpc-{index}")
489 }
490
491 /// **BIND-11 and BIND-13: the decision table.** The rows of `TABLE` in
492 /// `model/src/posture.rs`, at the wire ceiling of 10 connectors, each with the refusal's
493 /// words. A refusal is the launch's own message (the bdd scenarios and the fuzz harness
494 /// compare them against `Sandbox::run`).
495 ///
496 /// **Falsification** — 2026-09-24. Make `egress_posture_for` answer `Sealed` for a
497 /// connector-bearing request and the `(false, 1, ..)` rows go red; drop the ARN check and
498 /// the malformed rows answer `unsealed`. Both run and restored.
499 #[test]
500 fn the_posture_of_launch_options_is_the_models_table() {
501 use EgressPosture::{BestEffort, Open, Unsealed};
502 /// `(egress, connectors, malformed, deny, answer or the refusal's words)`.
503 type Row = (bool, usize, bool, bool, Result<EgressPosture, &'static str>);
504 let region = Region::UsEast1;
505 let malformed = "network-connector:vpc-0".to_string();
506 let table: [Row; 14] = [
507 (false, 0, false, false, Ok(Unsealed)),
508 (false, 0, false, true, Ok(BestEffort)),
509 (false, 1, false, false, Ok(Unsealed)),
510 (false, 1, false, true, Ok(BestEffort)),
511 (false, 10, false, false, Ok(Unsealed)),
512 (false, 1, true, false, Err("network connector ARN")),
513 (false, 11, false, false, Err("NetworkConnectorList ceiling")),
514 (false, 11, false, true, Err("NetworkConnectorList ceiling")),
515 (false, 11, true, false, Err("network connector ARN")),
516 (true, 0, false, false, Ok(Open)),
517 (true, 0, false, true, Err("opposite things")),
518 (
519 true,
520 1,
521 false,
522 false,
523 Err("INTERNET_EGRESS cannot be combined"),
524 ),
525 (true, 1, false, true, Err("opposite things")),
526 (
527 true,
528 1,
529 true,
530 false,
531 Err("INTERNET_EGRESS cannot be combined"),
532 ),
533 ];
534 for (egress, count, bad, deny, expected) in table {
535 let mut connectors: Vec<String> = (0..count).map(|i| vpc("us-east-1", i)).collect();
536 if bad {
537 connectors[0] = malformed.clone();
538 }
539 let answer = egress_posture_for(egress, &connectors, deny, Some(®ion));
540 let row = format!("egress={egress} connectors={count} malformed={bad} deny={deny}");
541 match (answer, expected) {
542 (Ok(posture), Ok(want)) => assert_eq!(posture, want, "{row}"),
543 (Err(error), Err(needle)) => {
544 assert_eq!(error.kind(), crate::ErrorKind::InvalidArg, "{row}: {error}");
545 assert!(error.to_string().contains(needle), "{row}: {error}");
546 }
547 (answer, expected) => panic!("{row}: {answer:?}, expected {expected:?}"),
548 }
549 }
550 }
551
552 /// **BIND-11: no launch option is a seal.** The whole option space at and past the ceiling.
553 #[test]
554 fn no_launch_options_answer_sealed() {
555 for egress in [false, true] {
556 for deny in [false, true] {
557 for count in 0..=crate::constants::MAX_NETWORK_CONNECTORS + 1 {
558 let connectors: Vec<String> = (0..count).map(|i| vpc("us-east-1", i)).collect();
559 if let Ok(posture) =
560 egress_posture_for(egress, &connectors, deny, Some(&Region::UsEast1))
561 {
562 assert!(!posture.is_sealed(), "{egress} {count} {deny}");
563 }
564 }
565 }
566 }
567 }
568
569 /// **BIND-13: without a region, each ARN is checked against the region it names**; with
570 /// one, an ARN in another region is the launch's own refusal.
571 #[test]
572 fn a_connector_is_checked_against_the_launch_region_when_there_is_one() {
573 let elsewhere = [vpc("eu-west-1", 0)];
574 assert_eq!(
575 egress_posture_for(false, &elsewhere, false, None).expect("a well-formed ARN"),
576 EgressPosture::Unsealed
577 );
578 let error = egress_posture_for(false, &elsewhere, false, Some(&Region::UsEast1))
579 .expect_err("an ARN from another region cannot attach to a us-east-1 launch");
580 assert!(error.to_string().contains("in us-east-1"), "{error}");
581 let error = egress_posture_for(false, &["not-an-arn".into()], false, None)
582 .expect_err("a string that is no ARN names no region either");
583 assert_eq!(error.kind(), crate::ErrorKind::InvalidArg);
584 }
585
586 /// The exact ARN, as a literal, for the region the measurements were taken in. The
587 /// format came from a measurement rather than from the ARN grammar, so a
588 /// reconstruction of it in the test would agree with a reconstruction in the code.
589 #[test]
590 fn the_ingress_arn_is_the_measured_literal() {
591 assert_eq!(
592 ConnectorIntent::AllIngress.arn(&Region::UsEast1),
593 "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:ALL_INGRESS"
594 );
595 }
596
597 /// The egress ARN, likewise — and note the wire name is `INTERNET_EGRESS` while the
598 /// variant is `Egress`, which is the thing a `Display` derive would have got wrong.
599 #[test]
600 fn the_egress_arn_uses_the_internet_egress_wire_name() {
601 assert_eq!(
602 ConnectorIntent::Egress.arn(&Region::UsEast1),
603 "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:INTERNET_EGRESS"
604 );
605 }
606
607 /// The region is interpolated rather than fixed, for every region including the
608 /// escape hatch. TRAP-4 is "for the request region", and a hardcoded `us-east-1`
609 /// would pass both tests above.
610 #[test]
611 fn the_arn_carries_the_request_region_for_every_region() {
612 for region in crate::region::MICROVM_REGIONS {
613 let arn = ConnectorIntent::AllIngress.arn(®ion);
614 assert!(
615 arn.contains(&format!(":lambda:{}:aws:", region.as_str())),
616 "{arn}"
617 );
618 }
619 let unlisted = Region::unlisted("me-south-1");
620 assert!(
621 ConnectorIntent::Egress
622 .arn(&unlisted)
623 .contains(":lambda:me-south-1:aws:"),
624 "the escape hatch still derives an ARN for its own region"
625 );
626 }
627
628 /// TRAP-11, rewritten on the ground that holds. This test used to assert no intent
629 /// names `SHELL_INGRESS`, standing on the claim that the shell was a console-only
630 /// debugging path — a claim `docs/PLATFORM.md` (measured 2026-08-15) refutes: the
631 /// shell endpoint is a real PTY and programmatically drivable. What holds instead is
632 /// that **one interactive session is not programmatic exec**, so the variant exists,
633 /// exactly one intent renders it, and the check on the rendered ARNs stays — a
634 /// variant named something else must not render `SHELL_INGRESS` either.
635 ///
636 /// The other half of the revised guard — a launch carries exactly the connectors its
637 /// caller asked for — lives with the lifecycle test in `microvm.rs`.
638 ///
639 /// **Falsification** — add a second variant whose wire name contains `SHELL`, or
640 /// rename [`ConnectorIntent::ShellIngress`]'s wire name, and this fails.
641 #[test]
642 fn shell_ingress_is_one_deliberate_intent() {
643 assert_eq!(
644 ConnectorIntent::ALL.len(),
645 4,
646 "four intents, and shell is deliberately one of them"
647 );
648 let shells: Vec<ConnectorIntent> = ConnectorIntent::ALL
649 .into_iter()
650 .filter(|intent| intent.wire_name().contains("SHELL"))
651 .collect();
652 assert_eq!(
653 shells,
654 vec![ConnectorIntent::ShellIngress],
655 "exactly one intent names the shell, and it is the one that says so"
656 );
657 assert_eq!(
658 ConnectorIntent::ShellIngress.arn(&Region::UsEast1),
659 "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:SHELL_INGRESS"
660 );
661 }
662
663 /// `HTTP_INGRESS`, the measured literal — the finer-grained ingress that pairs with
664 /// the shell connector, since `ALL_INGRESS` cannot combine with either
665 /// (`docs/PLATFORM.md`, measured 2026-08-15).
666 #[test]
667 fn the_http_ingress_arn_is_the_measured_literal() {
668 assert_eq!(
669 ConnectorIntent::HttpIngress.arn(&Region::UsEast1),
670 "arn:aws:lambda:us-east-1:aws:network-connector:aws-network-connector:HTTP_INGRESS"
671 );
672 }
673
674 /// No two intents render the same ARN. The string-replace shape this replaced
675 /// could produce two identical ARNs if the names ever became substrings of one
676 /// another, and that is the failure this asserts against — now across the whole
677 /// set rather than the original pair.
678 #[test]
679 fn no_two_intents_render_the_same_arn() {
680 let arns: Vec<String> = ConnectorIntent::ALL
681 .iter()
682 .map(|intent| intent.arn(&Region::EuWest1))
683 .collect();
684 for (i, a) in arns.iter().enumerate() {
685 for b in &arns[i + 1..] {
686 assert_ne!(a, b);
687 }
688 }
689 assert!(!arns[0].contains("INTERNET"), "{}", arns[0]);
690 }
691
692 /// Every derived ARN clears the model's `NetworkConnector` bounds (min 1, max 2048),
693 /// including for the longest region name. The model states no pattern, so length is
694 /// the only constraint there is to check — and it is checked here rather than
695 /// trusted because a 2048-character ARN would be rejected on the wire with the same
696 /// "malformed" message a bare name gets.
697 #[test]
698 fn every_derived_arn_fits_the_models_connector_bounds() {
699 for region in crate::region::MICROVM_REGIONS {
700 for intent in ConnectorIntent::ALL {
701 let arn = intent.arn(®ion);
702 assert!((1..=2048).contains(&arn.len()), "{arn}");
703 assert!(arn.starts_with("arn:aws:lambda:"), "{arn}");
704 }
705 }
706 }
707}