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

Support static addressing via net.toml #2445

Merged
merged 8 commits into from
Sep 30, 2022
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
42 changes: 39 additions & 3 deletions PROVISIONING-METAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ When these services fail, your machine will not connect to any cluster and will
#### `net.toml` structure

The configuration file must be valid TOML and have the filename `net.toml`.
The first and required top level key in the file is `version`, currently only `1` is supported.
The first and required top level key in the file is `version`; the latest is version `2`.
The rest of the file is a map of interface name to supported settings.
Interface names are expected to be correct as per `udevd` naming, no interface naming or matching is supported.
(See the note below regarding `udevd` interface naming.)
Expand All @@ -92,9 +92,22 @@ Interface names are expected to be correct as per `udevd` naming, no interface n
* `enabled` (boolean, required): Enables DHCP6.
* `optional` (boolean): the system will request a lease using this protocol, but will not wait for a valid lease to consider this interface configured.

As of version `2` static addressing with simple routes is supported via the below settings.
Please keep in mind that when using static addresses, DNS information must be supplied to the system via user data: [`settings.dns`](https://github.com/bottlerocket-os/bottlerocket#network-settings).
* `static4` (map): IPv4 static address settings.
* `addresses` (list of quoted IPv4 address including prefix): The desired IPv4 IP addresses, including prefix i.e. `["192.168.14.2/24"]`. The first IP in the list will be used as the primary IP which `kubelet` will use when joining the cluster. If IPv4 and IPv6 static addresses exist, the first IPv4 address is used.
* `static6` (map): IPv6 static address settings.
* `addresses` (list of quoted IPv6 address including prefix): The desired IPv6 IP addresses, including prefix i.e. `["2001:dead:beef::2/64"]`. The first IP in the list will be used as the primary IP which `kubelet` will use when joining the cluster. If IPv4 and IPv6 static addresses exist, the first IPv4 address is used.

zmrow marked this conversation as resolved.
Show resolved Hide resolved
* `route` (map): Static route; multiple routes can be added. (cannot be used in conjuction with DHCP)
* `to` (`"default"` or IP address with prefix, required): Destination address.
* `from` (IP address): Source IP address.
* `via` (IP address): Gateway IP address. If no gateway is provided, a scope of `link` is assumed.
* `route-metric` (integer): Relative route priority.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I do not know if we need to be that specific but this should probably be unsigned integer.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm. I don't think we need to get that specific, but we'll definitely change this if we get reports of folks having issues with it!


Example `net.toml` with comments:
```toml
version = 1
version = 2

# "eno1" is the interface name
[eno1]
Expand All @@ -108,12 +121,35 @@ primary = true
# `enabled` is a boolean and is a required key when
# setting up DHCP this way
enabled = true
# Route metric may be supplied for ipv4
# Route metric may be supplied for IPv4
route-metric = 200

[eno2.dhcp6]
enabled = true
optional = true

[eno3.static4]
addresses = ["10.0.0.10/24", "11.0.0.11/24"]

# Multiple routes may be configured
[[eno3.route]]
to = "default"
via = "10.0.0.1"
route-metric = 100

[[eno3.route]]
to = "default"
via = "11.0.0.1"
route-metric = 200

[eno4.static4]
addresses = ["192.168.14.5/24"]

# Using a source IP and non-default route
[[eno4.route]]
to = "10.10.10.0/24"
from = "192.168.14.5"
via = "192.168.14.25"
```

**An additional note on network device names**
Expand Down
75 changes: 48 additions & 27 deletions sources/api/netdog/src/cli/install.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use super::{error, InterfaceFamily, InterfaceType, Result};
use crate::dns::DnsSettings;
use crate::lease::{lease_path, LeaseInfo};
use crate::lease::{dhcp_lease_path, static_lease_path, LeaseInfo};
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: commit says "we use similar log" where I think "logic" is meant

use crate::{CURRENT_IP, PRIMARY_INTERFACE};
use argh::FromArgs;
use snafu::{OptionExt, ResultExt};
use snafu::{ensure, OptionExt, ResultExt};
use std::fs;
use std::net::IpAddr;
use std::path::PathBuf;
use std::path::{Path, PathBuf};

#[derive(FromArgs, PartialEq, Debug)]
#[argh(subcommand, name = "install")]
Expand Down Expand Up @@ -50,36 +50,57 @@ pub(crate) fn run(args: InstallArgs) -> Result<()> {
}

match (&args.interface_type, &args.interface_family) {
(InterfaceType::Dhcp, InterfaceFamily::Ipv4 | InterfaceFamily::Ipv6) => {
// A lease should exist when using DHCP
let primary_lease_path =
lease_path(&primary_interface).context(error::MissingLeaseSnafu {
interface: primary_interface,
})?;
if args.data_file != primary_lease_path {
return error::PrimaryLeaseConflictSnafu {
wicked_path: args.data_file,
generated_path: primary_lease_path,
}
.fail();
}

// Use DNS API settings if they exist, supplementing any missing settings with settings
// derived from the primary interface's DHCP lease
let lease =
LeaseInfo::from_lease(primary_lease_path).context(error::LeaseParseFailedSnafu)?;
let dns_settings = DnsSettings::from_config_or_lease(Some(&lease))
.context(error::GetDnsSettingsSnafu)?;
dns_settings
.write_resolv_conf()
.context(error::ResolvConfWriteFailedSnafu)?;

(
interface_type @ (InterfaceType::Dhcp | InterfaceType::Static),
InterfaceFamily::Ipv4 | InterfaceFamily::Ipv6,
) => {
let lease = fetch_lease(primary_interface, interface_type, args.data_file)?;
write_resolv_conf(&lease)?;
write_current_ip(&lease.ip_address.addr())?;
}
}
Ok(())
}

/// Given an interface, its type, and wicked's known location of the lease, compare our known lease
/// location, parse and return a LeaseInfo.
fn fetch_lease<S, P>(
interface: S,
interface_type: &InterfaceType,
data_file: P,
) -> Result<LeaseInfo>
where
S: AsRef<str>,
P: AsRef<Path>,
{
let interface = interface.as_ref();
let data_file = data_file.as_ref();
let lease_path = match interface_type {
InterfaceType::Dhcp => dhcp_lease_path(interface),
InterfaceType::Static => static_lease_path(interface),
}
.context(error::MissingLeaseSnafu { interface })?;

ensure!(
data_file == lease_path,
error::PrimaryLeaseConflictSnafu {
wicked_path: data_file,
generated_path: lease_path,
}
);

LeaseInfo::from_lease(&lease_path).context(error::LeaseParseFailedSnafu)
}

/// Given a lease, fetch DNS settings from the lease and/or config and write the resolv.conf
fn write_resolv_conf(lease: &LeaseInfo) -> Result<()> {
let dns_settings =
DnsSettings::from_config_or_lease(Some(lease)).context(error::GetDnsSettingsSnafu)?;
dns_settings
.write_resolv_conf()
.context(error::ResolvConfWriteFailedSnafu)
}

/// Persist the current IP address to file
fn write_current_ip(ip: &IpAddr) -> Result<()> {
fs::write(CURRENT_IP, ip.to_string())
Expand Down
1 change: 1 addition & 0 deletions sources/api/netdog/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub(crate) use write_resolv_conf::WriteResolvConfArgs;
#[serde(rename_all = "kebab-case")]
enum InterfaceType {
Dhcp,
Static,
}

#[derive(Debug, PartialEq, Deserialize)]
Expand Down
9 changes: 5 additions & 4 deletions sources/api/netdog/src/cli/write_resolv_conf.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::{error, Result};
use crate::dns::DnsSettings;
use crate::lease::{lease_path, LeaseInfo};
use crate::lease::{dhcp_lease_path, LeaseInfo};
use crate::PRIMARY_INTERFACE;
use argh::FromArgs;
use snafu::ResultExt;
Expand All @@ -12,16 +12,17 @@ use std::fs;
pub(crate) struct WriteResolvConfArgs {}

pub(crate) fn run() -> Result<()> {
// Use DNS API settings if they exist, supplementing any missing settings with settings
// derived from the primary interface's DHCP lease if it exists
// Use DNS API settings if they exist, supplementing any missing settings with settings derived
// from the primary interface's DHCP lease if it exists. Static leases don't contain any DNS
// data, so don't bother looking there.
let primary_interface = fs::read_to_string(PRIMARY_INTERFACE)
.context(error::PrimaryInterfaceReadSnafu {
path: PRIMARY_INTERFACE,
})?
.trim()
.to_lowercase();

let primary_lease_path = lease_path(&primary_interface);
let primary_lease_path = dhcp_lease_path(&primary_interface);
let dns_settings = if let Some(primary_lease_path) = primary_lease_path {
let lease =
LeaseInfo::from_lease(&primary_lease_path).context(error::LeaseParseFailedSnafu)?;
Expand Down
2 changes: 1 addition & 1 deletion sources/api/netdog/src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl DnsSettings {
/// Merge missing DNS settings into `self` using DHCP lease
fn merge_lease(&mut self, lease: &LeaseInfo) {
if self.nameservers.is_none() {
self.nameservers = Some(lease.dns_servers.clone());
self.nameservers = lease.dns_servers.clone();
}

if self.search.is_none() {
Expand Down
34 changes: 29 additions & 5 deletions sources/api/netdog/src/lease.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ lazy_static! {
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub(crate) struct LeaseInfo {
// When multiple IP addresses exist for an interface, the second address's key in the lease
// file will be `IPADDR_1`, `IPADDR_2`, and so on. Parsing the lease for "ipaddr" means we
// will always pick up the first configured IP address.
#[serde(rename = "ipaddr")]
pub(crate) ip_address: IpNet,
#[serde(rename = "dnsservers")]
pub(crate) dns_servers: BTreeSet<IpAddr>,
pub(crate) dns_servers: Option<BTreeSet<IpAddr>>,
#[serde(rename = "dnsdomain")]
pub(crate) dns_domain: Option<String>,
#[serde(rename = "dnssearch")]
Expand Down Expand Up @@ -63,15 +66,36 @@ impl LeaseInfo {
}
}

/// Return the path to a given interface's ipv4/ipv6 lease if it exists, favoring ipv4 if both
/// Return the path to a given interface's DHCP ipv4/ipv6 lease if it exists, favoring ipv4 if both
/// ipv4 and ipv6 exist
pub(crate) fn lease_path<S>(interface: S) -> Option<PathBuf>
pub(crate) fn dhcp_lease_path<S>(interface: S) -> Option<PathBuf>
where
S: AsRef<str>,
{
get_lease_path("dhcp", interface)
}

/// Return the path to a given interface's static ipv4/ipv6 lease if it exists, favoring ipv4 if
/// both ipv4 and ipv6 exist
pub(crate) fn static_lease_path<S>(interface: S) -> Option<PathBuf>
where
S: AsRef<str>,
{
get_lease_path("static", interface)
}

/// Given a lease type and interface, return the path to the ipv4/6 lease file if it exists,
/// favoring ipv4 if both ipv4 and ipv6 exist
fn get_lease_path<S1, S2>(lease_type: S1, interface: S2) -> Option<PathBuf>
where
S1: AsRef<str>,
S2: AsRef<str>,
{
let lease_type = lease_type.as_ref();
let interface = interface.as_ref();
let ipv4 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.dhcp.ipv4", interface));
let ipv6 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.dhcp.ipv6", interface));

let ipv4 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.{}.ipv4", interface, lease_type));
let ipv6 = Path::new(LEASE_DIR).join(format!("leaseinfo.{}.{}.ipv6", interface, lease_type));

// If both ipv4 and ipv6 leases exist, use the ipv4 lease for DNS settings
let ipv4_exists = Path::exists(&ipv4);
Expand Down
6 changes: 5 additions & 1 deletion sources/api/netdog/src/net_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
//! These structures are the user-facing options for configuring one or more network interfaces.
mod dhcp;
mod error;
mod static_address;
mod v1;
mod v2;

use crate::wicked::WickedInterface;
pub(crate) use dhcp::{Dhcp4ConfigV1, Dhcp4OptionsV1, Dhcp6ConfigV1, Dhcp6OptionsV1};
pub(crate) use error::{Error, Result};
use serde::Deserialize;
use snafu::{ensure, ResultExt};
pub(crate) use static_address::{RouteTo, RouteV1, StaticConfigV1};
use std::fs;
use std::path::Path;
use std::str::FromStr;
Expand Down Expand Up @@ -93,7 +96,8 @@ fn deserialize_config(config_str: &str) -> Result<Box<dyn Interfaces>> {
} = toml::from_str(config_str).context(error::NetConfigParseSnafu)?;

let net_config: Box<dyn Interfaces> = match version {
1 => validate_config::<NetConfigV1>(interface_config)?,
1 => validate_config::<v1::NetConfigV1>(interface_config)?,
2 => validate_config::<v2::NetConfigV2>(interface_config)?,
_ => {
return error::InvalidNetConfigSnafu {
reason: format!("Unknown network config version: {}", version),
Expand Down
65 changes: 65 additions & 0 deletions sources/api/netdog/src/net_config/static_address.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use ipnet::IpNet;
use serde::Deserialize;
use snafu::ResultExt;
use std::collections::BTreeSet;
use std::convert::TryFrom;
use std::net::IpAddr;

#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct StaticConfigV1 {
pub(crate) addresses: BTreeSet<IpNet>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub(crate) struct RouteV1 {
pub(crate) to: RouteTo,
pub(crate) from: Option<IpAddr>,
pub(crate) via: Option<IpAddr>,
#[serde(rename = "route-metric")]
pub(crate) route_metric: Option<u32>,
}

#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "String")]
pub(crate) enum RouteTo {
DefaultRoute,
Ip(IpNet),
}

// Allows the user to pass the string "default" or a valid ip address prefix. We can't use an
// untagged enum for this (#[serde(untagged)]) because "default" directly maps to one of the
// variants. Serde will only allow the "untagged" attribute if neither variant directly matches.
impl TryFrom<String> for RouteTo {
type Error = error::Error;

fn try_from(input: String) -> Result<Self> {
let input = input.to_lowercase();
Ok(match input.as_str() {
"default" => RouteTo::DefaultRoute,
_ => {
let ip: IpNet = input
.parse()
.context(error::InvalidRouteDestinationSnafu { input })?;
RouteTo::Ip(ip)
}
})
}
}

mod error {
use snafu::Snafu;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub(crate) enum Error {
#[snafu(display("Invalid route destination, must be 'default' or a valid IP address prefix. Received '{}': {}", input, source))]
InvalidRouteDestination {
input: String,
source: ipnet::AddrParseError,
},
}
}

type Result<T> = std::result::Result<T, error::Error>;
3 changes: 3 additions & 0 deletions sources/api/netdog/src/net_config/test_macros/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
pub(super) mod basic;
#[cfg(test)]
pub(super) mod dhcp;
#[cfg(test)]
pub(super) mod static_address;

pub(super) use basic::basic_tests;
pub(super) use dhcp::dhcp_tests;
pub(super) use static_address::static_address_tests;

/// gen_boilerplate!() is a convenience macro meant to be used inside of test macros to generate
/// some generally useful boilerplate code. It creates a `VERSION` constant in case the test
Expand Down
Loading