Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dogtag: implement imds and reverse dns hostname tools #3898

Merged
merged 1 commit into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions sources/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions sources/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ members = [

"imdsclient",

"dogtag",

"driverdog",

"early-boot-config/early-boot-config",
Expand Down
30 changes: 30 additions & 0 deletions sources/dogtag/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[package]
name = "dogtag"
version = "0.1.0"
authors = ["Jarrett Tierney <[email protected]>"]
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" }
16 changes: 16 additions & 0 deletions sources/dogtag/README.md
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not ship the IMDS resolver on non-aws variants.


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`.
9 changes: 9 additions & 0 deletions sources/dogtag/README.tpl
Original file line number Diff line number Diff line change
@@ -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`.
39 changes: 39 additions & 0 deletions sources/dogtag/bin/imds.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use dogtag::Cli;
use imdsclient::ImdsClient;
use snafu::{OptionExt, ResultExt};

type Result<T> = std::result::Result<T, error::Error>;

/// 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
Comment on lines +7 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these comments apply; imdsclient always uses IMDSv2 and the IPv4 address (which is still valid on IPV6-only ENIs).

#[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"))]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "IMDS"

NoHostname,
}
}
34 changes: 34 additions & 0 deletions sources/dogtag/bin/reverse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use dns_lookup::lookup_addr;
use dogtag::Cli;
use snafu::ResultExt;

type Result<T> = std::result::Result<T, error::Error>;

/// 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<std::net::AddrParseError>,
},
#[snafu(display("Failed to lookup hostname via dns {}", source))]
Lookup {
#[snafu(source(from(std::io::Error, Box::new)))]
source: Box<std::io::Error>,
},
}
}
3 changes: 3 additions & 0 deletions sources/dogtag/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
generate_readme::from_lib().unwrap()
}
84 changes: 84 additions & 0 deletions sources/dogtag/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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";
jmt-lab marked this conversation as resolved.
Show resolved Hide resolved

/// 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<T> = std::result::Result<T, error::Error>;

/// find_hostname will utilize the helpers located in /var/bottlerocket/dogtag/ to try and discover the hostname
pub async fn find_hostname() -> Result<String> {
debug!(
"attempting to discover hostname helpers in {}",
DOGTAG_BIN_PATH
);
// We want to do reverse sort as we want to prioritize higher numbers first
jmt-lab marked this conversation as resolved.
Show resolved Hide resolved
// 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<PathBuf> = WalkDir::new(DOGTAG_BIN_PATH)
.max_depth(1)
.min_depth(1)
.sort_by_file_name()
.into_iter()
.collect::<std::result::Result<Vec<_>, _>>()
.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);
Comment on lines +52 to +55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see where we are passing the ip_address argument, which at least the reverse DNS helper needs or else it will immediately error out.

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 {})
Comment on lines +51 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original logic in ab3b3e6 had a fallback where the host's IP address was used in a slightly cleaned up way (dots replaced with dashes) in these two cases:

  1. lookup_addr() returns the IP address rather than a name
  2. no host is found at all

That logic was later removed in 0de2d11 since in some cases (EKS-A clusters) we want the hostname to exactly match the IP.

0b9658f fixed a different bug related to direct use of an IPv6 address, where colons needed to be replaced with dashes.

Is that handled in netdog? It might be cleaner to do it here for separation of concerns, so netdog only has to put in an IP address and it gets back a hostname (from IMDS, reverse DNS, IPv4 address, or sanitized IPv6 address).

}

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,
}
}