Skip to main content

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(&region));
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(&region);
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(&region);
705                assert!((1..=2048).contains(&arn.len()), "{arn}");
706                assert!(arn.starts_with("arn:aws:lambda:"), "{arn}");
707            }
708        }
709    }
710}