nss_docker_ng/
lib.rs

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 // TODO: Implement me, see https://github.com/petski/nss-docker-ng/issues/1
24    }
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 // TODO: Implement me, see https://github.com/petski/nss-docker-ng/issues/2
33    }
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    // Check if the query ends with the expected suffix and if the address family is IPv4
70    if !(query.ends_with(SUFFIX)
71        && family == AddressFamily::IPv4 // TODO you no v6? See https://github.com/petski/nss-docker-ng/issues/3
72        && query.len() > SUFFIX.len())
73    {
74        return Ok(None);
75    }
76
77    // Initialize Docker API client
78    let mut docker = Docker::new(provider.get_docker_uri())?;
79    docker.adjust_api_version().await?;
80
81    // Strip suffix from query
82    let query_stripped = &query[..query.len() - SUFFIX.len()];
83
84    // Fetch container information
85    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    // Name of the container (remove leading "/" if necessary)
130    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    // From https://docs.docker.com/engine/api/v1.44/#tag/Container/operation/ContainerInspect:
141    // > Network mode to use for this container. Supported standard values are: bridge, host, none, and
142    // > container:<name|id>. Any other value is taken as a custom network's name to which this container
143    // > should connect to
144    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    // NetworkMode host, none, and those starting with "container:" don't have an IP address
157    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    // Extract networks from network settings
167    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    // The documentation on https://docs.docker.com/engine/api/v1.44/#tag/Container/operation/ContainerInspect
182    // is incomplete. There is another NetworkMode "default":
183    // > which is bridge for Docker Engine, and overlay for Swarm.
184    //
185    // See: https://github.com/docker/docker-py/issues/986
186    if network_mode == "default" && !networks.contains_key("default") {
187        network_mode = "bridge"; // TODO add swarm support. Is this really used/needed?
188    }
189
190    // Get the end point settings for the network with the name in network_mode
191    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    /*
403     * Returns a server and its mocks based on https://github.com/lipanski/mockito
404     */
405    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}