fakecloud_core/
container_net.rs1pub fn detect_container_cli() -> Option<String> {
27 if let Ok(cli) = std::env::var("FAKECLOUD_CONTAINER_CLI") {
28 return if cli_available(&cli) { Some(cli) } else { None };
29 }
30 if cli_available("docker") {
31 Some("docker".to_string())
32 } else if cli_available("podman") {
33 Some("podman".to_string())
34 } else {
35 None
36 }
37}
38
39pub fn cli_available(cli: &str) -> bool {
42 std::process::Command::new(cli)
43 .arg("info")
44 .stdout(std::process::Stdio::null())
45 .stderr(std::process::Stdio::null())
46 .status()
47 .map(|s| s.success())
48 .unwrap_or(false)
49}
50
51pub fn is_podman_binary(cli: &str) -> bool {
56 std::path::Path::new(cli)
57 .file_name()
58 .and_then(|n| n.to_str())
59 .map(|n| n.contains("podman"))
60 .unwrap_or(false)
61}
62
63pub fn detect_bridge_gateway(cli: &str) -> Option<String> {
66 let output = std::process::Command::new(cli)
67 .args([
68 "network",
69 "inspect",
70 "bridge",
71 "--format",
72 "{{range .IPAM.Config}}{{.Gateway}}{{end}}",
73 ])
74 .output()
75 .ok()?;
76 if !output.status.success() {
77 return None;
78 }
79 let gateway = String::from_utf8_lossy(&output.stdout).trim().to_string();
80 if gateway.is_empty() || !gateway.contains('.') {
81 return None;
82 }
83 Some(gateway)
84}
85
86#[derive(Debug, Clone)]
89pub struct HostNetworking {
90 pub host_alias: String,
94 pub add_host_arg: Option<String>,
98 pub sibling_host: String,
103}
104
105impl HostNetworking {
106 pub fn detect(cli: &str) -> Self {
109 let (host_alias, add_host_arg) = resolve_host_alias(cli);
110 let sibling_host = resolve_sibling_host(std::env::var("FAKECLOUD_IN_CONTAINER").ok());
111 Self {
112 host_alias,
113 add_host_arg,
114 sibling_host,
115 }
116 }
117
118 pub fn push_add_host_args(&self, argv: &mut Vec<String>) {
122 if let Some(arg) = &self.add_host_arg {
123 argv.push("--add-host".to_string());
124 argv.push(arg.clone());
125 }
126 }
127}
128
129pub fn resolve_host_alias(cli: &str) -> (String, Option<String>) {
133 if is_podman_binary(cli) {
134 ("host.containers.internal".to_string(), None)
138 } else if cfg!(target_os = "linux") {
139 let ip = detect_bridge_gateway(cli).unwrap_or_else(|| "172.17.0.1".to_string());
143 (
144 "host.docker.internal".to_string(),
145 Some(format!("host.docker.internal:{ip}")),
146 )
147 } else {
148 (
151 "host.docker.internal".to_string(),
152 Some("host.docker.internal:host-gateway".to_string()),
153 )
154 }
155}
156
157pub fn resolve_sibling_host(env_value: Option<String>) -> String {
166 let in_container = env_value
167 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
168 .unwrap_or(false);
169 if in_container {
170 "host.docker.internal".to_string()
171 } else {
172 "127.0.0.1".to_string()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn is_podman_binary_matches_bare_name() {
182 assert!(is_podman_binary("podman"));
183 assert!(is_podman_binary("podman-remote"));
184 }
185
186 #[test]
187 fn is_podman_binary_matches_absolute_path() {
188 assert!(is_podman_binary("/opt/homebrew/bin/podman"));
189 assert!(is_podman_binary("/usr/local/bin/podman-remote"));
190 }
191
192 #[test]
193 fn is_podman_binary_rejects_docker() {
194 assert!(!is_podman_binary("docker"));
195 assert!(!is_podman_binary("/usr/local/bin/docker"));
196 assert!(!is_podman_binary("docker-credential-helper"));
197 }
198
199 #[test]
200 fn resolve_host_alias_podman_has_no_add_host() {
201 let (alias, add_host) = resolve_host_alias("podman");
202 assert_eq!(alias, "host.containers.internal");
203 assert_eq!(add_host, None);
204 let (alias, add_host) = resolve_host_alias("/opt/homebrew/bin/podman");
205 assert_eq!(alias, "host.containers.internal");
206 assert_eq!(add_host, None);
207 }
208
209 #[test]
210 fn resolve_host_alias_docker_emits_add_host() {
211 let (alias, add_host) = resolve_host_alias("docker");
212 assert_eq!(alias, "host.docker.internal");
213 assert!(add_host.is_some());
216 assert!(add_host.unwrap().starts_with("host.docker.internal:"));
217 }
218
219 #[test]
220 fn resolve_sibling_host_defaults_to_loopback() {
221 assert_eq!(resolve_sibling_host(None), "127.0.0.1");
222 assert_eq!(resolve_sibling_host(Some(String::new())), "127.0.0.1");
223 assert_eq!(resolve_sibling_host(Some("0".to_string())), "127.0.0.1");
224 assert_eq!(resolve_sibling_host(Some("false".to_string())), "127.0.0.1");
225 }
226
227 #[test]
228 fn resolve_sibling_host_uses_docker_internal_when_in_container() {
229 assert_eq!(
230 resolve_sibling_host(Some("1".to_string())),
231 "host.docker.internal"
232 );
233 assert_eq!(
234 resolve_sibling_host(Some("true".to_string())),
235 "host.docker.internal"
236 );
237 assert_eq!(
238 resolve_sibling_host(Some("TRUE".to_string())),
239 "host.docker.internal"
240 );
241 }
242
243 #[test]
244 fn push_add_host_args_noop_for_podman() {
245 let net = HostNetworking {
246 host_alias: "host.containers.internal".to_string(),
247 add_host_arg: None,
248 sibling_host: "127.0.0.1".to_string(),
249 };
250 let mut argv = vec!["create".to_string()];
251 net.push_add_host_args(&mut argv);
252 assert_eq!(argv, vec!["create".to_string()]);
253 }
254
255 #[test]
256 fn push_add_host_args_emits_for_docker() {
257 let net = HostNetworking {
258 host_alias: "host.docker.internal".to_string(),
259 add_host_arg: Some("host.docker.internal:host-gateway".to_string()),
260 sibling_host: "127.0.0.1".to_string(),
261 };
262 let mut argv = vec!["create".to_string()];
263 net.push_add_host_args(&mut argv);
264 assert_eq!(
265 argv,
266 vec![
267 "create".to_string(),
268 "--add-host".to_string(),
269 "host.docker.internal:host-gateway".to_string(),
270 ]
271 );
272 }
273}