diff --git a/sources/api/netdog/src/cli/generate_hostname.rs b/sources/api/netdog/src/cli/generate_hostname.rs new file mode 100644 index 00000000000..63fd153e33a --- /dev/null +++ b/sources/api/netdog/src/cli/generate_hostname.rs @@ -0,0 +1,32 @@ +use super::{error, print_json, Result}; +use crate::CURRENT_IP; +use argh::FromArgs; +use dns_lookup::lookup_addr; +use snafu::ResultExt; +use std::fs; +use std::net::IpAddr; +use std::str::FromStr; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "generate-hostname")] +/// Generate hostname from DNS reverse lookup or use current IP +pub(crate) struct GenerateHostnameArgs {} + +/// Attempt to resolve assigned IP address, if unsuccessful use the IP as the hostname. +/// +/// The result is returned as JSON. (intended for use as a settings generator) +pub(crate) fn run() -> Result<()> { + let ip_string = fs::read_to_string(CURRENT_IP) + .context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?; + let ip = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?; + let hostname = match lookup_addr(&ip) { + Ok(hostname) => hostname, + Err(e) => { + eprintln!("Reverse DNS lookup failed: {}", e); + ip_string + } + }; + + // sundog expects JSON-serialized output + print_json(hostname) +} diff --git a/sources/api/netdog/src/cli/generate_net_config.rs b/sources/api/netdog/src/cli/generate_net_config.rs new file mode 100644 index 00000000000..2d239d6fd2a --- /dev/null +++ b/sources/api/netdog/src/cli/generate_net_config.rs @@ -0,0 +1,56 @@ +use super::{error, Result}; +use crate::{net_config, DEFAULT_NET_CONFIG_FILE, KERNEL_CMDLINE, PRIMARY_INTERFACE}; +use argh::FromArgs; +use snafu::{OptionExt, ResultExt}; +use std::{fs, path::Path}; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "generate-net-config")] +/// Generate wicked network configuration +pub(crate) struct GenerateNetConfigArgs {} + +/// Generate configuration for network interfaces. +pub(crate) fn run() -> Result<()> { + let maybe_net_config = if Path::exists(Path::new(DEFAULT_NET_CONFIG_FILE)) { + net_config::from_path(DEFAULT_NET_CONFIG_FILE).context(error::NetConfigParseSnafu { + path: DEFAULT_NET_CONFIG_FILE, + })? + } else { + net_config::from_command_line(KERNEL_CMDLINE).context(error::NetConfigParseSnafu { + path: KERNEL_CMDLINE, + })? + }; + + // `maybe_net_config` could be `None` if no interfaces were defined + let net_config = match maybe_net_config { + Some(net_config) => net_config, + None => { + eprintln!("No network interfaces were configured"); + return Ok(()); + } + }; + + let primary_interface = net_config + .primary_interface() + .context(error::GetPrimaryInterfaceSnafu)?; + write_primary_interface(primary_interface)?; + + let wicked_interfaces = net_config.as_wicked_interfaces(); + for interface in wicked_interfaces { + interface + .write_config_file() + .context(error::InterfaceConfigWriteSnafu)?; + } + Ok(()) +} + +/// Persist the primary interface name to file +fn write_primary_interface(interface: S) -> Result<()> +where + S: AsRef, +{ + let interface = interface.as_ref(); + fs::write(PRIMARY_INTERFACE, interface).context(error::PrimaryInterfaceWriteSnafu { + path: PRIMARY_INTERFACE, + }) +} diff --git a/sources/api/netdog/src/cli/install.rs b/sources/api/netdog/src/cli/install.rs new file mode 100644 index 00000000000..99d404d034a --- /dev/null +++ b/sources/api/netdog/src/cli/install.rs @@ -0,0 +1,93 @@ +use super::{error, InterfaceFamily, InterfaceType, Result}; +use crate::lease::LeaseInfo; +use crate::{CURRENT_IP, PRIMARY_INTERFACE, RESOLV_CONF}; +use argh::FromArgs; +use rand::prelude::SliceRandom; +use rand::thread_rng; +use snafu::ResultExt; +use std::fmt::Write; +use std::fs; +use std::net::IpAddr; +use std::path::PathBuf; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "install")] +/// Write resolv.conf and current IP to disk +pub(crate) struct InstallArgs { + #[argh(option, short = 'i')] + /// name of the network interface + interface_name: String, + + #[argh(option, short = 't')] + /// network interface type + interface_type: InterfaceType, + + #[argh(option, short = 'f')] + /// network interface family (ipv4/6) + interface_family: InterfaceFamily, + + #[argh(positional)] + /// lease info data file + data_file: PathBuf, + + #[argh(positional)] + // wicked adds `info` to the call to this program. We don't do anything with it but must + // be able to parse the option to avoid failing + /// ignored + info: Option, +} + +pub(crate) fn run(args: InstallArgs) -> Result<()> { + // Wicked doesn't mangle interface names, but let's be defensive. + let install_interface = args.interface_name.trim().to_lowercase(); + let primary_interface = fs::read_to_string(PRIMARY_INTERFACE) + .context(error::PrimaryInterfaceReadSnafu { + path: PRIMARY_INTERFACE, + })? + .trim() + .to_lowercase(); + + if install_interface != primary_interface { + return Ok(()); + } + + match (&args.interface_type, &args.interface_family) { + (InterfaceType::Dhcp, InterfaceFamily::Ipv4) => { + let info = + LeaseInfo::from_lease(&args.data_file).context(error::LeaseParseFailedSnafu { + path: &args.data_file, + })?; + // Randomize name server order, for libc implementations like musl that send + // queries to the first N servers. + let mut dns_servers: Vec<_> = info.dns_servers.iter().collect(); + dns_servers.shuffle(&mut thread_rng()); + write_resolv_conf(&dns_servers, &info.dns_search)?; + write_current_ip(&info.ip_address.addr())?; + } + _ => eprintln!("Unhandled 'install' command: {:?}", &args), + } + Ok(()) +} + +/// Write resolver configuration for libc. +fn write_resolv_conf(dns_servers: &[&IpAddr], dns_search: &Option>) -> Result<()> { + let mut output = String::new(); + + if let Some(s) = dns_search { + writeln!(output, "search {}", s.join(" ")).context(error::ResolvConfBuildFailedSnafu)?; + } + + for n in dns_servers { + writeln!(output, "nameserver {}", n).context(error::ResolvConfBuildFailedSnafu)?; + } + + fs::write(RESOLV_CONF, output) + .context(error::ResolvConfWriteFailedSnafu { path: RESOLV_CONF })?; + Ok(()) +} + +/// Persist the current IP address to file +fn write_current_ip(ip: &IpAddr) -> Result<()> { + fs::write(CURRENT_IP, ip.to_string()) + .context(error::CurrentIpWriteFailedSnafu { path: CURRENT_IP }) +} diff --git a/sources/api/netdog/src/cli/mod.rs b/sources/api/netdog/src/cli/mod.rs new file mode 100644 index 00000000000..28d31de52e6 --- /dev/null +++ b/sources/api/netdog/src/cli/mod.rs @@ -0,0 +1,120 @@ +pub(crate) mod generate_hostname; +pub(crate) mod generate_net_config; +pub(crate) mod install; +pub(crate) mod node_ip; +pub(crate) mod prepare_primary_interface; +pub(crate) mod remove; +pub(crate) mod set_hostname; + +pub(crate) use generate_hostname::GenerateHostnameArgs; +pub(crate) use generate_net_config::GenerateNetConfigArgs; +pub(crate) use install::InstallArgs; +pub(crate) use node_ip::NodeIpArgs; +pub(crate) use prepare_primary_interface::PreparePrimaryInterfaceArgs; +pub(crate) use remove::RemoveArgs; +use serde::{Deserialize, Serialize}; +pub(crate) use set_hostname::SetHostnameArgs; +use snafu::ResultExt; + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +enum InterfaceType { + Dhcp, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "kebab-case")] +enum InterfaceFamily { + Ipv4, + Ipv6, +} + +// Implement `from_str()` so argh can attempt to deserialize args into their proper types +derive_fromstr_from_deserialize!(InterfaceType); +derive_fromstr_from_deserialize!(InterfaceFamily); + +/// Helper function that serializes the input to JSON and prints it +fn print_json(val: S) -> Result<()> +where + S: AsRef + Serialize, +{ + let val = val.as_ref(); + let output = serde_json::to_string(val).context(error::JsonSerializeSnafu { output: val })?; + println!("{}", output); + Ok(()) +} + +/// Potential errors during netdog execution +mod error { + use crate::{lease, net_config, wicked}; + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(crate)))] + #[allow(clippy::enum_variant_names)] + pub(crate) enum Error { + #[snafu(display("Failed to write current IP to '{}': {}", path.display(), source))] + CurrentIpWriteFailed { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to read current IP data in '{}': {}", path.display(), source))] + CurrentIpReadFailed { path: PathBuf, source: io::Error }, + + #[snafu(display("'systemd-sysctl' failed: {}", stderr))] + FailedSystemdSysctl { stderr: String }, + + #[snafu(display("Failed to discern primary interface"))] + GetPrimaryInterface, + + #[snafu(display("Failed to write hostname to '{}': {}", path.display(), source))] + HostnameWriteFailed { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to write network interface configuration: {}", source))] + InterfaceConfigWrite { source: wicked::Error }, + + #[snafu(display("Invalid IP address '{}': {}", ip, source))] + IpFromString { + ip: String, + source: std::net::AddrParseError, + }, + + #[snafu(display("Error serializing to JSON: '{}': {}", output, source))] + JsonSerialize { + output: String, + source: serde_json::error::Error, + }, + + #[snafu(display("Failed to read/parse lease data in '{}': {}", path.display(), source))] + LeaseParseFailed { path: PathBuf, source: lease::Error }, + + #[snafu(display("Unable to read/parse network config from '{}': {}", path.display(), source))] + NetConfigParse { + path: PathBuf, + source: net_config::Error, + }, + + #[snafu(display("Failed to write primary interface to '{}': {}", path.display(), source))] + PrimaryInterfaceWrite { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to read primary interface from '{}': {}", path.display(), source))] + PrimaryInterfaceRead { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to build resolver configuration: {}", source))] + ResolvConfBuildFailed { source: std::fmt::Error }, + + #[snafu(display("Failed to write resolver configuration to '{}': {}", path.display(), source))] + ResolvConfWriteFailed { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to build sysctl config: {}", source))] + SysctlConfBuild { source: std::fmt::Error }, + + #[snafu(display("Failed to write sysctl config to '{}': {}", path.display(), source))] + SysctlConfWrite { path: PathBuf, source: io::Error }, + + #[snafu(display("Failed to run 'systemd-sysctl': {}", source))] + SystemdSysctlExecution { source: io::Error }, + } +} + +pub(crate) type Result = std::result::Result; diff --git a/sources/api/netdog/src/cli/node_ip.rs b/sources/api/netdog/src/cli/node_ip.rs new file mode 100644 index 00000000000..50a8dedbdb7 --- /dev/null +++ b/sources/api/netdog/src/cli/node_ip.rs @@ -0,0 +1,23 @@ +use super::{error, print_json, Result}; +use crate::CURRENT_IP; +use argh::FromArgs; +use snafu::ResultExt; +use std::fs; +use std::net::IpAddr; +use std::str::FromStr; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "node-ip")] +/// Return the current IP address +pub(crate) struct NodeIpArgs {} + +/// Return the current IP address as JSON (intended for use as a settings generator) +pub(crate) fn run() -> Result<()> { + let ip_string = fs::read_to_string(CURRENT_IP) + .context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?; + // Validate that we read a proper IP address + let _ = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?; + + // sundog expects JSON-serialized output + print_json(ip_string) +} diff --git a/sources/api/netdog/src/cli/prepare_primary_interface.rs b/sources/api/netdog/src/cli/prepare_primary_interface.rs new file mode 100644 index 00000000000..8f984312ffd --- /dev/null +++ b/sources/api/netdog/src/cli/prepare_primary_interface.rs @@ -0,0 +1,74 @@ +use super::{error, Result}; +use crate::{PRIMARY_INTERFACE, PRIMARY_SYSCTL_CONF, SYSTEMD_SYSCTL}; +use argh::FromArgs; +use snafu::{ensure, ResultExt}; +use std::fmt::Write; +use std::fs; +use std::path::Path; +use std::process::Command; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "prepare-primary-interface")] +/// Sets the default sysctls for the primary interface +pub(crate) struct PreparePrimaryInterfaceArgs {} + +/// Set and apply default sysctls for the primary network interface +pub(crate) fn run() -> Result<()> { + let primary_interface = + fs::read_to_string(PRIMARY_INTERFACE).context(error::PrimaryInterfaceReadSnafu { + path: PRIMARY_INTERFACE, + })?; + write_interface_sysctl(primary_interface, PRIMARY_SYSCTL_CONF)?; + + // Execute `systemd-sysctl` with our configuration file to set the sysctls + let systemd_sysctl_result = Command::new(SYSTEMD_SYSCTL) + .arg(PRIMARY_SYSCTL_CONF) + .output() + .context(error::SystemdSysctlExecutionSnafu)?; + ensure!( + systemd_sysctl_result.status.success(), + error::FailedSystemdSysctlSnafu { + stderr: String::from_utf8_lossy(&systemd_sysctl_result.stderr) + } + ); + Ok(()) +} + +/// Write the default sysctls for a given interface to a given path +fn write_interface_sysctl(interface: S, path: P) -> Result<()> +where + S: AsRef, + P: AsRef, +{ + let interface = interface.as_ref(); + let path = path.as_ref(); + // TODO if we accumulate more of these we should have a better way to create than format!() + // Note: The dash (-) preceding the "net..." variable assignment below is important; it + // ensures failure to set the variable for any reason will be logged, but not cause the sysctl + // service to fail + // Accept router advertisement (RA) packets even if IPv6 forwarding is enabled on interface + let ipv6_accept_ra = format!("-net.ipv6.conf.{}.accept_ra = 2", interface); + // Enable loose mode for reverse path filter + let ipv4_rp_filter = format!("-net.ipv4.conf.{}.rp_filter = 2", interface); + + let mut output = String::new(); + writeln!(output, "{}", ipv6_accept_ra).context(error::SysctlConfBuildSnafu)?; + writeln!(output, "{}", ipv4_rp_filter).context(error::SysctlConfBuildSnafu)?; + + fs::write(path, output).context(error::SysctlConfWriteSnafu { path })?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_sysctls() { + let interface = "eno1"; + let fake_file = tempfile::NamedTempFile::new().unwrap(); + let expected = "-net.ipv6.conf.eno1.accept_ra = 2\n-net.ipv4.conf.eno1.rp_filter = 2\n"; + write_interface_sysctl(interface, &fake_file).unwrap(); + assert_eq!(std::fs::read_to_string(&fake_file).unwrap(), expected); + } +} diff --git a/sources/api/netdog/src/cli/remove.rs b/sources/api/netdog/src/cli/remove.rs new file mode 100644 index 00000000000..c9a240d557b --- /dev/null +++ b/sources/api/netdog/src/cli/remove.rs @@ -0,0 +1,26 @@ +use super::{InterfaceFamily, InterfaceType, Result}; +use argh::FromArgs; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "remove")] +// `wicked` calls `remove` with the below args and failing to parse them can cause an error in +// `wicked`. +/// Does nothing +pub(crate) struct RemoveArgs { + #[argh(option, short = 'i')] + /// name of the network interface + interface_name: String, + + #[argh(option, short = 't')] + /// network interface type + interface_type: InterfaceType, + + #[argh(option, short = 'f')] + /// network interface family (ipv4/6) + interface_family: InterfaceFamily, +} + +pub(crate) fn run(_: RemoveArgs) -> Result<()> { + eprintln!("The 'remove' command is not implemented."); + Ok(()) +} diff --git a/sources/api/netdog/src/cli/set_hostname.rs b/sources/api/netdog/src/cli/set_hostname.rs new file mode 100644 index 00000000000..86c4cecaefa --- /dev/null +++ b/sources/api/netdog/src/cli/set_hostname.rs @@ -0,0 +1,22 @@ +use super::{error, Result}; +use crate::KERNEL_HOSTNAME; +use argh::FromArgs; +use snafu::ResultExt; +use std::fs; + +#[derive(FromArgs, PartialEq, Debug)] +#[argh(subcommand, name = "set-hostname")] +/// Sets the hostname +pub(crate) struct SetHostnameArgs { + #[argh(positional)] + /// hostname for the system + hostname: String, +} + +/// Sets the hostname for the system +pub(crate) fn run(args: SetHostnameArgs) -> Result<()> { + fs::write(KERNEL_HOSTNAME, args.hostname).context(error::HostnameWriteFailedSnafu { + path: KERNEL_HOSTNAME, + })?; + Ok(()) +} diff --git a/sources/api/netdog/src/interface_name.rs b/sources/api/netdog/src/interface_name.rs index 397bbb219f3..f273806d176 100644 --- a/sources/api/netdog/src/interface_name.rs +++ b/sources/api/netdog/src/interface_name.rs @@ -61,9 +61,7 @@ impl TryFrom for InterfaceName { } ); - Ok(Self { - inner: input.to_string(), - }) + Ok(Self { inner: input }) } } diff --git a/sources/api/netdog/src/lease.rs b/sources/api/netdog/src/lease.rs new file mode 100644 index 00000000000..42f66977bbd --- /dev/null +++ b/sources/api/netdog/src/lease.rs @@ -0,0 +1,82 @@ +//! The lease module contains the struct and code needed to parse a wicked DHCP lease file +use ipnet::IpNet; +use lazy_static::lazy_static; +use regex::Regex; +use serde::Deserialize; +use snafu::ResultExt; +use std::collections::BTreeSet; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::net::IpAddr; +use std::path::Path; + +// Matches wicked's shell-like syntax for DHCP lease variables: +// FOO='BAR' -> key=FOO, val=BAR +lazy_static! { + static ref LEASE_PARAM: Regex = Regex::new(r"^(?P[A-Z]+)='(?P.+)'$").unwrap(); +} + +/// Stores fields extracted from a DHCP lease. +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub(crate) struct LeaseInfo { + #[serde(rename = "ipaddr")] + pub(crate) ip_address: IpNet, + #[serde(rename = "dnsservers")] + pub(crate) dns_servers: BTreeSet, + #[serde(rename = "dnsdomain")] + pub(crate) dns_domain: Option, + #[serde(rename = "dnssearch")] + pub(crate) dns_search: Option>, +} + +impl LeaseInfo { + /// Parse lease data file into a LeaseInfo structure. + pub(crate) fn from_lease

