Skip to content

Commit 2ea3982

Browse files
committed
fix(podman): resolve local callbacks to loopback on Linux
Since RFC 0012 (#2942), the callback-capable supervisor always runs in the host network namespace, for both rootful and rootless Podman. The driver still requested a callback listener via bridge-network gateway inspection (rootful) or default-route negotiation (rootless pasta), and aliased host.containers.internal/host.openshell.internal through Podman's host-gateway resolver. On multi-homed hosts, Podman's host-gateway resolution and the driver's own negotiation can each pick a different interface than the one the supervisor actually shares with the gateway (loopback), leaving sandbox creation unreachable. Request loopback directly for the automatic local-callback path on Linux, and resolve the host aliases to 127.0.0.1 to match, regardless of rootful/rootless mode or which rootless network helper Podman reports. An explicit host_gateway_ip override is unaffected. macOS Podman Machine behavior (gvproxy host-loopback forwarding) is unchanged. Fixes #3412 Signed-off-by: politerealism <burdcat17@gmail.com>
1 parent 1fe79f5 commit 2ea3982

9 files changed

Lines changed: 183 additions & 215 deletions

File tree

‎architecture/gateway.md‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1026,13 +1026,12 @@ system entry instead of pretending to delete package-manager owned state.
10261026
the host can bind that bridge IP.
10271027
- Podman-backed macOS gateways use gvproxy's host-loopback IP for sandbox host
10281028
aliases by default so stale Podman machine images do not need Podman's
1029-
`host-gateway` resolver. Linux Podman keeps the resolver unless
1030-
`host_gateway_ip` is configured. Rootful Podman can request its exact bridge
1031-
gateway listener. Rootless Podman explicitly reporting pasta requests the
1032-
private IPv4 source selected by the host default route rather than an
1033-
arbitrary private interface. Slirp4netns, other helpers, and missing helper
1034-
metadata fail closed for local callbacks until a rootless-network namespace
1035-
relay is available.
1029+
`host-gateway` resolver. Linux Podman resolves sandbox host aliases to
1030+
loopback directly by default, for both rootful and rootless operation: the
1031+
callback-capable supervisor always shares the host network namespace with
1032+
the gateway (RFC 0012), so loopback is reachable independent of host
1033+
topology and rootless network helper. An explicit `host_gateway_ip`
1034+
overrides either platform default with a concrete address.
10361035
- Gateway restarts recover persisted objects from storage, but live relay
10371036
streams must be re-established by supervisors.
10381037
- User-facing behavior changes must update published docs in `docs/`; this file

‎crates/openshell-driver-podman/NETWORKING.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@ the upstream corporate proxy apply only to the supervisor. The gateway's SSH
3131
tunnel uses the supervisor relay over its private Unix socket, so the driver
3232
does not publish a supervisor port.
3333

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

3942
These runtime-managed network helpers are outside the workload trust boundary.
4043
Sharing the workload's user namespace preserves volume UID/GID mapping; it

‎crates/openshell-driver-podman/src/client.rs‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -767,26 +767,6 @@ impl PodmanClient {
767767
.await
768768
}
769769

770-
/// Inspect a network and return the gateway IP of its first subnet.
771-
///
772-
/// The gateway IP is the host's address on the bridge network, used by
773-
/// sandbox containers to call back to the gateway server.
774-
pub async fn network_gateway_ip(&self, name: &str) -> Result<Option<String>, PodmanApiError> {
775-
validate_name(name)?;
776-
let encoded = url_encode(name);
777-
let path = format!("/libpod/networks/{encoded}/json");
778-
let resp: Value = self.request_json(hyper::Method::GET, &path, None).await?;
779-
// The response has "subnets": [{"gateway": "10.89.1.1", "subnet": "..."}]
780-
let gateway = resp
781-
.get("subnets")
782-
.and_then(|s| s.as_array())
783-
.and_then(|arr| arr.first())
784-
.and_then(|sub| sub.get("gateway"))
785-
.and_then(|g| g.as_str())
786-
.map(String::from);
787-
Ok(gateway)
788-
}
789-
790770
// ── Image operations ────────────────────────────────────────────────
791771

792772
/// Pull an image if it is not already present locally.

‎crates/openshell-driver-podman/src/config.rs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,15 @@ pub struct PodmanComputeConfig {
5353
pub network_name: String,
5454
/// Host gateway IP used for sandbox host aliases.
5555
///
56-
/// Empty uses Podman's `host-gateway` resolver. macOS defaults to
57-
/// gvproxy's host-loopback IP because stale Podman machines may fail to
58-
/// resolve `host-gateway` while still serving `host.containers.internal`
59-
/// through gvproxy.
56+
/// Empty on Linux resolves the alias to loopback directly: since RFC
57+
/// 0012, the callback-capable supervisor always shares the host network
58+
/// namespace with the gateway, so loopback is always reachable and
59+
/// independent of host topology (see #3412 for the multi-homed-host
60+
/// failure this replaced). Empty on macOS uses gvproxy's host-loopback
61+
/// IP because stale Podman machines may fail to resolve `host-gateway`
62+
/// while still serving `host.containers.internal` through gvproxy. Set
63+
/// explicitly to override either default, e.g. for a containerized or
64+
/// remote gateway.
6065
pub host_gateway_ip: String,
6166
/// Container stop timeout in seconds (SIGTERM → SIGKILL).
6267
pub stop_timeout_secs: u32,

‎crates/openshell-driver-podman/src/container.rs‎

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,16 +1612,32 @@ fn provider_spiffe_workload_api_socket_mount_source(config: &PodmanComputeConfig
16121612

16131613
fn hostadd_entries(config: &PodmanComputeConfig) -> Vec<String> {
16141614
let host_gateway_ip = config.host_gateway_ip.trim();
1615-
if host_gateway_ip.is_empty() {
1615+
if !host_gateway_ip.is_empty() {
16161616
return vec![
1617-
"host.containers.internal:host-gateway".into(),
1618-
"host.openshell.internal:host-gateway".into(),
1617+
format!("host.containers.internal:{host_gateway_ip}"),
1618+
format!("host.openshell.internal:{host_gateway_ip}"),
16191619
];
16201620
}
16211621

1622+
if cfg!(target_os = "linux") {
1623+
// Since RFC 0012 (#2942), the callback-capable supervisor always
1624+
// shares the host network namespace with the gateway. Resolve the
1625+
// alias to loopback directly instead of Podman's `host-gateway`
1626+
// magic value, which independently picks a host interface and can
1627+
// select a different, unreachable one on multi-homed hosts
1628+
// (#3412). `gateway_listener_requirements` requests a matching
1629+
// loopback listener for the same reason.
1630+
return vec![
1631+
"host.containers.internal:127.0.0.1".into(),
1632+
"host.openshell.internal:127.0.0.1".into(),
1633+
];
1634+
}
1635+
1636+
// Non-Linux (Podman Machine): gvproxy already forwards `host-gateway`
1637+
// to the VM host's loopback, so the magic value resolves correctly.
16221638
vec![
1623-
format!("host.containers.internal:{host_gateway_ip}"),
1624-
format!("host.openshell.internal:{host_gateway_ip}"),
1639+
"host.containers.internal:host-gateway".into(),
1640+
"host.openshell.internal:host-gateway".into(),
16251641
]
16261642
}
16271643

@@ -2600,7 +2616,44 @@ mod tests {
26002616
}
26012617

26022618
#[test]
2619+
#[cfg(target_os = "linux")]
2620+
fn container_spec_injects_host_aliases() {
2621+
// Since RFC 0012 (#2942), the supervisor shares the host network
2622+
// namespace with the gateway, so the automatic (no explicit
2623+
// host_gateway_ip) alias resolves directly to loopback rather than
2624+
// Podman's `host-gateway` magic value, which can select a
2625+
// different, unreachable interface on multi-homed hosts (#3412).
2626+
let sandbox = test_sandbox("test-id", "test-name");
2627+
let config = test_config();
2628+
let spec = build_container_spec(&sandbox, &config);
2629+
2630+
let hostadd: Vec<&str> = spec["hostadd"]
2631+
.as_array()
2632+
.expect("hostadd should be an array")
2633+
.iter()
2634+
.filter_map(|v| v.as_str())
2635+
.collect();
2636+
2637+
assert!(
2638+
hostadd.contains(&"host.containers.internal:127.0.0.1"),
2639+
"missing Podman host alias"
2640+
);
2641+
assert!(
2642+
hostadd.contains(&"host.openshell.internal:127.0.0.1"),
2643+
"missing OpenShell stable host alias"
2644+
);
2645+
assert!(
2646+
!hostadd.contains(&"host.docker.internal:127.0.0.1"),
2647+
"Podman should not inject Docker's host alias"
2648+
);
2649+
}
2650+
2651+
#[test]
2652+
#[cfg(not(target_os = "linux"))]
26032653
fn container_spec_injects_host_aliases() {
2654+
// Podman Machine's gvproxy forwards the `host-gateway` magic value
2655+
// to the VM host's loopback correctly, so the automatic path keeps
2656+
// using it unchanged on non-Linux hosts.
26042657
let sandbox = test_sandbox("test-id", "test-name");
26052658
let config = test_config();
26062659
let spec = build_container_spec(&sandbox, &config);

0 commit comments

Comments
 (0)