use std::collections::HashMap;
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(target_os = "linux")]
use crate::{Connection, Result};
const SOCKET_TIMEOUT: Duration = Duration::from_secs(2);
fn discover_sockets() -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = vec![
PathBuf::from("/var/run/docker.sock"),
PathBuf::from("/run/podman/podman.sock"),
];
if let Some(home) = std::env::var_os("HOME") {
paths.push(PathBuf::from(&home).join(".docker/run/docker.sock"));
}
if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
paths.push(PathBuf::from(&xdg).join("podman/podman.sock"));
}
paths.into_iter().filter(|p| p.exists()).collect()
}
fn http_get(socket: &Path, endpoint: &str) -> std::io::Result<String> {
let mut stream = UnixStream::connect(socket)?;
stream.set_read_timeout(Some(SOCKET_TIMEOUT))?;
stream.set_write_timeout(Some(SOCKET_TIMEOUT))?;
let req = format!("GET {endpoint} HTTP/1.0\r\nHost: localhost\r\n\r\n");
stream.write_all(req.as_bytes())?;
let mut response = String::new();
stream.read_to_string(&mut response)?;
Ok(response.split("\r\n\r\n").nth(1).unwrap_or("").to_string())
}
struct ContainerInfo {
#[allow(dead_code)]
id: String,
name: String,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
ips: Vec<String>,
#[cfg_attr(not(target_os = "macos"), allow(dead_code))]
published_ports: Vec<u16>,
}
fn list_from_socket(socket: &Path) -> std::io::Result<Vec<ContainerInfo>> {
let body = http_get(socket, "/containers/json")?;
let containers: Vec<serde_json::Value> = serde_json::from_str(&body)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
Ok(containers
.iter()
.filter_map(|c| {
let id = c["Id"].as_str()?.to_string();
let name = c["Labels"]["com.docker.compose.service"]
.as_str()
.or_else(|| {
c["Names"]
.as_array()
.and_then(|n| n.first())
.and_then(|n| n.as_str())
.map(|n| n.trim_start_matches('/'))
})?
.to_string();
let ips: Vec<String> = c["NetworkSettings"]["Networks"]
.as_object()
.map(|nets| {
nets.values()
.filter_map(|n| n["IPAddress"].as_str())
.filter(|ip| !ip.is_empty())
.map(std::string::ToString::to_string)
.collect()
})
.unwrap_or_default();
let published_ports: Vec<u16> = c["Ports"]
.as_array()
.map(|ports| {
ports
.iter()
.filter_map(|p| {
p["PublicPort"].as_u64().and_then(|n| u16::try_from(n).ok())
})
.collect()
})
.unwrap_or_default();
Some(ContainerInfo {
id,
name,
ips,
published_ports,
})
})
.collect())
}
fn list_containers() -> Vec<(PathBuf, ContainerInfo)> {
let mut all = Vec::new();
for socket in discover_sockets() {
if let Ok(containers) = list_from_socket(&socket) {
for c in containers {
all.push((socket.clone(), c));
}
}
}
all
}
#[cfg(target_os = "linux")]
fn container_host_pid(socket: &Path, container_id: &str) -> std::io::Result<u32> {
let body = http_get(socket, &format!("/containers/{container_id}/json"))?;
let data: serde_json::Value = serde_json::from_str(&body)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
data["State"]["Pid"]
.as_u64()
.and_then(|n| u32::try_from(n).ok())
.filter(|&p| p > 0)
.ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "container has no host PID")
})
}
#[cfg(target_os = "linux")]
#[must_use]
pub(crate) fn container_ip_to_service() -> HashMap<String, String> {
list_containers()
.into_iter()
.flat_map(|(_, c)| c.ips.into_iter().map(move |ip| (ip, c.name.clone())))
.collect()
}
#[cfg(target_os = "macos")]
#[must_use]
pub(crate) fn container_published_ports() -> HashMap<u16, String> {
list_containers()
.into_iter()
.flat_map(|(_, c)| {
c.published_ports
.into_iter()
.map(move |port| (port, c.name.clone()))
})
.collect()
}
#[cfg(target_os = "linux")]
pub(crate) fn get_container_connections() -> Result<Vec<Connection>> {
let containers = list_containers();
if containers.is_empty() {
return Ok(vec![]);
}
let pid_map = crate::platform::linux::build_inode_pid_map()?;
let mut result = Vec::new();
for (socket, container) in &containers {
let Ok(pid) = container_host_pid(socket, &container.id) else {
continue;
};
if let Ok(conns) =
crate::platform::linux::get_connections_in_namespace(pid, &container.name, &pid_map)
{
result.extend(conns);
}
}
Ok(result)
}
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
#[must_use]
pub(crate) fn parse_docker_proxy_ip(cmdline: &[u8]) -> Option<String> {
let args: Vec<&str> = cmdline
.split(|&b| b == 0)
.filter_map(|s| std::str::from_utf8(s).ok())
.collect();
let pos = args.iter().position(|&a| a == "-container-ip")?;
let ip = args.get(pos + 1)?;
if ip.is_empty() {
None
} else {
Some((*ip).to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cmdline(args: &[&str]) -> Vec<u8> {
let mut out = Vec::new();
for arg in args {
out.extend_from_slice(arg.as_bytes());
out.push(0);
}
out
}
#[test]
fn parse_docker_proxy_ip_found() {
let raw = cmdline(&[
"/usr/bin/docker-proxy",
"-container-ip",
"172.17.0.2",
"-container-port",
"80",
]);
assert_eq!(parse_docker_proxy_ip(&raw), Some("172.17.0.2".to_string()));
}
#[test]
fn parse_docker_proxy_ip_missing_flag() {
let raw = cmdline(&["/usr/bin/docker-proxy", "-host-ip", "0.0.0.0"]);
assert_eq!(parse_docker_proxy_ip(&raw), None);
}
#[test]
fn parse_docker_proxy_ip_flag_at_end() {
let raw = cmdline(&["/usr/bin/docker-proxy", "-container-ip"]);
assert_eq!(parse_docker_proxy_ip(&raw), None);
}
}