fix(gateway): try harder to detect Podman (#1536)

Auto-detection previously treated Podman as available only when the podman CLI
was visible on PATH. However, package manager services can run with a
restricted PATH, which lets Docker be selected even when a Podman API socket is
reachable. Additionally, podman may symlink /var/run/docker.sock to podman's
machine unix socket, which would be incorrectly detected as Docker. Worse
still: the podman machine may not even be running.

This replaces the Podman binary check with a functional HTTP probe against the
standard Podman socket paths. The probe requires /_ping to answer with a
Libpod-Api-Version header before treating the socket as Podman, which lets the
gateway select the embedded Podman driver only when the API is usable.

Signed-off-by: Kris Hicks <khicks@nvidia.com>
This commit is contained in:
krishicks
2026-06-03 08:01:44 -07:00
committed by GitHub
parent 5102cb9413
commit 1f07bf04be
+201 -6
View File
@@ -5,12 +5,16 @@
use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(unix)]
use std::io::{Read, Write};
use std::net::SocketAddr;
#[cfg(unix)]
use std::os::unix::fs::FileTypeExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
#[cfg(unix)]
use std::time::Duration;
// ── Public default constants ────────────────────────────────────────────
//
@@ -101,8 +105,8 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
return Some(ComputeDriverKind::Kubernetes);
}
// Podman: check if podman binary is available
if is_binary_available("podman") {
// Podman: check for a reachable local API socket.
if is_podman_available() {
return Some(ComputeDriverKind::Podman);
}
@@ -122,6 +126,54 @@ fn is_binary_available(name: &str) -> bool {
.is_ok_and(|output| output.status.success())
}
fn is_podman_available() -> bool {
podman_socket_candidates()
.iter()
.any(|path| podman_socket_responds(path))
}
fn podman_socket_candidates() -> Vec<PathBuf> {
let socket = std::env::var("OPENSHELL_PODMAN_SOCKET")
.ok()
.filter(|path| !path.trim().is_empty())
.map(PathBuf::from);
podman_socket_candidates_from_env(
socket,
std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from),
std::env::var_os("HOME").map(PathBuf::from),
)
}
fn podman_socket_candidates_from_env(
socket: Option<PathBuf>,
runtime_dir: Option<PathBuf>,
home: Option<PathBuf>,
) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Some(path) = socket {
candidates.push(path);
}
if let Some(runtime_dir) = runtime_dir {
candidates.push(runtime_dir.join("podman/podman.sock"));
}
#[cfg(target_os = "linux")]
{
candidates.push(PathBuf::from(format!(
"/run/user/{}/podman/podman.sock",
current_uid()
)));
}
if let Some(home) = home {
candidates.push(home.join(".local/share/containers/podman/machine/podman.sock"));
}
candidates
}
fn is_docker_available() -> bool {
is_binary_available("docker") || docker_socket_available()
}
@@ -165,12 +217,86 @@ fn is_unix_socket(path: &Path) -> bool {
.is_ok_and(|metadata| metadata.file_type().is_socket())
}
#[cfg(unix)]
fn podman_socket_responds(path: &Path) -> bool {
unix_socket_http_ping(path, |response| {
http_response_is_success(response) && contains_ascii(response, b"Libpod-Api-Version:")
})
}
#[cfg(unix)]
fn unix_socket_http_ping(path: &Path, accepts_response: impl FnOnce(&[u8]) -> bool) -> bool {
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
const PING_REQUEST: &[u8] =
b"GET /_ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
if !is_unix_socket(path) {
return false;
}
let Ok(mut stream) = std::os::unix::net::UnixStream::connect(path) else {
return false;
};
if stream.set_read_timeout(Some(PROBE_TIMEOUT)).is_err()
|| stream.set_write_timeout(Some(PROBE_TIMEOUT)).is_err()
|| stream.write_all(PING_REQUEST).is_err()
{
return false;
}
let mut response = [0_u8; 512];
let mut total = 0;
while total < response.len() {
let Ok(n) = stream.read(&mut response[total..]) else {
return false;
};
if n == 0 {
break;
}
total += n;
if contains_ascii(&response[..total], b"\r\n\r\n") {
break;
}
}
total > 0 && accepts_response(&response[..total])
}
#[cfg(unix)]
fn http_response_is_success(response: &[u8]) -> bool {
response.starts_with(b"HTTP/1.1 200") || response.starts_with(b"HTTP/1.0 200")
}
#[cfg(unix)]
fn contains_ascii(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle))
}
#[cfg(all(unix, test))]
fn is_reachable_unix_socket(path: &Path) -> bool {
is_unix_socket(path) && std::os::unix::net::UnixStream::connect(path).is_ok()
}
#[cfg(all(unix, target_os = "linux"))]
fn current_uid() -> u32 {
use std::os::unix::fs::MetadataExt;
std::fs::metadata("/proc/self").map_or(0, |metadata| metadata.uid())
}
#[cfg(not(unix))]
fn is_unix_socket(path: &Path) -> bool {
let _ = path;
false
}
#[cfg(not(unix))]
fn podman_socket_responds(path: &Path) -> bool {
let _ = path;
false
}
/// Server configuration.
///
/// Built programmatically in [`crate::Config::new`] and the gateway CLI from
@@ -597,10 +723,15 @@ const fn default_ssh_session_ttl_secs() -> u64 {
#[cfg(test)]
mod tests {
#[cfg(unix)]
use super::is_reachable_unix_socket;
use super::{
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, detect_driver,
docker_host_unix_socket_path, is_unix_socket,
docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
podman_socket_responds,
};
#[cfg(unix)]
use std::io::{Read as _, Write as _};
use std::net::SocketAddr;
#[cfg(unix)]
use std::os::unix::net::UnixListener;
@@ -704,9 +835,10 @@ mod tests {
}
#[test]
fn detect_driver_returns_none_without_k8s_env_or_binaries() {
// When KUBERNETES_SERVICE_HOST is not set and no docker/podman binaries
// or Docker socket are available, detect_driver should return None.
fn detect_driver_returns_none_without_k8s_env_or_local_runtime() {
// When KUBERNETES_SERVICE_HOST is not set, no Docker binary/socket is
// available, and no Podman API socket is available, detect_driver
// should return None.
// This test may pass or fail depending on the test environment,
// but it documents the expected behavior.
let _ = detect_driver(); // Returns Some or None based on environment
@@ -730,10 +862,73 @@ mod tests {
let _listener = UnixListener::bind(&socket_path).expect("bind unix socket");
assert!(is_unix_socket(&socket_path));
assert!(is_reachable_unix_socket(&socket_path));
let regular_file = temp_dir.path().join("not-a-socket");
std::fs::write(&regular_file, b"not a socket").expect("write regular file");
assert!(!is_unix_socket(&regular_file));
assert!(!is_reachable_unix_socket(&regular_file));
}
#[cfg(unix)]
#[test]
fn podman_socket_probe_accepts_successful_ping_response() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let socket_path = temp_dir.path().join("podman.sock");
let listener = UnixListener::bind(&socket_path).expect("bind podman socket");
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept podman probe");
let mut request = [0_u8; 128];
let n = stream.read(&mut request).expect("read podman probe");
assert!(request[..n].starts_with(b"GET /_ping HTTP/1.1\r\n"));
stream
.write_all(
b"HTTP/1.1 200 OK\r\nLibpod-Api-Version: 5.8.2\r\nContent-Length: 2\r\n\r\nOK",
)
.expect("write podman ping response");
});
assert!(podman_socket_responds(&socket_path));
handle.join().expect("probe server exits");
}
#[cfg(unix)]
#[test]
fn podman_socket_probe_rejects_docker_ping_response() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
let socket_path = temp_dir.path().join("podman.sock");
let listener = UnixListener::bind(&socket_path).expect("bind podman socket");
let handle = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().expect("accept podman probe");
let mut request = [0_u8; 128];
let n = stream.read(&mut request).expect("read podman probe");
assert!(request[..n].starts_with(b"GET /_ping HTTP/1.1\r\n"));
stream
.write_all(
b"HTTP/1.1 200 OK\r\nServer: Docker/29.2.1\r\nContent-Length: 2\r\n\r\nOK",
)
.expect("write docker ping response");
});
assert!(!podman_socket_responds(&socket_path));
handle.join().expect("probe server exits");
}
#[test]
fn podman_socket_candidates_include_env_runtime_and_home_paths() {
let candidates = podman_socket_candidates_from_env(
Some(PathBuf::from("/tmp/custom-podman.sock")),
Some(PathBuf::from("/tmp/runtime")),
Some(PathBuf::from("/tmp/home")),
);
assert!(candidates.contains(&PathBuf::from("/tmp/custom-podman.sock")));
assert!(candidates.contains(&PathBuf::from("/tmp/runtime/podman/podman.sock")));
assert!(candidates.contains(&PathBuf::from(
"/tmp/home/.local/share/containers/podman/machine/podman.sock"
)));
}
#[test]