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