Skip to content

Commit 9abd176

Browse files
committed
fix(gateway): try harder to detect Podman
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>
1 parent f0f17bf commit 9abd176

1 file changed

Lines changed: 201 additions & 6 deletions

File tree

crates/openshell-core/src/config.rs

Lines changed: 201 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
66
use serde::{Deserialize, Serialize};
77
use std::fmt;
8+
#[cfg(unix)]
9+
use std::io::{Read, Write};
810
use std::net::SocketAddr;
911
#[cfg(unix)]
1012
use std::os::unix::fs::FileTypeExt;
1113
use std::path::{Path, PathBuf};
1214
use std::process::Command;
1315
use std::str::FromStr;
16+
#[cfg(unix)]
17+
use std::time::Duration;
1418

1519
// ── Public default constants ────────────────────────────────────────────
1620
//
@@ -96,8 +100,8 @@ pub fn detect_driver() -> Option<ComputeDriverKind> {
96100
return Some(ComputeDriverKind::Kubernetes);
97101
}
98102

99-
// Podman: check if podman binary is available
100-
if is_binary_available("podman") {
103+
// Podman: check for a reachable local API socket.
104+
if is_podman_available() {
101105
return Some(ComputeDriverKind::Podman);
102106
}
103107

@@ -117,6 +121,54 @@ fn is_binary_available(name: &str) -> bool {
117121
.is_ok_and(|output| output.status.success())
118122
}
119123

124+
fn is_podman_available() -> bool {
125+
podman_socket_candidates()
126+
.iter()
127+
.any(|path| podman_socket_responds(path))
128+
}
129+
130+
fn podman_socket_candidates() -> Vec<PathBuf> {
131+
let socket = std::env::var("OPENSHELL_PODMAN_SOCKET")
132+
.ok()
133+
.filter(|path| !path.trim().is_empty())
134+
.map(PathBuf::from);
135+
podman_socket_candidates_from_env(
136+
socket,
137+
std::env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from),
138+
std::env::var_os("HOME").map(PathBuf::from),
139+
)
140+
}
141+
142+
fn podman_socket_candidates_from_env(
143+
socket: Option<PathBuf>,
144+
runtime_dir: Option<PathBuf>,
145+
home: Option<PathBuf>,
146+
) -> Vec<PathBuf> {
147+
let mut candidates = Vec::new();
148+
149+
if let Some(path) = socket {
150+
candidates.push(path);
151+
}
152+
153+
if let Some(runtime_dir) = runtime_dir {
154+
candidates.push(runtime_dir.join("podman/podman.sock"));
155+
}
156+
157+
#[cfg(target_os = "linux")]
158+
{
159+
candidates.push(PathBuf::from(format!(
160+
"/run/user/{}/podman/podman.sock",
161+
current_uid()
162+
)));
163+
}
164+
165+
if let Some(home) = home {
166+
candidates.push(home.join(".local/share/containers/podman/machine/podman.sock"));
167+
}
168+
169+
candidates
170+
}
171+
120172
fn is_docker_available() -> bool {
121173
is_binary_available("docker") || docker_socket_available()
122174
}
@@ -160,12 +212,86 @@ fn is_unix_socket(path: &Path) -> bool {
160212
.is_ok_and(|metadata| metadata.file_type().is_socket())
161213
}
162214

215+
#[cfg(unix)]
216+
fn podman_socket_responds(path: &Path) -> bool {
217+
unix_socket_http_ping(path, |response| {
218+
http_response_is_success(response) && contains_ascii(response, b"Libpod-Api-Version:")
219+
})
220+
}
221+
222+
#[cfg(unix)]
223+
fn unix_socket_http_ping(path: &Path, accepts_response: impl FnOnce(&[u8]) -> bool) -> bool {
224+
const PROBE_TIMEOUT: Duration = Duration::from_secs(1);
225+
const PING_REQUEST: &[u8] =
226+
b"GET /_ping HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
227+
228+
if !is_unix_socket(path) {
229+
return false;
230+
}
231+
232+
let Ok(mut stream) = std::os::unix::net::UnixStream::connect(path) else {
233+
return false;
234+
};
235+
if stream.set_read_timeout(Some(PROBE_TIMEOUT)).is_err()
236+
|| stream.set_write_timeout(Some(PROBE_TIMEOUT)).is_err()
237+
|| stream.write_all(PING_REQUEST).is_err()
238+
{
239+
return false;
240+
}
241+
242+
let mut response = [0_u8; 512];
243+
let mut total = 0;
244+
while total < response.len() {
245+
let Ok(n) = stream.read(&mut response[total..]) else {
246+
return false;
247+
};
248+
if n == 0 {
249+
break;
250+
}
251+
total += n;
252+
if contains_ascii(&response[..total], b"\r\n\r\n") {
253+
break;
254+
}
255+
}
256+
total > 0 && accepts_response(&response[..total])
257+
}
258+
259+
#[cfg(unix)]
260+
fn http_response_is_success(response: &[u8]) -> bool {
261+
response.starts_with(b"HTTP/1.1 200") || response.starts_with(b"HTTP/1.0 200")
262+
}
263+
264+
#[cfg(unix)]
265+
fn contains_ascii(haystack: &[u8], needle: &[u8]) -> bool {
266+
haystack
267+
.windows(needle.len())
268+
.any(|window| window.eq_ignore_ascii_case(needle))
269+
}
270+
271+
#[cfg(all(unix, test))]
272+
fn is_reachable_unix_socket(path: &Path) -> bool {
273+
is_unix_socket(path) && std::os::unix::net::UnixStream::connect(path).is_ok()
274+
}
275+
276+
#[cfg(all(unix, target_os = "linux"))]
277+
fn current_uid() -> u32 {
278+
use std::os::unix::fs::MetadataExt;
279+
280+
std::fs::metadata("/proc/self").map_or(0, |metadata| metadata.uid())
281+
}
282+
163283
#[cfg(not(unix))]
164284
fn is_unix_socket(path: &Path) -> bool {
165285
let _ = path;
166286
false
167287
}
168288