(lease_file: P) -> Result + where + P: AsRef, + { + let lease_file = lease_file.as_ref(); + let f = File::open(lease_file).context(error::LeaseReadFailedSnafu { path: lease_file })?; + let f = BufReader::new(f); + + let mut env = Vec::new(); + for line in f.lines() { + let line = line.context(error::LeaseReadFailedSnafu { path: lease_file })?; + // We ignore any line that does not match the regex. + for cap in LEASE_PARAM.captures_iter(&line) { + let key = cap.name("key").map(|k| k.as_str()); + let val = cap.name("val").map(|v| v.as_str()); + if let (Some(k), Some(v)) = (key, val) { + // If present, replace spaces with commas so Envy deserializes into a list. + env.push((k.to_string(), v.replace(' ', ","))) + } + } + } + + // Envy implements a serde `Deserializer` for an iterator of key/value pairs. That lets us + // feed in the key/value pairs from the lease file and get a `LeaseInfo` struct. If not all + // expected values are present in the file, it will fail; any extra values are ignored. + envy::from_iter::<_, LeaseInfo>(env) + .context(error::LeaseParseFailedSnafu { path: lease_file }) + } +} + +mod error { + use snafu::Snafu; + use std::io; + use std::path::PathBuf; + + #[derive(Debug, Snafu)] + #[snafu(visibility(pub(crate)))] + pub(crate) enum Error { + #[snafu(display("Failed to parse lease data in '{}': {}", path.display(), source))] + LeaseParseFailed { path: PathBuf, source: envy::Error }, + + #[snafu(display("Failed to read lease data in '{}': {}", path.display(), source))] + LeaseReadFailed { path: PathBuf, source: io::Error }, + } +} + +pub(crate) use error::Error; +type Result = std::result::Result; diff --git a/sources/api/netdog/src/main.rs b/sources/api/netdog/src/main.rs index 749fce1d6ce..a61df69a22b 100644 --- a/sources/api/netdog/src/main.rs +++ b/sources/api/netdog/src/main.rs @@ -30,28 +30,14 @@ file in `/etc/sysctl.d`, and then executes `systemd-sysctl` to apply them. #[macro_use] extern crate serde_plain; +mod cli; mod interface_name; +mod lease; mod net_config; mod wicked; use argh::FromArgs; -use dns_lookup::lookup_addr; -use envy; -use ipnet::IpNet; -use lazy_static::lazy_static; -use rand::seq::SliceRandom; -use rand::thread_rng; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use snafu::{ensure, OptionExt, ResultExt}; -use std::collections::BTreeSet; -use std::fmt::Write; -use std::fs::{self, File}; -use std::io::{BufRead, BufReader}; -use std::net::IpAddr; -use std::path::{Path, PathBuf}; -use std::process::{self, Command}; -use std::str::FromStr; +use std::process; static RESOLV_CONF: &str = "/etc/resolv.conf"; static KERNEL_HOSTNAME: &str = "/proc/sys/kernel/hostname"; @@ -62,43 +48,6 @@ static DEFAULT_NET_CONFIG_FILE: &str = "/var/lib/bottlerocket/net.toml"; static PRIMARY_SYSCTL_CONF: &str = "/etc/sysctl.d/90-primary_interface.conf"; static SYSTEMD_SYSCTL: &str = "/usr/lib/systemd/systemd-sysctl"; -// Matches wicked's shell-like syntax for DHCP lease variables: -// FOO='BAR' -> key=FOO, val=BAR -lazy_static! { - static ref LEASE_PARAM: Regex = Regex::new(r"^(?P[A-Z]+)='(?P.+)'$").unwrap(); -} - -/// Stores fields extracted from a DHCP lease. -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -struct LeaseInfo { - #[serde(rename = "ipaddr")] - ip_address: IpNet, - #[serde(rename = "dnsservers")] - dns_servers: BTreeSet, - #[serde(rename = "dnsdomain")] - dns_domain: Option, - #[serde(rename = "dnssearch")] - dns_search: Option>, -} - -#[derive(Debug, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case")] -enum InterfaceType { - Dhcp, -} - -#[derive(Debug, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case")] -enum InterfaceFamily { - Ipv4, - Ipv6, -} - -// Implement `from_str()` so argh can attempt to deserialize args into their proper types -derive_fromstr_from_deserialize!(InterfaceType); -derive_fromstr_from_deserialize!(InterfaceFamily); - /// Stores user-supplied arguments. #[derive(FromArgs, PartialEq, Debug)] struct Args { @@ -109,335 +58,25 @@ struct Args { #[derive(FromArgs, PartialEq, Debug)] #[argh(subcommand)] enum SubCommand { - Install(InstallArgs), - Remove(RemoveArgs), - NodeIp(NodeIpArgs), - GenerateHostname(GenerateHostnameArgs), - GenerateNetConfig(GenerateNetConfigArgs), - SetHostname(SetHostnameArgs), - PreparePrimaryInterface(PreparePrimaryInterfaceArgs), -} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "install")] -/// Write resolv.conf and current IP to disk -struct InstallArgs { - #[argh(option, short = 'i')] - /// name of the network interface - interface_name: String, - - #[argh(option, short = 't')] - /// network interface type - interface_type: InterfaceType, - - #[argh(option, short = 'f')] - /// network interface family (ipv4/6) - interface_family: InterfaceFamily, - - #[argh(positional)] - /// lease info data file - data_file: PathBuf, - - #[argh(positional)] - // wicked adds `info` to the call to this program. We don't do anything with it but must - // be able to parse the option to avoid failing - /// ignored - info: Option, -} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "remove")] -// `wicked` calls `remove` with the below args and failing to parse them can cause an error in -// `wicked`. -/// Does nothing -struct RemoveArgs { - #[argh(option, short = 'i')] - /// name of the network interface - interface_name: String, - - #[argh(option, short = 't')] - /// network interface type - interface_type: InterfaceType, - - #[argh(option, short = 'f')] - /// network interface family (ipv4/6) - interface_family: InterfaceFamily, -} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "node-ip")] -/// Return the current IP address -struct NodeIpArgs {} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "generate-hostname")] -/// Generate hostname from DNS reverse lookup or use current IP -struct GenerateHostnameArgs {} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "generate-net-config")] -/// Generate wicked network configuration -struct GenerateNetConfigArgs {} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "set-hostname")] -/// Sets the hostname -struct SetHostnameArgs { - #[argh(positional)] - /// hostname for the system - hostname: String, -} - -#[derive(FromArgs, PartialEq, Debug)] -#[argh(subcommand, name = "prepare-primary-interface")] -/// Sets the default sysctls for the primary interface -struct PreparePrimaryInterfaceArgs {} - -/// Parse lease data file into a LeaseInfo structure. -fn parse_lease_info

(lease_file: P) -> Result -where - P: AsRef, -{ - let lease_file = lease_file.as_ref(); - let f = File::open(lease_file).context(error::LeaseReadFailedSnafu { path: lease_file })?; - let f = BufReader::new(f); - - let mut env = Vec::new(); - for line in f.lines() { - let line = line.context(error::LeaseReadFailedSnafu { path: lease_file })?; - // We ignore any line that does not match the regex. - for cap in LEASE_PARAM.captures_iter(&line) { - let key = cap.name("key").map(|k| k.as_str()); - let val = cap.name("val").map(|v| v.as_str()); - if let (Some(k), Some(v)) = (key, val) { - // If present, replace spaces with commas so Envy deserializes into a list. - env.push((k.to_string(), v.replace(" ", ","))) - } - } - } - - // Envy implements a serde `Deserializer` for an iterator of key/value pairs. That lets us - // feed in the key/value pairs from the lease file and get a `LeaseInfo` struct. If not all - // expected values are present in the file, it will fail; any extra values are ignored. - Ok(envy::from_iter::<_, LeaseInfo>(env) - .context(error::LeaseParseFailedSnafu { path: lease_file })?) -} - -/// Write resolver configuration for libc. -fn write_resolv_conf(dns_servers: &[&IpAddr], dns_search: &Option>) -> Result<()> { - let mut output = String::new(); - - if let Some(s) = dns_search { - writeln!(output, "search {}", s.join(" ")).context(error::ResolvConfBuildFailedSnafu)?; - } - - for n in dns_servers { - writeln!(output, "nameserver {}", n).context(error::ResolvConfBuildFailedSnafu)?; - } - - fs::write(RESOLV_CONF, output) - .context(error::ResolvConfWriteFailedSnafu { path: RESOLV_CONF })?; - Ok(()) -} - -/// Persist the current IP address to file -fn write_current_ip(ip: &IpAddr) -> Result<()> { - fs::write(CURRENT_IP, ip.to_string()) - .context(error::CurrentIpWriteFailedSnafu { path: CURRENT_IP }) -} - -fn install(args: InstallArgs) -> Result<()> { - // Wicked doesn't mangle interface names, but let's be defensive. - let install_interface = args.interface_name.trim().to_lowercase(); - let primary_interface = fs::read_to_string(PRIMARY_INTERFACE) - .context(error::PrimaryInterfaceReadSnafu { - path: PRIMARY_INTERFACE, - })? - .trim() - .to_lowercase(); - - if install_interface != primary_interface { - return Ok(()); - } - - match (&args.interface_type, &args.interface_family) { - (InterfaceType::Dhcp, InterfaceFamily::Ipv4) => { - let info = parse_lease_info(&args.data_file)?; - // Randomize name server order, for libc implementations like musl that send - // queries to the first N servers. - let mut dns_servers: Vec<_> = info.dns_servers.iter().collect(); - dns_servers.shuffle(&mut thread_rng()); - write_resolv_conf(&dns_servers, &info.dns_search)?; - write_current_ip(&info.ip_address.addr())?; - } - _ => eprintln!("Unhandled 'install' command: {:?}", &args), - } - Ok(()) -} - -fn remove(args: RemoveArgs) -> Result<()> { - match ( - &args.interface_name, - &args.interface_type, - &args.interface_family, - ) { - _ => eprintln!("The 'remove' command is not implemented."), - } - Ok(()) -} - -/// Return the current IP address as JSON (intended for use as a settings generator) -fn node_ip() -> Result<()> { - let ip_string = fs::read_to_string(CURRENT_IP) - .context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?; - // Validate that we read a proper IP address - let _ = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?; - - // sundog expects JSON-serialized output - Ok(print_json(ip_string)?) + Install(cli::InstallArgs), + Remove(cli::RemoveArgs), + NodeIp(cli::NodeIpArgs), + GenerateHostname(cli::GenerateHostnameArgs), + GenerateNetConfig(cli::GenerateNetConfigArgs), + SetHostname(cli::SetHostnameArgs), + PreparePrimaryInterface(cli::PreparePrimaryInterfaceArgs), } -/// Attempt to resolve assigned IP address, if unsuccessful use the IP as the hostname. -/// -/// The result is returned as JSON. (intended for use as a settings generator) -fn generate_hostname() -> Result<()> { - let ip_string = fs::read_to_string(CURRENT_IP) - .context(error::CurrentIpReadFailedSnafu { path: CURRENT_IP })?; - let ip = IpAddr::from_str(&ip_string).context(error::IpFromStringSnafu { ip: &ip_string })?; - let hostname = match lookup_addr(&ip) { - Ok(hostname) => hostname, - Err(e) => { - eprintln!("Reverse DNS lookup failed: {}", e); - ip_string - } - }; - - // sundog expects JSON-serialized output - Ok(print_json(hostname)?) -} - -/// Generate configuration for network interfaces. -fn generate_net_config() -> Result<()> { - let maybe_net_config = if Path::exists(Path::new(DEFAULT_NET_CONFIG_FILE)) { - net_config::from_path(DEFAULT_NET_CONFIG_FILE).context(error::NetConfigParseSnafu { - path: DEFAULT_NET_CONFIG_FILE, - })? - } else { - net_config::from_command_line(KERNEL_CMDLINE).context(error::NetConfigParseSnafu { - path: KERNEL_CMDLINE, - })? - }; - - // `maybe_net_config` could be `None` if no interfaces were defined - let net_config = match maybe_net_config { - Some(net_config) => net_config, - None => { - eprintln!("No network interfaces were configured"); - return Ok(()); - } - }; - - let primary_interface = net_config - .primary_interface() - .context(error::GetPrimaryInterfaceSnafu)?; - write_primary_interface(primary_interface)?; - - let wicked_interfaces = net_config.into_wicked_interfaces(); - for interface in wicked_interfaces { - interface - .write_config_file() - .context(error::InterfaceConfigWriteSnafu)?; - } - Ok(()) -} - -/// Persist the primary interface name to file -fn write_primary_interface(interface: S) -> Result<()> -where - S: AsRef, -{ - let interface = interface.as_ref(); - fs::write(PRIMARY_INTERFACE, interface).context(error::PrimaryInterfaceWriteSnafu { - path: PRIMARY_INTERFACE, - }) -} - -/// Helper function that serializes the input to JSON and prints it -fn print_json(val: S) -> Result<()> -where - S: AsRef + Serialize, -{ - let val = val.as_ref(); - let output = serde_json::to_string(val).context(error::JsonSerializeSnafu { output: val })?; - println!("{}", output); - Ok(()) -} - -/// Sets the hostname for the system -fn set_hostname(args: SetHostnameArgs) -> Result<()> { - fs::write(KERNEL_HOSTNAME, args.hostname).context(error::HostnameWriteFailedSnafu { - path: KERNEL_HOSTNAME, - })?; - Ok(()) -} - -/// Set and apply default sysctls for the primary network interface -fn prepare_primary_interface() -> Result<()> { - let primary_interface = - fs::read_to_string(PRIMARY_INTERFACE).context(error::PrimaryInterfaceReadSnafu { - path: PRIMARY_INTERFACE, - })?; - write_interface_sysctl(primary_interface, PRIMARY_SYSCTL_CONF)?; - - // Execute `systemd-sysctl` with our configuration file to set the sysctls - let systemd_sysctl_result = Command::new(SYSTEMD_SYSCTL) - .arg(PRIMARY_SYSCTL_CONF) - .output() - .context(error::SystemdSysctlExecutionSnafu)?; - ensure!( - systemd_sysctl_result.status.success(), - error::FailedSystemdSysctlSnafu { - stderr: String::from_utf8_lossy(&systemd_sysctl_result.stderr) - } - ); - Ok(()) -} - -/// Write the default sysctls for a given interface to a given path -fn write_interface_sysctl(interface: S, path: P) -> Result<()> -where - S: AsRef, - P: AsRef, -{ - let interface = interface.as_ref(); - let path = path.as_ref(); - // TODO if we accumulate more of these we should have a better way to create than format!() - // Note: The dash (-) preceding the "net..." variable assignment below is important; it - // ensures failure to set the variable for any reason will be logged, but not cause the sysctl - // service to fail - // Accept router advertisement (RA) packets even if IPv6 forwarding is enabled on interface - let ipv6_accept_ra = format!("-net.ipv6.conf.{}.accept_ra = 2", interface); - // Enable loose mode for reverse path filter - let ipv4_rp_filter = format!("-net.ipv4.conf.{}.rp_filter = 2", interface); - - let mut output = String::new(); - writeln!(output, "{}", ipv6_accept_ra).context(error::SysctlConfBuildSnafu)?; - writeln!(output, "{}", ipv4_rp_filter).context(error::SysctlConfBuildSnafu)?; - - fs::write(path, output).context(error::SysctlConfWriteSnafu { path })?; - Ok(()) -} - -fn run() -> Result<()> { +fn run() -> cli::Result<()> { let args: Args = argh::from_env(); match args.subcommand { - SubCommand::Install(args) => install(args)?, - SubCommand::Remove(args) => remove(args)?, - SubCommand::NodeIp(_) => node_ip()?, - SubCommand::GenerateHostname(_) => generate_hostname()?, - SubCommand::GenerateNetConfig(_) => generate_net_config()?, - SubCommand::SetHostname(args) => set_hostname(args)?, - SubCommand::PreparePrimaryInterface(_) => prepare_primary_interface()?, + SubCommand::Install(args) => cli::install::run(args)?, + SubCommand::Remove(args) => cli::remove::run(args)?, + SubCommand::NodeIp(_) => cli::node_ip::run()?, + SubCommand::GenerateHostname(_) => cli::generate_hostname::run()?, + SubCommand::GenerateNetConfig(_) => cli::generate_net_config::run()?, + SubCommand::SetHostname(args) => cli::set_hostname::run(args)?, + SubCommand::PreparePrimaryInterface(_) => cli::prepare_primary_interface::run()?, } Ok(()) } @@ -451,96 +90,3 @@ fn main() { process::exit(1); } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn default_sysctls() { - let interface = "eno1"; - let fake_file = tempfile::NamedTempFile::new().unwrap(); - let expected = "-net.ipv6.conf.eno1.accept_ra = 2\n-net.ipv4.conf.eno1.rp_filter = 2\n"; - write_interface_sysctl(interface, &fake_file).unwrap(); - assert_eq!(std::fs::read_to_string(&fake_file).unwrap(), expected); - } -} - -/// Potential errors during netdog execution -mod error { - use crate::{net_config, wicked}; - use envy; - use snafu::Snafu; - use std::io; - use std::path::PathBuf; - - #[derive(Debug, Snafu)] - #[snafu(visibility(pub(super)))] - #[allow(clippy::enum_variant_names)] - pub(super) enum Error { - #[snafu(display("Failed to read lease data in '{}': {}", path.display(), source))] - LeaseReadFailed { path: PathBuf, source: io::Error }, - - #[snafu(display("Failed to parse lease data in '{}': {}", path.display(), source))] - LeaseParseFailed { path: PathBuf, source: envy::Error }, - - #[snafu(display("Failed to build resolver configuration: {}", source))] - ResolvConfBuildFailed { source: std::fmt::Error }, - - #[snafu(display("Failed to write resolver configuration to '{}': {}", path.display(), source))] - ResolvConfWriteFailed { path: PathBuf, source: io::Error }, - - #[snafu(display("Failed to write hostname to '{}': {}", path.display(), source))] - HostnameWriteFailed { path: PathBuf, source: io::Error }, - - #[snafu(display("Invalid IP address '{}': {}", ip, source))] - IpFromString { - ip: String, - source: std::net::AddrParseError, - }, - - #[snafu(display("Failed to write current IP to '{}': {}", path.display(), source))] - CurrentIpWriteFailed { path: PathBuf, source: io::Error }, - - #[snafu(display("Failed to read current IP data in '{}': {}", path.display(), source))] - CurrentIpReadFailed { path: PathBuf, source: io::Error }, - - #[snafu(display("Error serializing to JSON: '{}': {}", output, source))] - JsonSerialize { - output: String, - source: serde_json::error::Error, - }, - - #[snafu(display("Unable to read/parse network config from '{}': {}", path.display(), source))] - NetConfigParse { - path: PathBuf, - source: net_config::Error, - }, - - #[snafu(display("Failed to write network interface configuration: {}", source))] - InterfaceConfigWrite { source: wicked::Error }, - - #[snafu(display("Failed to write primary interface to '{}': {}", path.display(), source))] - PrimaryInterfaceWrite { path: PathBuf, source: io::Error }, - - #[snafu(display("Failed to read primary interface from '{}': {}", path.display(), source))] - PrimaryInterfaceRead { path: PathBuf, source: io::Error }, - - #[snafu(display("Failed to discern primary interface"))] - GetPrimaryInterface, - - #[snafu(display("Failed to build sysctl config: {}", source))] - SysctlConfBuild { source: std::fmt::Error }, - - #[snafu(display("Failed to write sysctl config to '{}': {}", path.display(), source))] - SysctlConfWrite { path: PathBuf, source: io::Error }, - - #[snafu(display("Failed to run 'systemd-sysctl': {}", source))] - SystemdSysctlExecution { source: io::Error }, - - #[snafu(display("'systemd-sysctl' failed: {}", stderr))] - FailedSystemdSysctl { stderr: String }, - } -} - -type Result = std::result::Result; diff --git a/sources/api/netdog/src/net_config/mod.rs b/sources/api/netdog/src/net_config/mod.rs index 99b6a6e4450..a2d5e8cd838 100644 --- a/sources/api/netdog/src/net_config/mod.rs +++ b/sources/api/netdog/src/net_config/mod.rs @@ -29,7 +29,7 @@ pub(crate) trait Interfaces { /// Converts the network config into a list of `WickedInterface` structs, suitable for writing /// to file - fn into_wicked_interfaces(&self) -> Vec; + fn as_wicked_interfaces(&self) -> Vec; } impl Interfaces for Box { @@ -41,8 +41,8 @@ impl Interfaces for Box { (**self).has_interfaces() } - fn into_wicked_interfaces(&self) -> Vec { - (**self).into_wicked_interfaces() + fn as_wicked_interfaces(&self) -> Vec { + (**self).as_wicked_interfaces() } } diff --git a/sources/api/netdog/src/net_config/v1.rs b/sources/api/netdog/src/net_config/v1.rs index 76ae273b971..ed59d2b97ad 100644 --- a/sources/api/netdog/src/net_config/v1.rs +++ b/sources/api/netdog/src/net_config/v1.rs @@ -50,7 +50,7 @@ impl Interfaces for NetConfigV1 { !self.interfaces.is_empty() } - fn into_wicked_interfaces(&self) -> Vec { + fn as_wicked_interfaces(&self) -> Vec { let mut wicked_interfaces = Vec::with_capacity(self.interfaces.len()); for (name, config) in &self.interfaces { let wicked_dhcp4 = config.dhcp4.clone().map(WickedDhcp4::from); @@ -113,7 +113,7 @@ impl FromStr for NetConfigV1 { fn from_str(s: &str) -> std::result::Result { let (name, options) = s - .split_once(":") + .split_once(':') .context(error::InvalidInterfaceDefSnafu { definition: s })?; if options.is_empty() || name.is_empty() { diff --git a/sources/api/netdog/src/wicked.rs b/sources/api/netdog/src/wicked.rs index 36d0475df85..3762c0a6e31 100644 --- a/sources/api/netdog/src/wicked.rs +++ b/sources/api/netdog/src/wicked.rs @@ -247,7 +247,7 @@ mod tests { for ok_str in ok { let net_config = NetConfigV1::from_str(&ok_str).unwrap(); - let wicked_interfaces = net_config.into_wicked_interfaces(); + let wicked_interfaces = net_config.as_wicked_interfaces(); for interface in wicked_interfaces { let generated = quick_xml::se::to_string(&interface).unwrap(); @@ -266,7 +266,7 @@ mod tests { let net_config_path = net_config().join("net_config.toml"); let net_config = net_config::from_path(&net_config_path).unwrap().unwrap(); - let wicked_interfaces = net_config.into_wicked_interfaces(); + let wicked_interfaces = net_config.as_wicked_interfaces(); for interface in wicked_interfaces { let mut path = wicked_config().join(interface.name.to_string()); path.set_extension("xml");