Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -1026,13 +1026,12 @@ system entry instead of pretending to delete package-manager owned state.
the host can bind that bridge IP.
- Podman-backed macOS gateways use gvproxy's host-loopback IP for sandbox host
aliases by default so stale Podman machine images do not need Podman's
`host-gateway` resolver. Linux Podman keeps the resolver unless
`host_gateway_ip` is configured. Rootful Podman can request its exact bridge
gateway listener. Rootless Podman explicitly reporting pasta requests the
private IPv4 source selected by the host default route rather than an
arbitrary private interface. Slirp4netns, other helpers, and missing helper
metadata fail closed for local callbacks until a rootless-network namespace
relay is available.
`host-gateway` resolver. Linux Podman resolves sandbox host aliases to
loopback directly by default, for both rootful and rootless operation: the
callback-capable supervisor always shares the host network namespace with
the gateway (RFC 0012), so loopback is reachable independent of host
topology and rootless network helper. An explicit `host_gateway_ip`
overrides either platform default with a concrete address.
- Gateway restarts recover persisted objects from storage, but live relay
streams must be re-established by supervisors.
- User-facing behavior changes must update published docs in `docs/`; this file
Expand Down
11 changes: 7 additions & 4 deletions crates/openshell-driver-podman/NETWORKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ the upstream corporate proxy apply only to the supervisor. The gateway's SSH
tunnel uses the supervisor relay over its private Unix socket, so the driver
does not publish a supervisor port.

Rootful Podman uses the configured bridge and its gateway address. Rootless
local callbacks require the existing pasta path; slirp4netns or unknown helpers
require an explicitly remote `grpc_endpoint`. On macOS, Podman Machine provides
the runtime and host-loopback forwarding.
On Linux, the supervisor's host network namespace is the same as the
gateway's, for both rootful and rootless Podman, so the automatic callback
route resolves the sandbox host aliases to loopback directly rather than
Podman's `host-gateway` resolver, which can select a different, unreachable
interface on multi-homed hosts. Set `host_gateway_ip` explicitly to override
with a different address (for example, a containerized or remote gateway).
On macOS, Podman Machine provides the runtime and host-loopback forwarding.

These runtime-managed network helpers are outside the workload trust boundary.
Sharing the workload's user namespace preserves volume UID/GID mapping; it
Expand Down
20 changes: 0 additions & 20 deletions crates/openshell-driver-podman/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -767,26 +767,6 @@ impl PodmanClient {
.await
}

/// Inspect a network and return the gateway IP of its first subnet.
///
/// The gateway IP is the host's address on the bridge network, used by
/// sandbox containers to call back to the gateway server.
pub async fn network_gateway_ip(&self, name: &str) -> Result<Option<String>, PodmanApiError> {
validate_name(name)?;
let encoded = url_encode(name);
let path = format!("/libpod/networks/{encoded}/json");
let resp: Value = self.request_json(hyper::Method::GET, &path, None).await?;
// The response has "subnets": [{"gateway": "10.89.1.1", "subnet": "..."}]
let gateway = resp
.get("subnets")
.and_then(|s| s.as_array())
.and_then(|arr| arr.first())
.and_then(|sub| sub.get("gateway"))
.and_then(|g| g.as_str())
.map(String::from);
Ok(gateway)
}

// ── Image operations ────────────────────────────────────────────────

