Skip to content

Commit

Permalink
Merge pull request #2445 from zmrow/static-addressing
Browse files Browse the repository at this point in the history
Support static addressing via `net.toml`
  • Loading branch information
zmrow authored Sep 30, 2022
2 parents 15fc86a + f50bbbd commit d752435
Show file tree
Hide file tree
Showing 35 changed files with 1,029 additions and 178 deletions.
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.

* `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.

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

0 comments on commit d752435

Please sign in to comment.