Skip to content

Commit d31a034

Browse files
committed
fix(gateway): support delayed podman bridge binds
Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 8f5b374 commit d31a034

12 files changed

Lines changed: 387 additions & 24 deletions

File tree

architecture/gateway.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,24 @@ reflection, non-callback inference APIs, and HTTP routes before normal request
6666
authentication. The operator-configured primary listener retains the full
6767
multiplexed API surface.
6868

69+
Rootful Podman can report a private bridge gateway before netavark assigns that
70+
address. If an exact built-in Podman callback bind fails with
71+
`EADDRNOTAVAIL`, the Linux gateway retries the same address with
72+
`IP_FREEBIND`. The listener remains callback-only and becomes reachable when
73+
the first sandbox materializes the bridge. If delayed exact binding also fails
74+
while the gateway itself runs in a container, the gateway replaces the
75+
loopback primary and missing bridge sockets with one IPv4 wildcard socket. The
76+
wildcard defaults to callback-only authorization; only traffic addressed to
77+
the configured loopback endpoint receives primary scope. This keeps user and
78+
administrator APIs off the container's non-loopback interfaces, but it does
79+
make the sandbox-callable gRPC surface reachable on every IPv4 interface in
80+
that container namespace for the lifetime of the gateway process. Sandbox
81+
mTLS/JWT authentication and the RPC allowlist remain mandatory defenses.
82+
Delayed binding does not apply to rootless Podman, an explicit
83+
`host_gateway_ip`, Docker or external drivers, public callback addresses, or
84+
bind errors other than `EADDRNOTAVAIL`. A host gateway never uses the wildcard
85+
fallback.
86+
6987
The `rpc_auth` classification is also the source of truth for negotiated
7088
listener exposure: marking an RPC as `sandbox` or `dual` makes it callable on
7189
these listeners. Review such changes as both authorization and network-surface

crates/openshell-driver-docker/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,6 +1979,7 @@ impl ComputeDriver for DockerComputeDriver {
19791979
DockerGatewayRoute::HostGateway => "docker host-gateway IPv4 loopback",
19801980
}
19811981
.to_string(),
1982+
allow_delayed_bind: false,
19821983
selector: Some(Selector::ExactBindAddress(bind_address.to_string())),
19831984
}]
19841985
});

crates/openshell-driver-podman/NETWORKING.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,25 @@ the supervisor's RPCs. Otherwise, it creates an additional listener that
281281
exposes only the gateway's sandbox-callable gRPC methods. Operator, health,
282282
reflection, and HTTP requests must use the primary listener.
283283

284+
Netavark may not assign a rootful managed bridge gateway until the first
285+
sandbox joins the network. If that exact private callback address fails to bind
286+
with `EADDRNOTAVAIL`, the Linux gateway uses `IP_FREEBIND` to bind the same
287+
address before the interface exists. The listener remains callback-only and
288+
becomes reachable when netavark materializes the bridge. Only the in-process
289+
built-in Podman driver can mark its discovered rootful managed-bridge address
290+
as eligible; rootless Podman, explicit `host_gateway_ip` values, and external
291+
drivers cannot activate delayed binding.
292+
293+
If delayed exact binding fails while both the gateway and rootful Podman run
294+
inside another Linux container, the gateway uses one scoped IPv4 wildcard
295+
listener for that process. Connections addressed to loopback retain primary
296+
scope; connections addressed to the Podman bridge or any other IPv4 interface
297+
are callback-only. This exposes the sandbox-callable gRPC surface on every
298+
IPv4 interface in the outer container namespace, so deployments should still
299+
restrict that namespace at the container-network boundary. A gateway running
300+
directly on a host never uses the wildcard fallback. Neither strategy exposes
301+
operator, health, reflection, or HTTP routes on non-loopback interfaces.
302+
284303
### Layer 3 Inner Sandbox Network Namespace
285304