/// Pull an image if it is not already present locally.
Expand Down
13 changes: 9 additions & 4 deletions crates/openshell-driver-podman/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@ pub struct PodmanComputeConfig {
pub network_name: String,
/// Host gateway IP used for sandbox host aliases.
///
/// Empty uses Podman's `host-gateway` resolver. macOS defaults to
/// gvproxy's host-loopback IP because stale Podman machines may fail to
/// resolve `host-gateway` while still serving `host.containers.internal`
/// through gvproxy.
/// Empty on Linux resolves the alias to loopback directly: since RFC
/// 0012, the callback-capable supervisor always shares the host network
/// namespace with the gateway, so loopback is always reachable and
/// independent of host topology (see #3412 for the multi-homed-host
/// failure this replaced). Empty on macOS uses gvproxy's host-loopback
/// IP because stale Podman machines may fail to resolve `host-gateway`
/// while still serving `host.containers.internal` through gvproxy. Set
/// explicitly to override either default, e.g. for a containerized or
/// remote gateway.
pub host_gateway_ip: String,
/// Container stop timeout in seconds (SIGTERM → SIGKILL).
pub stop_timeout_secs: u32,
Expand Down
63 changes: 58 additions & 5 deletions crates/openshell-driver-podman/src/container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1612,16 +1612,32 @@ fn provider_spiffe_workload_api_socket_mount_source(config: &PodmanComputeConfig

fn hostadd_entries(config: &PodmanComputeConfig) -> Vec<String> {
let host_gateway_ip = config.host_gateway_ip.trim();
if host_gateway_ip.is_empty() {
if !host_gateway_ip.is_empty() {
return vec![
"host.containers.internal:host-gateway".into(),
"host.openshell.internal:host-gateway".into(),
format!("host.containers.internal:{host_gateway_ip}"),
format!("host.openshell.internal:{host_gateway_ip}"),
];
}

if cfg!(target_os = "linux") {
// Since RFC 0012 (#2942), the callback-capable supervisor always
// shares the host network namespace with the gateway. Resolve the
// alias to loopback directly instead of Podman's `host-gateway`
// magic value, which independently picks a host interface and can
// select a different, unreachable one on multi-homed hosts
// (#3412). `gateway_listener_requirements` requests a matching
// loopback listener for the same reason.
return vec![
"host.containers.internal:127.0.0.1".into(),
"host.openshell.internal:127.0.0.1".into(),
];
}

// Non-Linux (Podman Machine): gvproxy already forwards `host-gateway`
// to the VM host's loopback, so the magic value resolves correctly.
vec![
format!("host.containers.internal:{host_gateway_ip}"),
format!("host.openshell.internal:{host_gateway_ip}"),
"host.containers.internal:host-gateway".into(),
"host.openshell.internal:host-gateway".into(),
]
}

Expand Down Expand Up @@ -2600,7 +2616,44 @@ mod tests {
}

#[test]
#[cfg(target_os = "linux")]
fn container_spec_injects_host_aliases() {
// Since RFC 0012 (#2942), the supervisor shares the host network
// namespace with the gateway, so the automatic (no explicit
// host_gateway_ip) alias resolves directly to loopback rather than
// Podman's `host-gateway` magic value, which can select a
// different, unreachable interface on multi-homed hosts (#3412).
let sandbox = test_sandbox("test-id", "test-name");
let config = test_config();
let spec = build_container_spec(&sandbox, &config);

let hostadd: Vec<&str> = spec["hostadd"]
.as_array()
.expect("hostadd should be an array")
.iter()
.filter_map(|v| v.as_str())
.collect();

assert!(
hostadd.contains(&"host.containers.internal:127.0.0.1"),
"missing Podman host alias"
);
assert!(
hostadd.contains(&"host.openshell.internal:127.0.0.1"),
"missing OpenShell stable host alias"
);
assert!(
!hostadd.contains(&"host.docker.internal:127.0.0.1"),
"Podman should not inject Docker's host alias"
);
}

#[test]
#[cfg(not(target_os = "linux"))]
fn container_spec_injects_host_aliases() {
// Podman Machine's gvproxy forwards the `host-gateway` magic value
// to the VM host's loopback correctly, so the automatic path keeps
// using it unchanged on non-Linux hosts.
let sandbox = test_sandbox("test-id", "test-name");
let config = test_config();
let spec = build_container_spec(&sandbox, &config);
Expand Down
Loading
Loading