Skip to content
Draft
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
28 changes: 19 additions & 9 deletions .github/workflows/conformance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,18 @@ jobs:
rpm-release: ${{ needs.version.outputs.rpm_release }}

fedora:
name: Fedora with Rootless Podman
name: Fedora with ${{ matrix.name }} Podman
needs: [build-conformance, build-rpm]
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- name: Rootless
configuration: podman-rootless
- name: Rootful
configuration: podman-rootful
permissions:
actions: read
contents: read
Expand Down Expand Up @@ -154,8 +162,10 @@ jobs:
name: openshell-conformance-x86_64-unknown-linux-musl
path: conformance-input

- name: Run RPM gateway continuity conformance
- name: Run RPM gateway conformance
shell: bash
env:
PODMAN_CONFIGURATION: ${{ matrix.configuration }}
run: |
set -euo pipefail
chmod +x conformance-input/openshell-conformance
Expand All @@ -171,13 +181,13 @@ jobs:

OPENSHELL_TEST_GUEST_CACHE_DISABLE=1 nix run .#test-guest -- \
--distro fedora \
--with podman-rootless \
--with "${PODMAN_CONFIGURATION}" \
--with selinux \
--copy "${candidate_cli_package[0]}:/var/lib/openshell-conformance/candidate/openshell.rpm" \
--copy "${candidate_gateway_package[0]}:/var/lib/openshell-conformance/candidate/openshell-gateway.rpm" \
--install "${candidate_cli_package[0]}" \
--install "${candidate_gateway_package[0]}" \
Comment on lines -176 to +187

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SDAChess just a note. This removes the "upgrade" test. There were some rough edges to sort out. For example, with the "upgrade" flow as defined, we were testing the latest stable release for the initial test cases and then only upgrading.

The latest release also didn't work as expected with "rootful podman" (which is also why the Fedora canary is failing).

--copy conformance-input/openshell-conformance:/tmp/openshell-conformance \
--copy nix/test-guest/conformance-plans/gateway-upgrade-restart.toml:/tmp/conformance-plan.toml \
--provision openshell-rpm-latest-release \
--copy nix/test-guest/conformance-plans/gateway-restart.toml:/tmp/conformance-plan.toml \
--provision openshell-rpm \
--provision gateway-podman \
--provision openshell-rpm-gateway-upgrade \
-- /tmp/openshell-conformance run --plan /tmp/conformance-plan.toml
-- /home/openshell/.local/bin/openshell-test-guest-as-gateway-user \
/tmp/openshell-conformance run --plan /tmp/conformance-plan.toml
18 changes: 18 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ reflection, non-callback inference APIs, and HTTP routes before normal request
authentication. The operator-configured primary listener retains the full
multiplexed API surface.

Rootful Podman can report a private bridge gateway before netavark assigns that
address. If an exact built-in Podman callback bind fails with
`EADDRNOTAVAIL`, the Linux gateway retries the same address with
`IP_FREEBIND`. The listener remains callback-only and becomes reachable when
the first sandbox materializes the bridge. If delayed exact binding also fails
while the gateway itself runs in a container, the gateway replaces the
loopback primary and missing bridge sockets with one IPv4 wildcard socket. The
wildcard defaults to callback-only authorization; only traffic addressed to
the configured loopback endpoint receives primary scope. This keeps user and
administrator APIs off the container's non-loopback interfaces, but it does
make the sandbox-callable gRPC surface reachable on every IPv4 interface in
that container namespace for the lifetime of the gateway process. Sandbox
mTLS/JWT authentication and the RPC allowlist remain mandatory defenses.
Delayed binding does not apply to rootless Podman, an explicit
`host_gateway_ip`, Docker or external drivers, public callback addresses, or
bind errors other than `EADDRNOTAVAIL`. A host gateway never uses the wildcard
fallback.

The `rpc_auth` classification is also the source of truth for negotiated
listener exposure: marking an RPC as `sandbox` or `dual` makes it callable on
these listeners. Review such changes as both authorization and network-surface
Expand Down
19 changes: 19 additions & 0 deletions crates/openshell-driver-podman/NETWORKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,25 @@ the supervisor's RPCs. Otherwise, it creates an additional listener that
exposes only the gateway's sandbox-callable gRPC methods. Operator, health,
reflection, and HTTP requests must use the primary listener.

Netavark may not assign a rootful managed bridge gateway until the first
sandbox joins the network. If that exact private callback address fails to bind
with `EADDRNOTAVAIL`, the Linux gateway uses `IP_FREEBIND` to bind the same
address before the interface exists. The listener remains callback-only and
becomes reachable when netavark materializes the bridge. Only the in-process
built-in Podman driver can mark its discovered rootful managed-bridge address
as eligible; rootless Podman, explicit `host_gateway_ip` values, and external
drivers cannot activate delayed binding.

If delayed exact binding fails while both the gateway and rootful Podman run
inside another Linux container, the gateway uses one scoped IPv4 wildcard
listener for that process. Connections addressed to loopback retain primary
scope; connections addressed to the Podman bridge or any other IPv4 interface
are callback-only. This exposes the sandbox-callable gRPC surface on every
IPv4 interface in the outer container namespace, so deployments should still
restrict that namespace at the container-network boundary. A gateway running
directly on a host never uses the wildcard fallback. Neither strategy exposes
operator, health, reflection, or HTTP routes on non-loopback interfaces.

### Layer 3 Inner Sandbox Network Namespace

