diff --git a/sources/Cargo.lock b/sources/Cargo.lock index 3367f2a72f5..77f26dd4df8 100644 --- a/sources/Cargo.lock +++ b/sources/Cargo.lock @@ -1672,6 +1672,20 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dogtag" +version = "0.1.0" +dependencies = [ + "argh", + "dns-lookup", + "generate-readme", + "imdsclient", + "log", + "snafu 0.8.2", + "tokio", + "walkdir", +] + [[package]] name = "driverdog" version = "0.1.0" diff --git a/sources/Cargo.toml b/sources/Cargo.toml index 56502873aeb..c0a8e8f95e2 100644 --- a/sources/Cargo.toml +++ b/sources/Cargo.toml @@ -89,6 +89,8 @@ members = [ "imdsclient", + "dogtag", + "driverdog", "early-boot-config/early-boot-config", diff --git a/sources/dogtag/Cargo.toml b/sources/dogtag/Cargo.toml new file mode 100644 index 00000000000..dbca249a23d --- /dev/null +++ b/sources/dogtag/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "dogtag" +version = "0.1.0" +authors = ["Jarrett Tierney "] +license = "Apache-2.0 OR MIT" +edition = "2021" +publish = false +exclude = ["README.md"] + +[[bin]] +name = "20-imds" +path = "bin/imds.rs" + +[[bin]] +name = "10-reverse-dns" +path = "bin/reverse.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +argh = "0.1" +dns-lookup = "2" +imdsclient = { version = "0.1", path = "../imdsclient" } +log = "0.4" +snafu = "0.8" +tokio = { version = "~1.32", features = ["macros"]} # LTS +walkdir = "2" + +[build-dependencies] +generate-readme = { version = "0.1", path = "../generate-readme" } diff --git a/sources/dogtag/README.md b/sources/dogtag/README.md new file mode 100644 index 00000000000..875a509d7a2 --- /dev/null +++ b/sources/dogtag/README.md @@ -0,0 +1,16 @@ +# dogtag + +Current version: 0.1.0 + +dogtag resolves the hostname of a bottlerocket server/instance. It's used to generate settings.network.hostname. To accomplish this, it uses a set of standalone binaries in /var/bottlerocket/dogtag that resolve the hostname via different methods. + +Currently, bottlerocket ships with two hostname resolver binaries: + +20-imds - Fetches hostname from EC2 Instance Metadata Service +10-reverse-dns - Uses reverse DNS lookup to resolve the hostname + +dogtag runs the resolvers in /var/bottlerocket/dogtag in reverse alphanumerical order until one of them returns a hostname, at which point it will exit early and print the returned hostname to stdout. + +## Colophon + +This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/dogtag/README.tpl b/sources/dogtag/README.tpl new file mode 100644 index 00000000000..7b992c507f5 --- /dev/null +++ b/sources/dogtag/README.tpl @@ -0,0 +1,9 @@ +# {{crate}} + +Current version: {{version}} + +{{readme}} + +## Colophon + +This text was generated from `README.tpl` using [cargo-readme](https://crates.io/crates/cargo-readme), and includes the rustdoc from `src/main.rs`. diff --git a/sources/dogtag/bin/imds.rs b/sources/dogtag/bin/imds.rs new file mode 100644 index 00000000000..e2e02ed1b9a --- /dev/null +++ b/sources/dogtag/bin/imds.rs @@ -0,0 +1,39 @@ +use dogtag::Cli; +use imdsclient::ImdsClient; +use snafu::{OptionExt, ResultExt}; + +type Result = std::result::Result; + +/// Implements a hostname lookup tool by fetching the public hostname +/// from the instance metadata via IMDS. It will interface with IMDS +/// via: +/// +/// * Check for IPv6, default to IPv4 if not available +/// * Check for IMDSv2, fallback to IMDSv1 if not enabled +#[tokio::main] +async fn main() -> Result<()> { + // Even though for this helper we do not need any arguments + // still validate to ensure the helper follows standards. + let _: Cli = argh::from_env(); + let mut imds = ImdsClient::new(); + let hostname = imds + .fetch_hostname() + .await + .context(error::ImdsSnafu)? + .context(error::NoHostnameSnafu)?; + println!("{}", hostname); + Ok(()) +} + +mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(super) enum Error { + #[snafu(display("failed to fetch hostname from IMDS: {}", source))] + Imds { source: imdsclient::Error }, + #[snafu(display("no hostname returned by imds"))] + NoHostname, + } +} diff --git a/sources/dogtag/bin/reverse.rs b/sources/dogtag/bin/reverse.rs new file mode 100644 index 00000000000..633494b621f --- /dev/null +++ b/sources/dogtag/bin/reverse.rs @@ -0,0 +1,34 @@ +use dns_lookup::lookup_addr; +use dogtag::Cli; +use snafu::ResultExt; + +type Result = std::result::Result; + +/// Looks up the public hostname by using dns-lookup to +/// resolve it from the ip address provided +fn main() -> Result<()> { + let cli: Cli = argh::from_env(); + let ip: std::net::IpAddr = cli.ip_address.parse().context(error::InvalidIpSnafu)?; + let hostname = lookup_addr(&ip).context(error::LookupSnafu)?; + println!("{}", hostname); + Ok(()) +} + +mod error { + use snafu::Snafu; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(super)))] + pub(super) enum Error { + #[snafu(display("Invalid ip address passed to tool {}", source))] + InvalidIp { + #[snafu(source(from(std::net::AddrParseError, Box::new)))] + source: Box, + }, + #[snafu(display("Failed to lookup hostname via dns {}", source))] + Lookup { + #[snafu(source(from(std::io::Error, Box::new)))] + source: Box, + }, + } +} diff --git a/sources/dogtag/build.rs b/sources/dogtag/build.rs new file mode 100644 index 00000000000..6ea8d9b39d9 --- /dev/null +++ b/sources/dogtag/build.rs @@ -0,0 +1,3 @@ +fn main() { + generate_readme::from_lib().unwrap() +} diff --git a/sources/dogtag/src/lib.rs b/sources/dogtag/src/lib.rs new file mode 100644 index 00000000000..0e220c6fb6a --- /dev/null +++ b/sources/dogtag/src/lib.rs @@ -0,0 +1,84 @@ +/*! +dogtag resolves the hostname of a bottlerocket server/instance. It's used to generate settings.network.hostname. To accomplish this, it uses a set of standalone binaries in /var/bottlerocket/dogtag that resolve the hostname via different methods. + +Currently, bottlerocket ships with two hostname resolver binaries: + +20-imds - Fetches hostname from EC2 Instance Metadata Service +10-reverse-dns - Uses reverse DNS lookup to resolve the hostname + +dogtag runs the resolvers in /var/bottlerocket/dogtag in reverse alphanumerical order until one of them returns a hostname, at which point it will exit early and print the returned hostname to stdout. + */ +use argh::FromArgs; +use log::debug; +use snafu::ResultExt; +use std::{path::PathBuf, process}; +use walkdir::WalkDir; + +const DOGTAG_BIN_PATH: &str = "/var/bottlerocket/dogtag"; + +/// Cli defines the standard cmdline interface for all hostname handlers +#[derive(FromArgs)] +#[argh(description = "hostname resolution tool")] +pub struct Cli { + #[argh(option)] + #[argh(description = "ip_address of the host")] + pub ip_address: String, +} + +pub type Result = std::result::Result; + +/// find_hostname will utilize the helpers located in /var/bottlerocket/dogtag/ to try and discover the hostname +pub async fn find_hostname() -> Result { + debug!( + "attempting to discover hostname helpers in {}", + DOGTAG_BIN_PATH + ); + // We want to do reverse sort as we want to prioritize higher numbers first + // this is because it makes it easier to add more of these and not have to worry about + // bumping the binary name for existing ones + let mut hostname_helpers: Vec = WalkDir::new(DOGTAG_BIN_PATH) + .max_depth(1) + .min_depth(1) + .sort_by_file_name() + .into_iter() + .collect::, _>>() + .context(error::WalkdirSnafu)? + .into_iter() + .map(|x| x.into_path()) + .collect(); + hostname_helpers.reverse(); + + for helper in hostname_helpers.iter() { + let output = process::Command::new(helper) + .output() + .map(Some) + .unwrap_or(None); + if let Some(output) = output.as_ref() { + // Read the std output + if output.status.success() { + let hostname = String::from_utf8_lossy(output.stdout.as_slice()).to_string(); + return Ok(hostname.trim().to_string()); + } + } + } + Err(error::Error::NoHelper {}) +} + +pub mod error { + use snafu::Snafu; + + #[derive(Snafu, Debug)] + #[snafu(visibility(pub))] + pub enum Error { + #[snafu(display("Failed to detect hostname due to an io error: {}", source))] + Walkdir { source: walkdir::Error }, + #[snafu(display( + "Failed to detect hostname, no helpers are installed in path or io error occurred" + ))] + NoHelper, + #[snafu(display( + "Failed to detect hostname, no helper installed was able to resolve the hostname" + ))] + FailHostname, + } +}