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
160
161
162
163
164
165
166
#[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;

static SUFFIX: &str = ".docker";
static DOCKER_URI: &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(
    query: &str,
    family: AddressFamily,
) -> Result<Option<Host>, Box<dyn Error>> {
    // Check if the query ends with the expected suffix and if the address family is IPv4
    if !(query.ends_with(SUFFIX) &&
        family == AddressFamily::IPv4 && // TODO you no v6? See https://github.com/petski/nss-docker-ng/issues/3
        query.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 query
    let query_stripped = &query[..query.len() - SUFFIX.len()];

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

    // Name of the container (remove leading "/" if necessary)
    let name = match inspect_result.name {
        Some(mut name) => {
            if name.starts_with('/') {
                name.remove(0);
            }
            name
        }
        None => return Err("No Name".into()),
    };

    // 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 mut 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, network_mode);
            network_mode
        }
        None => 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,
            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.is_empty() {
                return Err("Found 0 networks".into());
            }
            debug_eprintln!("Found {} network(s) for '{}'", networks.keys().len(), name);
            networks
        }
        None => return Err("Found no networks".into()),
    };

    // The documentation on https://docs.docker.com/engine/api/v1.44/#tag/Container/operation/ContainerInspect
    // is incomplete. There is another NetworkMode "default":
    // > which is bridge for Docker Engine, and overlay for Swarm.
    //
    // See: https://github.com/docker/docker-py/issues/986
    if network_mode == "default" && networks.get("default").is_none() {
        network_mode = "bridge"; // TODO add swwarm support
    }

    // 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()),
    };

    let ip_address = match &end_point_settings.ip_address {
        Some(ip_address) => {
            if ip_address.is_empty() {
                return Err("IP address is an empty string".into());
            }
            ip_address
        }
        None => return Err("Endpoint has no IP address".into()),
    };

    return match Ipv4Addr::from_str(ip_address) {
        Ok(ip) => {
            let id = match inspect_result.id.as_ref() {
                Some(id) => id,
                None => return Err("No Id".into()),
            };

            Ok(Some(Host {
                name: [name.to_string(), SUFFIX.to_string()].join(""),
                addresses: Addresses::V4(vec![ip]),
                aliases: vec![[id[..12].to_string(), SUFFIX.to_string()].join("")],
            }))
        }
        Err(_e) => {
            return Err(format!("Failed to parse IP address '{}': {}", ip_address, _e).into());
        }
    };
}