286305
Inside the container, the supervisor creates another network namespace for the

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ impl PodmanComputeDriver {
561561
reason:
562562
"Podman rootless pasta callback uses the host default-route interface"
563563
.to_string(),
564+
allow_delayed_bind: false,
564565
selector: Some(Selector::DefaultRouteInterface(
565566
GatewayDefaultRouteInterfaceRequirement {},
566567
)),
@@ -586,6 +587,10 @@ impl PodmanComputeDriver {
586587
})?;
587588
Ok(vec![GatewayListenerRequirement {
588589
reason: format!("Podman network '{}' host gateway", self.config.network_name),
590+
// A rootful managed bridge can be created after gateway
591+
// startup. An explicit override is operator-owned, and
592+
// rootless networking must never broaden the listener.
593+
allow_delayed_bind: !self.rootless && self.config.host_gateway_ip.trim().is_empty(),
589594
selector: Some(Selector::ExactBindAddress(
590595
SocketAddr::new(gateway_ip, callback_port).to_string(),
591596
)),
@@ -596,6 +601,7 @@ impl PodmanComputeDriver {
596601
Ok(vec![GatewayListenerRequirement {
597602
reason: "Podman machine callback forwarding terminates on gateway loopback"
598603
.to_string(),
604+
allow_delayed_bind: false,
599605
selector: Some(Selector::LoopbackInterface(
600606
GatewayLoopbackInterfaceRequirement {},
601607
)),

crates/openshell-server/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ k8s-openapi = { workspace = true }
3434

3535
# Async runtime
3636
tokio = { workspace = true }
37-
socket2 = { workspace = true }
37+
socket2 = { workspace = true, features = ["all"] }
3838
nix = { workspace = true }
3939

4040
# gRPC

crates/openshell-server/src/compute/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ pub enum GatewayListenerRequirement {
138138
address: SocketAddr,
139139
driver_name: String,
140140
reason: String,
141+
allow_delayed_bind: bool,
141142
},
142143
DefaultRouteInterface {
143144
driver_name: String,
@@ -149,6 +150,12 @@ pub enum GatewayListenerRequirement {
149150
},
150151
}
151152

153+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154+
pub enum GatewayListenerBindPolicy {
155+
Deny,
156+
TrustedBuiltinPodman,
157+
}
158+
152159
impl GatewayListenerRequirement {
153160
pub fn driver_name(&self) -> &str {
154161
match self {
@@ -587,6 +594,7 @@ impl ComputeRuntime {
587594
driver_name: String,
588595
driver: SharedComputeDriver,
589596
driver_process: Option<Arc<ManagedDriverProcess>>,
597+
listener_bind_policy: GatewayListenerBindPolicy,
590598
store: Arc<Store>,
591599
sandbox_index: SandboxIndex,
592600
sandbox_watch_bus: SandboxWatchBus,
@@ -642,6 +650,9 @@ impl ComputeRuntime {
642650
address,
643651
driver_name: driver_name.clone(),
644652
reason: requirement.reason,
653+
allow_delayed_bind: listener_bind_policy
654+
== GatewayListenerBindPolicy::TrustedBuiltinPodman
655+
&& requirement.allow_delayed_bind,
645656
})
646657
}
647658
Selector::DefaultRouteInterface(_) => {
@@ -724,6 +735,7 @@ impl ComputeRuntime {
724735
endpoint.name,
725736
driver,
726737
endpoint.driver_process,
738+
GatewayListenerBindPolicy::Deny,
727739
store,
728740
sandbox_index,
729741
sandbox_watch_bus,
@@ -10487,6 +10499,7 @@ mod tests {
1048710499
"test-driver".to_string(),
1048810500
Arc::new(TestDriver::default()),
1048910501
None,
10502+
GatewayListenerBindPolicy::Deny,
1049010503
store,
1049110504
SandboxIndex::new(),
1049210505
SandboxWatchBus::new(),
@@ -10674,6 +10687,7 @@ mod tests {
1067410687
address: "172.19.0.1:17670".parse().unwrap(),
1067510688
driver_name: "docker".to_string(),
1067610689
reason: "external driver managed bridge".to_string(),
10690+
allow_delayed_bind: false,
1067710691
}]
1067810692
);
1067910693

0 commit comments

Comments
 (0)