<!-- nornir:class: design -->
# OCI network mode — the airgap `--network none` wire (Skidbladnir's #1 blocker)
> **Why.** Skidbladnir's airgap container route needs a container that reaches **no
> network** — `podman run --network none`. jera's `ContainerSpec` already *renders*
> that choice (`net: NetMode`, `network_arg()` → `--network none`), but draupnir owns
> the ONE OCI engine and its create body carried `port_bindings` + `binds` and **no
> `network_mode`** — so jera could express the airgap but it never reached the live
> `podman create`. This threads the net mode onto the create body, **additively**, so
> `NetMode::None` actually cuts the container off from egress at the real run.
## The knob (a `net` field on `BootSpec`, `NetMode::Default` = unchanged)
Every `draupnir::BootSpec` is built through a constructor + builders — no consumer
(nornir/korp/knut/tunnr/jera/Skidbladnir) constructs it by raw struct literal (audited:
the only raw `BootSpec {…}` literals in the constellation are each repo's *own* local
`BootSpec` type). So a new field is safe as long as the four constructors default it.
`net` defaults to `NetMode::Default`, which renders **no** `--network` flag and leaves
the create body **byte-for-byte identical** to a spec that never named a net mode.
```rust
pub enum NetMode { Default, None, Host, Bridge } // Default = no --network flag
impl NetMode {
pub fn oci_value(self) -> Option<&'static str>; // None | "none" | "host" | "bridge"
pub fn is_default(self) -> bool; // parity guard
pub fn from_oci_value(v: Option<&str>) -> Self; // the cross-repo wire (jera passes this)
}
pub struct BootSpec { /* …, */ pub net: NetMode }
impl BootSpec { pub fn with_net(self, net: NetMode) -> Self; } // builder
```
`NetMode` mirrors jera's `ContainerSpec::NetMode` field-for-field. jera passes its
rendered value across the repo boundary as the **OCI string** (`"none"`/`"host"`/
`"bridge"`) via `NetMode::from_oci_value(jera_spec.net.oci_value())` — no cross-repo
variant coupling. `net` is **container-only**: `validate()` rejects a non-default net
on KVM/Redfish (parity with the `cmd`/`ports` container-only guards).
## The wire (rides the existing bind delegate chain)
The create body already had a `*_with_binds` delegate chain; net rides it with one
more `*_and_net` layer, so the bind-free / net-free entries stay byte-identical.
| net-free (unchanged) | net-aware (new) | note |
|---|---|---|
| `Engine::create_body_with_binds` | `Engine::create_body_with_binds_and_net` | pure body builder; `None` net ⇒ identical body (`HostConfig.network_mode` stays `None`) |
| `Engine::create_and_start_with_binds` | `Engine::create_and_start_with_binds_and_net` | create+start with `HostConfig.network_mode` |
| — | `Boot::boot` / `boot_with_binds` **read `spec.net`** | both boot paths thread `spec.net.oci_value()` onto the create body |
Because both boot paths read `spec.net`, **every** downstream entry —
`Boot::boot`, `boot_with_binds`, `run_to_completion`, `run_to_completion_with_binds` —
honours the net mode automatically. A consumer just sets `spec.net`.
```
jera ContainerSpec { net: NetMode::None } draupnir (ONE OCI engine, bollard)
┌──────────────────────────────────────┐ ┌───────────────────────────────────┐
│ to_draupnir(): │ │ boot / boot_with_binds read spec.net│
│ d.net = NetMode::from_oci_value( │ ───────▶ │ → create_and_start_with_binds_and │
│ self.net.oci_value()) // "none" │ │ _net(…, Some("none")) │
│ run_to_completion_with_binds(&d, …) │ │ → HostConfig.network_mode="none" │
└──────────────────────────────────────┘ │ → podman create --network none │
(was: expressed but dropped at the run) └───────────────────────────────────┘
```
## The jera handoff (one line)
jera's `ContainerSpec::to_draupnir()` builds the `draupnir::BootSpec` that BOTH its
boot leg (`DraupnirContainerBootRunner`) and its run-to-completion leg
(`run_to_completion_with_binds`) consume. One added line carries the net mode through
both:
```rust
// in jera ContainerSpec::to_draupnir()
d.net = draupnir::NetMode::from_oci_value(self.net.oci_value());
```
After that line, `NetMode::None` reaches the live podman run, and Skidbladnir can
un-gate its airgap container route (drop the `--network none` fallback to a direct
`podman` subprocess).
## Status
- draupnir: **landed** — builds default (no bollard) + `--features backend-oci`;
RED-when-broken engine test `create_body_sets_network_mode_none_and_default_is_byte_identical`
+ testmatrix cell `net_mode_renders_oci_value_and_is_container_only` green.
- jera: **one-line `to_draupnir()` change pending** (above) — then the net flows.
- Skidbladnir: un-gate the airgap route once jera's line lands.