Skip to content

Commit 7ae95dc

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 7ae95dc

1 file changed

Lines changed: 205 additions & 6 deletions

File tree

crates/openshell-core/src/config.rs

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

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

293+
#[cfg(not(unix))]
294+
fn podman_socket_responds(path: &Path) -> bool {
295+
let _ = path;
296+
false
297+
}
298+
169299
/// Server configuration.
170300
#[derive(Debug, Clone, Serialize, Deserialize)]
171301
pub struct Config {
@@ -593,10 +723,15 @@ const fn default_ssh_session_ttl_secs() -> u64 {
593723

594724
#[cfg(test)]
595725
mod tests {
726+
#[cfg(unix)]
727+
use super::is_reachable_unix_socket;
596728
use super::{
597729
ComputeDriverKind, Config, DEFAULT_SERVICE_ROUTING_DOMAIN, detect_driver,
598-
docker_host_unix_socket_path, is_unix_socket,
730+
docker_host_unix_socket_path, is_unix_socket, podman_socket_candidates_from_env,
731+
podman_socket_responds,
599732
};
733+
#[cfg(unix)]
734+
use std::io::{Read as _, Write as _};
600735
use std::net::SocketAddr;
601736
#[cfg(unix)]
602737
use std::os::unix::net::UnixListener;
@@ -700,9 +835,10 @@ mod tests {
700835
}
701836

702837
#[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.
838+
fn detect_driver_returns_none_without_k8s_env_or_local_runtime() {
839+
// When KUBERNETES_SERVICE_HOST is not set, no Docker binary/socket is
840+
// available, and no Podman API socket is available, detect_driver
841+
// should return None.
706842
// This test may pass or fail depending on the test environment,
707843
// but it documents the expected behavior.
708844
let _ = detect_driver(); // Returns Some or None based on environment
@@ -726,10 +862,73 @@ mod tests {
726862
let _listener = UnixListener::bind(&socket_path).expect("bind unix socket");
727863

728864
assert!(is_unix_socket(&socket_path));
865+
assert!(is_reachable_unix_socket(&socket_path));
729866

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

735934
#[test]

0 commit comments

Comments
 (0)