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