1extern crate debug_print;
2extern crate docker_api;
3
4use debug_print::debug_eprintln;
5use docker_api::Docker;
6use libnss::host::{AddressFamily, Addresses, Host, HostHooks};
7use libnss::interop::Response;
8use libnss::libnss_host_hooks;
9use std::error::Error;
10use std::net::{IpAddr, Ipv4Addr};
11use std::str::FromStr;
12
13static SUFFIX: &str = ".docker";
14static DOCKER_URI: &str = "unix:///var/run/docker.sock";
15static CONTAINER_SUBDOMAINS_ALLOWED_LABEL: &str =
16 ".com.github.petski.nss-docker-ng.container-subdomains-allowed";
17
18struct DockerNG;
19libnss_host_hooks!(docker_ng, DockerNG);
20
21impl HostHooks for DockerNG {
22 fn get_all_entries() -> Response<Vec<Host>> {
23 Response::NotFound }
25
26 fn get_host_by_name(name: &str, family: AddressFamily) -> Response<Host> {
27 let provider = DefaultDockerUriProvider;
28 get_host_by_name_with_provider(name, family, &provider)
29 }
30
31 fn get_host_by_addr(_: IpAddr) -> Response<Host> {
32 Response::NotFound }
34}
35
36trait DockerUriProvider {
37 fn get_docker_uri(&self) -> String;
38}
39
40struct DefaultDockerUriProvider;
41
42impl DockerUriProvider for DefaultDockerUriProvider {
43 fn get_docker_uri(&self) -> String {
44 DOCKER_URI.to_string()
45 }
46}
47
48#[tokio::main]
49async fn get_host_by_name_with_provider(
50 query: &str,
51 family: AddressFamily,
52 provider: &dyn DockerUriProvider,
53) -> Response<Host> {
54 return match get_host_by_name_with_provider_inner(query, family, provider).await {
55 Ok(Some(host)) => Response::Success(host),
56 Ok(None) => Response::NotFound,
57 Err(_e) => {
58 debug_eprintln!("get_host_by_name '{}' failed: {}", query, _e);
59 Response::Unavail
60 }
61 };
62}
63
64async fn get_host_by_name_with_provider_inner(
65 query: &str,
66 family: AddressFamily,
67 provider: &dyn DockerUriProvider,
68) -> Result<Option<Host>, Box<dyn Error>> {
69 if !(query.ends_with(SUFFIX)
71 && family == AddressFamily::IPv4 && query.len() > SUFFIX.len())
73 {
74 return Ok(None);
75 }
76
77 let mut docker = Docker::new(provider.get_docker_uri())?;
79 docker.adjust_api_version().await?;
80
81 let query_stripped = &query[..query.len() - SUFFIX.len()];
83
84 let inspect_result = 'block: {
86 match docker.containers().get(query_stripped).inspect().await {
87 Ok(query_stripped_result) => query_stripped_result,
88 Err(_e) => {
89 debug_eprintln!("Failed to inspect container '{}': {}", query_stripped, _e);
90
91 if let Some((last_dot_index, _)) = query_stripped.match_indices('.').next_back() {
92 let query_stripped_main_domain =
93 &query_stripped[(last_dot_index + '.'.len_utf8())..];
94 match docker
95 .containers()
96 .get(query_stripped_main_domain)
97 .inspect()
98 .await
99 {
100 Ok(query_stripped_main_domain_result) => {
101 match query_stripped_main_domain_result.config.as_ref().and_then(
102 |config| {
103 config.labels.as_ref().and_then(|labels| {
104 labels.get(CONTAINER_SUBDOMAINS_ALLOWED_LABEL)
105 })
106 },
107 ) {
108 Some(label_value) => {
109 if label_value.eq("true")
110 || label_value.eq("True")
111 || label_value.eq("1")
112 {
113 break 'block query_stripped_main_domain_result;
114 } else {
115 return Ok(None);
116 }
117 }
118 None => return Ok(None),
119 }
120 }
121 Err(_e) => return Ok(None),
122 }
123 }
124 return Ok(None);
125 }
126 }
127 };
128
129 let name = match inspect_result.name {
131 Some(mut name) => {
132 if name.starts_with('/') {
133 name.remove(0);
134 }
135 name
136 }
137 None => return Err("No Name".into()),
138 };
139
140 let mut network_mode = match inspect_result.host_config.as_ref().and_then(|host_config| {
145 host_config
146 .get("NetworkMode")
147 .and_then(|value| value.as_str())
148 }) {
149 Some(network_mode) => {
150 debug_eprintln!("Container '{}' has NetworkMode '{}'", name, network_mode);
151 network_mode
152 }
153 None => return Err("Could not find NetworkMode".into()),
154 };
155
156 if ["none", "host"].contains(&network_mode) || network_mode.starts_with("container:") {
158 debug_eprintln!(
159 "Container '{}' is in NetworkMode {}, no IP here",
160 name,
161 network_mode
162 );
163 return Ok(None);
164 }
165
166 let networks = match inspect_result
168 .network_settings
169 .and_then(|settings| settings.networks)
170 {
171 Some(networks) => {
172 if networks.is_empty() {
173 return Err("Found 0 networks".into());
174 }
175 debug_eprintln!("Found {} network(s) for '{}'", networks.keys().len(), name);
176 networks
177 }
178 None => return Err("Found no networks".into()),
179 };
180
181 if network_mode == "default" && !networks.contains_key("default") {
187 network_mode = "bridge"; }
189
190 let end_point_settings = match networks.get(network_mode) {
192 Some(end_point_settings) => end_point_settings,
193 None => return Err(format!("Network '{network_mode}' not found").into()),
194 };
195
196 let ip_address = match &end_point_settings.ip_address {
197 Some(ip_address) => {
198 if ip_address.is_empty() {
199 return Err("IP address is an empty string".into());
200 }
201 ip_address
202 }
203 None => return Err("Endpoint has no IP address".into()),
204 };
205
206 match Ipv4Addr::from_str(ip_address) {
207 Ok(ip) => {
208 let id = match inspect_result.id.as_ref() {
209 Some(id) => id,
210 None => return Err("No Id".into()),
211 };
212
213 let mut aliases = vec![[id[..12].to_string(), SUFFIX.to_string()].join("")];
214
215 if name.ne(query_stripped) {
216 aliases.push([query_stripped.to_string(), SUFFIX.to_string()].join(""))
217 }
218
219 Ok(Some(Host {
220 name: [name.to_string(), SUFFIX.to_string()].join(""),
221 addresses: Addresses::V4(vec![ip]),
222 aliases,
223 }))
224 }
225 Err(_e) => Err(format!("Failed to parse IP address '{ip_address}': {_e}").into()),
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use mockall::{mock, predicate::*};
233 use mockito::{Mock, Server, ServerOpts};
234
235 mock! {
236 pub DockerUriProvider {}
237 impl DockerUriProvider for DockerUriProvider {
238 fn get_docker_uri(&self) -> String;
239 }
240 }
241
242 #[test]
243 fn test_get_host_by_name() {
244 let (_server, _mocks, mock_provider) = init_mocking_features();
245
246 assert_eq!(
247 get_host_by_name_with_provider(".foo", AddressFamily::IPv4, &mock_provider),
248 Response::NotFound
249 );
250
251 assert_eq!(
252 get_host_by_name_with_provider("foo.docker", AddressFamily::IPv6, &mock_provider),
253 Response::NotFound
254 );
255
256 assert_eq!(
257 get_host_by_name_with_provider(".docker", AddressFamily::IPv4, &mock_provider),
258 Response::NotFound
259 );
260
261 assert_eq!(
262 get_host_by_name_with_provider(
263 "sunny-default-bridge.docker",
264 AddressFamily::IPv4,
265 &mock_provider,
266 ),
267 Response::Success(Host {
268 name: "sunny-default-bridge.docker".to_string(),
269 aliases: vec!["c0ffeec0ffee.docker".to_string()],
270 addresses: Addresses::V4(vec![Ipv4Addr::new(172, 29, 0, 2)]),
271 })
272 );
273
274 assert_eq!(
275 get_host_by_name_with_provider(
276 "sunny-default-bridge-container-subdomains-allowed.docker",
277 AddressFamily::IPv4,
278 &mock_provider,
279 ),
280 Response::Success(Host {
281 name: "sunny-default-bridge-container-subdomains-allowed.docker".to_string(),
282 aliases: vec!["c0ffeec0ffee.docker".to_string()],
283 addresses: Addresses::V4(vec![Ipv4Addr::new(172, 29, 0, 2)]),
284 })
285 );
286
287 assert_eq!(
288 get_host_by_name_with_provider(
289 "mega.very.sunny-default-bridge-container-subdomains-allowed.docker",
290 AddressFamily::IPv4,
291 &mock_provider,
292 ),
293 Response::Success(Host {
294 name: "sunny-default-bridge-container-subdomains-allowed.docker".to_string(),
295 aliases: vec![
296 "c0ffeec0ffee.docker".to_string(),
297 "mega.very.sunny-default-bridge-container-subdomains-allowed.docker"
298 .to_string(),
299 ],
300 addresses: Addresses::V4(vec![Ipv4Addr::new(172, 29, 0, 2)]),
301 })
302 );
303
304 assert_eq!(
305 get_host_by_name_with_provider("rainy-404.docker", AddressFamily::IPv4, &mock_provider),
306 Response::NotFound
307 );
308
309 assert_eq!(
310 get_host_by_name_with_provider(
311 "rainy-no-name.docker",
312 AddressFamily::IPv4,
313 &mock_provider,
314 ),
315 Response::Unavail
316 );
317
318 assert_eq!(
319 get_host_by_name_with_provider(
320 "rainy-no-network-mode.docker",
321 AddressFamily::IPv4,
322 &mock_provider,
323 ),
324 Response::Unavail
325 );
326
327 for name in [
328 "rainy-network-mode-none.docker",
329 "rainy-network-mode-host.docker",
330 "rainy-network-mode-container.docker",
331 ] {
332 assert_eq!(
333 get_host_by_name_with_provider(name, AddressFamily::IPv4, &mock_provider),
334 Response::NotFound
335 );
336 }
337
338 assert_eq!(
339 get_host_by_name_with_provider(
340 "rainy-zero-networks.docker",
341 AddressFamily::IPv4,
342 &mock_provider,
343 ),
344 Response::Unavail
345 );
346
347 assert_eq!(
348 get_host_by_name_with_provider(
349 "rainy-no-networks.docker",
350 AddressFamily::IPv4,
351 &mock_provider,
352 ),
353 Response::Unavail
354 );
355
356 assert_eq!(
357 get_host_by_name_with_provider(
358 "rainy-network-not-exists.docker",
359 AddressFamily::IPv4,
360 &mock_provider,
361 ),
362 Response::Unavail
363 );
364
365 assert_eq!(
366 get_host_by_name_with_provider(
367 "rainy-ip-address-empty.docker",
368 AddressFamily::IPv4,
369 &mock_provider,
370 ),
371 Response::Unavail
372 );
373
374 assert_eq!(
375 get_host_by_name_with_provider(
376 "rainy-no-ip-address.docker",
377 AddressFamily::IPv4,
378 &mock_provider,
379 ),
380 Response::Unavail
381 );
382
383 assert_eq!(
384 get_host_by_name_with_provider(
385 "rainy-unparseable-ip-address.docker",
386 AddressFamily::IPv4,
387 &mock_provider,
388 ),
389 Response::Unavail
390 );
391
392 assert_eq!(
393 get_host_by_name_with_provider(
394 "rainy-no-id.docker",
395 AddressFamily::IPv4,
396 &mock_provider,
397 ),
398 Response::Unavail
399 );
400 }
401
402 fn init_mocking_features() -> (Server, [Mock; 17], MockDockerUriProvider) {
406 let mut server = Server::new_with_opts(ServerOpts {
407 assert_on_drop: true,
408 ..Default::default()
409 });
410
411 let url = server.url();
412
413 let mut mock_provider = MockDockerUriProvider::new();
414 mock_provider
415 .expect_get_docker_uri()
416 .return_const(url.to_owned());
417
418 let _version_mock = server
419 .mock("GET", "/version")
420 .expect(16)
421 .with_body_from_file("tests/resources/v1.44/version.body")
422 .create();
423
424 let _inspect_mock_sunny_default_bridge = server
425 .mock("GET", "/v1.44/containers/sunny-default-bridge/json")
426 .with_body_from_file("tests/resources/v1.44/containers/sunny-default-bridge/json.body")
427 .create();
428
429 let _inspect_mock_mega_very_sunny_default_bridge_container_subdomains_allowed = server
430 .mock("GET", "/v1.44/containers/mega.very.sunny-default-bridge-container-subdomains-allowed/json")
431 .with_status(404)
432 .with_body_from_file("tests/resources/v1.44/containers/mega.very.sunny-default-bridge-container-subdomains-allowed/json.body")
433 .create();
434
435 let _inspect_mock_sunny_default_bridge_container_subdomains_allowed = server
436 .mock("GET", "/v1.44/containers/sunny-default-bridge-container-subdomains-allowed/json")
437 .expect(2)
438 .with_body_from_file("tests/resources/v1.44/containers/sunny-default-bridge-container-subdomains-allowed/json.body")
439 .create();
440
441 let _inspect_mock_rainy_404 = server
442 .mock("GET", "/v1.44/containers/rainy-404/json")
443 .with_status(404)
444 .with_body_from_file("tests/resources/v1.44/containers/rainy-404/json.body")
445 .create();
446
447 let _inspect_mock_rainy_no_name = server
448 .mock("GET", "/v1.44/containers/rainy-no-name/json")
449 .with_body_from_file("tests/resources/v1.44/containers/rainy-no-name/json.body")
450 .create();
451
452 let _inspect_mock_rainy_no_network_mode = server
453 .mock("GET", "/v1.44/containers/rainy-no-network-mode/json")
454 .with_body_from_file("tests/resources/v1.44/containers/rainy-no-network-mode/json.body")
455 .create();
456
457 let _inspect_mock_rainy_network_mode_none = server
458 .mock("GET", "/v1.44/containers/rainy-network-mode-none/json")
459 .with_body_from_file(
460 "tests/resources/v1.44/containers/rainy-network-mode-none/json.body",
461 )
462 .create();
463
464 let _inspect_mock_rainy_network_mode_host = server
465 .mock("GET", "/v1.44/containers/rainy-network-mode-host/json")
466 .with_body_from_file(
467 "tests/resources/v1.44/containers/rainy-network-mode-host/json.body",
468 )
469 .create();
470
471 let _inspect_mock_rainy_network_mode_container = server
472 .mock("GET", "/v1.44/containers/rainy-network-mode-container/json")
473 .with_body_from_file(
474 "tests/resources/v1.44/containers/rainy-network-mode-container/json.body",
475 )
476 .create();
477
478 let _inspect_mock_rainy_zero_networks = server
479 .mock("GET", "/v1.44/containers/rainy-zero-networks/json")
480 .with_body_from_file("tests/resources/v1.44/containers/rainy-zero-networks/json.body")
481 .create();
482
483 let _inspect_mock_rainy_no_networks = server
484 .mock("GET", "/v1.44/containers/rainy-no-networks/json")
485 .with_body_from_file("tests/resources/v1.44/containers/rainy-no-networks/json.body")
486 .create();
487
488 let _inspect_mock_rainy_network_not_exists = server
489 .mock("GET", "/v1.44/containers/rainy-network-not-exists/json")
490 .with_body_from_file(
491 "tests/resources/v1.44/containers/rainy-network-not-exists/json.body",
492 )
493 .create();
494
495 let _inspect_mock_rainy_ip_address_empty = server
496 .mock("GET", "/v1.44/containers/rainy-ip-address-empty/json")
497 .expect(1)
498 .with_body_from_file(
499 "tests/resources/v1.44/containers/rainy-ip-address-empty/json.body",
500 )
501 .create();
502
503 let _inspect_mock_rainy_no_ip_address = server
504 .mock("GET", "/v1.44/containers/rainy-no-ip-address/json")
505 .with_body_from_file("tests/resources/v1.44/containers/rainy-no-ip-address/json.body")
506 .create();
507
508 let _inspect_mock_rainy_unparseable_ip_address = server
509 .mock("GET", "/v1.44/containers/rainy-unparseable-ip-address/json")
510 .with_body_from_file(
511 "tests/resources/v1.44/containers/rainy-unparseable-ip-address/json.body",
512 )
513 .create();
514
515 let _inspect_mock_rainy_no_id = server
516 .mock("GET", "/v1.44/containers/rainy-no-id/json")
517 .with_body_from_file("tests/resources/v1.44/containers/rainy-no-id/json.body")
518 .create();
519
520 (
521 server,
522 [
523 _version_mock,
524 _inspect_mock_sunny_default_bridge,
525 _inspect_mock_mega_very_sunny_default_bridge_container_subdomains_allowed,
526 _inspect_mock_sunny_default_bridge_container_subdomains_allowed,
527 _inspect_mock_rainy_404,
528 _inspect_mock_rainy_no_name,
529 _inspect_mock_rainy_no_network_mode,
530 _inspect_mock_rainy_network_mode_none,
531 _inspect_mock_rainy_network_mode_host,
532 _inspect_mock_rainy_network_mode_container,
533 _inspect_mock_rainy_zero_networks,
534 _inspect_mock_rainy_no_networks,
535 _inspect_mock_rainy_network_not_exists,
536 _inspect_mock_rainy_ip_address_empty,
537 _inspect_mock_rainy_no_ip_address,
538 _inspect_mock_rainy_unparseable_ip_address,
539 _inspect_mock_rainy_no_id,
540 ],
541 mock_provider,
542 )
543 }
544}