1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#[macro_use]
extern crate lazy_static;
extern crate debug_print;
extern crate docker_api;
extern crate libnss;

use debug_print::debug_eprintln;
use docker_api::Docker;
use libnss::host::{AddressFamily, Addresses, Host, HostHooks};
use libnss::interop::Response;
use libnss::libnss_host_hooks;
use std::error::Error;
use std::net::{IpAddr, Ipv4Addr};
use std::str::FromStr;
use tokio;

static SUFFIX: &'static str = ".docker";
static DOCKER_URI: &'static str = "unix:///var/run/docker.sock";

struct DockerNG;
libnss_host_hooks!(docker_ng, DockerNG);

impl HostHooks for DockerNG {
    fn get_all_entries() -> Response<Vec<Host>> {
        Response::NotFound // TODO: Implement me, see https://github.com/petski/nss-docker-ng/issues/1
    }

    fn get_host_by_name(name: &str, family: AddressFamily) -> Response<Host> {
        match get_host_by_name(name, family) {
            Ok(Some(host)) => Response::Success(host),
            Ok(None) => Response::NotFound,
            Err(_e) => {
                debug_eprintln!("get_host_by_name '{}' failed: {}", name, _e);
                Response::Unavail
            }
        }
    }

    fn get_host_by_addr(_: IpAddr) -> Response<Host> {
        Response::NotFound // TODO: Implement me, see https://github.com/petski/nss-docker-ng/issues/2
    }
}

#[tokio::main]
async fn get_host_by_name(
    name: &str,
    family: AddressFamily,
) -> Result<Option<Host>, Box<dyn Error>> {
    // Check if the name ends with the expected suffix and if the address family is IPv4
    if !(name.ends_with(SUFFIX) &&
        family == AddressFamily::IPv4 && // TODO you no v6? See https://github.com/petski/nss-docker-ng/issues/3
        name.len() > SUFFIX.len())
    {
        return Ok(None);
    }

    // Initialize Docker API client
    let mut docker = Docker::new(DOCKER_URI)?;
    docker.adjust_api_version().await?;

    // Strip suffix from name
    let name_stripped = &name[..name.len() - SUFFIX.len()];

    // Fetch container information
    let inspect_result = match docker.containers().get(name_stripped).inspect().await {
        Ok(result) => result,
        Err(_e) => {
            debug_eprintln!("Failed to inspect container '{}': {}", name_stripped, _e);
            return Ok(None);
        }
    };

    // From https://docs.docker.com/engine/api/v1.44/#tag/Container/operation/ContainerInspect:
    // > Network mode to use for this container. Supported standard values are: bridge, host, none, and
    // > container:<name|id>. Any other value is taken as a custom network's name to which this container
    // > should connect to
    let network_mode = match inspect_result.host_config.as_ref().and_then(|host_config| {
        host_config
            .get("NetworkMode")
            .and_then(|value| value.as_str())
    }) {
        Some(network_mode) => {
            debug_eprintln!(
                "Container '{}' has NetworkMode '{}'",
                name_stripped,
                network_mode
            );
            network_mode
        }
        None => {
            debug_eprintln!(
                "Could not find NetworkMode for container '{}'",
                name_stripped
            );
            return Err("Could not find NetworkMode".into());
        }
    };

    // NetworkMode host, none, and those starting with "container:" don't have an IP address
    if ["none", "host"].contains(&network_mode) || network_mode.starts_with("container:") {
        debug_eprintln!(
            "Container '{}' is in NetworkMode {}, no IP here",
            name_stripped,
            network_mode
        );
        return Ok(None);
    }

    // Extract networks from network settings
    let networks = match inspect_result
        .network_settings
        .and_then(|settings| settings.networks)
    {
        Some(networks) => {
            if networks.len() == 0 {
                return Err("Found 0 networks".into());
            }
            debug_eprintln!(
                "Found {} network(s) for '{}'",
                networks.keys().len(),
                name_stripped
            );
            networks
        }
        None => {
            debug_eprintln!("Found no networks for '{}'", name_stripped);
            return Err("Found no networks".into());
        }
    };

    // Get the end point settings for the network with the name in network_mode
    let end_point_settings = match networks.get(network_mode) {
        Some(end_point_settings) => end_point_settings,
        None => {
            return Err(format!("Network '{}' not found", network_mode).into());
        }
    };

    return match end_point_settings.clone().ip_address {
        Some(ip_address) => {
            if ip_address.is_empty() {
                return Err("IP address is an empty string".into());
            }
            match Ipv4Addr::from_str(&ip_address) {
                Ok(ip) => Ok(Some(Host {
                    name: name.to_string(),
                    addresses: Addresses::V4(vec![ip]),
                    aliases: Vec::new(),
                })),
                Err(_e) => {
                    return Err(
                        format!("Failed to parse IP address '{}': {}", ip_address, _e).into(),
                    );
                }
            }
        }
        None => Err("Endpoint has no IP address".into()),
    };
}