Inside the container, the supervisor creates another network namespace for the
Expand Down
27 changes: 20 additions & 7 deletions crates/openshell-driver-podman/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,17 @@ use openshell_core::gpu::{
CdiGpuDefaultSelector, CdiGpuInventory, CdiGpuSelectionError, driver_gpu_requirements,
effective_driver_gpu_count, validate_specific_gpu_device_request,
};
#[cfg(target_os = "linux")]
use openshell_core::proto::compute::v1::GatewayDefaultRouteInterfaceRequirement;
#[cfg(target_os = "macos")]
use openshell_core::proto::compute::v1::GatewayLoopbackInterfaceRequirement;
use openshell_core::proto::compute::v1::{
DriverSandbox, GatewayListenerRequirement, GetCapabilitiesResponse, GpuResourceRequirements,
gateway_listener_requirement::Selector,
};
#[cfg(target_os = "linux")]
use openshell_core::proto::compute::v1::{
GatewayDefaultRouteInterfaceRequirement, GatewayExactBindAddressRequirement,
};
#[cfg(target_os = "linux")]
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use std::sync::Arc;
Expand Down Expand Up @@ -586,9 +588,14 @@ impl PodmanComputeDriver {
})?;
Ok(vec![GatewayListenerRequirement {
reason: format!("Podman network '{}' host gateway", self.config.network_name),
selector: Some(Selector::ExactBindAddress(
SocketAddr::new(gateway_ip, callback_port).to_string(),
)),
selector: Some(Selector::ExactBind(GatewayExactBindAddressRequirement {
address: SocketAddr::new(gateway_ip, callback_port).to_string(),
// A rootful managed bridge can be created after gateway
// startup. An explicit override is operator-owned, and
// rootless networking must never broaden the listener.
allow_delayed_bind: !self.rootless
&& self.config.host_gateway_ip.trim().is_empty(),
})),
}])
}
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -2179,7 +2186,10 @@ mod tests {
assert_eq!(requirements.len(), 1);
assert_eq!(
requirements[0].selector,
Some(Selector::ExactBindAddress("10.89.1.1:17670".to_string()))
Some(Selector::ExactBind(GatewayExactBindAddressRequirement {
address: "10.89.1.1:17670".to_string(),
allow_delayed_bind: true,
}))
);
}

Expand All @@ -2199,7 +2209,10 @@ mod tests {

assert_eq!(
requirements[0].selector,
Some(Selector::ExactBindAddress("10.90.1.1:17670".to_string()))
Some(Selector::ExactBind(GatewayExactBindAddressRequirement {
address: "10.90.1.1:17670".to_string(),
allow_delayed_bind: false,
}))
);
}

Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ k8s-openapi = { workspace = true }

# Async runtime
tokio = { workspace = true }
socket2 = { workspace = true }
socket2 = { workspace = true, features = ["all"] }
nix = { workspace = true }

# gRPC
Expand Down
28 changes: 28 additions & 0 deletions crates/openshell-server/src/compute/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ pub enum GatewayListenerRequirement {
address: SocketAddr,
driver_name: String,
reason: String,
allow_delayed_bind: bool,
},
DefaultRouteInterface {
driver_name: String,
Expand All @@ -189,6 +190,12 @@ pub enum GatewayListenerRequirement {
},
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum GatewayListenerBindPolicy {
Deny,
TrustedBuiltinPodman,
}

impl GatewayListenerRequirement {
pub fn driver_name(&self) -> &str {
match self {
Expand Down Expand Up @@ -627,6 +634,7 @@ impl ComputeRuntime {
driver_name: String,
driver: SharedComputeDriver,
driver_process: Option<Arc<ManagedDriverProcess>>,
listener_bind_policy: GatewayListenerBindPolicy,
store: Arc<Store>,
sandbox_index: SandboxIndex,
sandbox_watch_bus: SandboxWatchBus,
Expand Down Expand Up @@ -682,6 +690,23 @@ impl ComputeRuntime {
address,
driver_name: driver_name.clone(),
reason: requirement.reason,
allow_delayed_bind: false,
})
}
Selector::ExactBind(exact_bind) => {
let address = exact_bind.address.parse::<SocketAddr>().map_err(|err| {
ComputeError::Message(format!(
"compute driver '{driver_name}' returned invalid gateway listener address '{}': {err}",
exact_bind.address
))
})?;
Ok(GatewayListenerRequirement::Exact {
address,
driver_name: driver_name.clone(),
reason: requirement.reason,
allow_delayed_bind: listener_bind_policy
== GatewayListenerBindPolicy::TrustedBuiltinPodman
&& exact_bind.allow_delayed_bind,
})
}
Selector::DefaultRouteInterface(_) => {
Expand Down Expand Up @@ -764,6 +789,7 @@ impl ComputeRuntime {
endpoint.name,
driver,
endpoint.driver_process,
GatewayListenerBindPolicy::Deny,
store,
sandbox_index,
sandbox_watch_bus,
Expand Down Expand Up @@ -10608,6 +10634,7 @@ mod tests {
"test-driver".to_string(),
Arc::new(TestDriver::default()),
None,
GatewayListenerBindPolicy::Deny,
store,
SandboxIndex::new(),
SandboxWatchBus::new(),
Expand Down Expand Up @@ -10795,6 +10822,7 @@ mod tests {
address: "172.19.0.1:17670".parse().unwrap(),
driver_name: "docker".to_string(),
reason: "external driver managed bridge".to_string(),
allow_delayed_bind: false,
}]
);

Expand Down
Loading
Loading