289+
#[cfg(not(unix))]
290+
fn podman_socket_responds(path: &Path) -> bool {
291+
let _ = path;
292+
false
293+
}
294+
169295
/// Server configuration.
170296
#[derive(Debug, Clone, Serialize, Deserialize)]
171297
pub struct Config {
@@ -593,10 +719,15 @@ const fn default_ssh_session_ttl_secs() -> u64 {
593719

594720
#[cfg(test)]
595721
mod tests {
722+
#[cfg(unix)]
723+
use super::is_reachable_unix_socket;
596724
use super::{
597725
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, detect_driver,
598-
docker_host_unix_socket_path, is_unix_socket,
726+
docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
727+
podman_socket_responds,
599728
};
729+
#[cfg(unix)]
730+
use std::io::{Read as _, Write as _};
600731
use std::net::SocketAddr;
601732
#[cfg(unix)]
602733
use std::os::unix::net::UnixListener;
@@ -700,9 +831,10 @@ mod tests {
700831
}
701832

702833
#[test]
703-
fn detect_driver_returns_none_without_k8s_env_or_binaries() {
704-
// When KUBERNETES_SERVICE_HOST is not set and no docker/podman binaries
705-
// or Docker socket are available, detect_driver should return None.
834+
fn detect_driver_returns_none_without_k8s_env_or_local_runtime() {
835+
// When KUBERNETES_SERVICE_HOST is not set, no Docker binary/socket is
836+
// available, and no Podman API socket is available, detect_driver
837+
// should return None.
706838
// This test may pass or fail depending on the test environment,
707839
// but it documents the expected behavior.
708840
let _ = detect_driver(); // Returns Some or None based on environment
@@ -726,10 +858,73 @@ mod tests {
726858
let _listener = UnixListener::bind(&socket_path).expect("bind unix socket");
727859

728860
assert!(is_unix_socket(&socket_path));
861+
assert!(is_reachable_unix_socket(&socket_path));
729862

730863
let regular_file = temp_dir.path().join("not-a-socket");
731864
std::fs::write(&regular_file, b"not a socket").expect("write regular file");
732865
assert!(!is_unix_socket(&regular_file));
866+
assert!(!is_reachable_unix_socket(&regular_file));
867+
}
868+
869+
#[cfg(unix)]
870+
#[test]
871+
fn podman_socket_probe_accepts_successful_ping_response() {
872+
let temp_dir = tempfile::tempdir().expect("create temp dir");
873+
let socket_path = temp_dir.path().join("podman.sock");
874+
let listener = UnixListener::bind(&socket_path).expect("bind podman socket");
875+
876+
let handle = std::thread::spawn(move || {
877+
let (mut stream, _) = listener.accept().expect("accept podman probe");
878+
let mut request = [0_u8; 128];
879+
let n = stream.read(&mut request).expect("read podman probe");
880+
assert!(request[..n].starts_with(b"GET /_ping HTTP/1.1\r\n"));
881+
stream
882+
.write_all(
883+
b"HTTP/1.1 200 OK\r\nLibpod-Api-Version: 5.8.2\r\nContent-Length: 2\r\n\r\nOK",
884+
)
885+
.expect("write podman ping response");
886+
});
887+
888+
assert!(podman_socket_responds(&socket_path));
889+
handle.join().expect("probe server exits");
890+
}
891+
892+
#[cfg(unix)]
893+
#[test]
894+
fn podman_socket_probe_rejects_docker_ping_response() {
895+
let temp_dir = tempfile::tempdir().expect("create temp dir");
896+
let socket_path = temp_dir.path().join("podman.sock");
897+
let listener = UnixListener::bind(&socket_path).expect("bind podman socket");
898+
899+
let handle = std::thread::spawn(move || {
900+
let (mut stream, _) = listener.accept().expect("accept podman probe");
901+
let mut request = [0_u8; 128];
902+
let n = stream.read(&mut request).expect("read podman probe");
903+
assert!(request[..n].starts_with(b"GET /_ping HTTP/1.1\r\n"));
904+
stream
905+
.write_all(
906+
b"HTTP/1.1 200 OK\r\nServer: Docker/29.2.1\r\nContent-Length: 2\r\n\r\nOK",
907+
)
908+
.expect("write docker ping response");
909+
});
910+
911+
assert!(!podman_socket_responds(&socket_path));
912+
handle.join().expect("probe server exits");
913+
}
914+
915+
#[test]
916+
fn podman_socket_candidates_include_env_runtime_and_home_paths() {
917+
let candidates = podman_socket_candidates_from_env(
918+
Some(PathBuf::from("/tmp/custom-podman.sock")),
919+
Some(PathBuf::from("/tmp/runtime")),
920+
Some(PathBuf::from("/tmp/home")),
921+
);
922+
923+
assert!(candidates.contains(&PathBuf::from("/tmp/custom-podman.sock")));
924+
assert!(candidates.contains(&PathBuf::from("/tmp/runtime/podman/podman.sock")));
925+
assert!(candidates.contains(&PathBuf::from(
926+
"/tmp/home/.local/share/containers/podman/machine/podman.sock"
927+
)));
733928
}
734929

735930
#[test]

0 commit comments

Comments
 (0)