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 DHCP option lookup from NetworkManager #861

Merged
merged 4 commits into from
Feb 1, 2023
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
460 changes: 457 additions & 3 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ slog-term = ">= 2.6, < 3"
tempfile = ">= 3.2, < 4"
users = "0.11"
vmw_backdoor = "0.2"
zbus = ">= 2.3, < 4"

[dev-dependencies]
mockito = ">= 0.29, < 0.32"
3 changes: 3 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ nav_order: 8

Major changes:

- Support reading DHCP options from NetworkManager, fixing 30s delay on
Azure, Azure Stack, and CloudStack

Minor changes:

- Add `AWS_AVAILABILITY_ZONE_ID` attribute to AWS
- Add release notes to documentation
- Fix default dependency ordering on all `checkin` services
- Fix failure setting SSH keys on IBM Cloud if none are provided
- Don't ignore network interfaces that appear during DHCP option lookup retry

Packaging changes:

Expand Down
6 changes: 2 additions & 4 deletions src/providers/cloudstack/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ use openssh_keys::PublicKey;

use crate::providers::MetadataProvider;
use crate::retry;
use crate::util;

const SERVER_ADDRESS: &str = "SERVER_ADDRESS";
use crate::util::DhcpOption;

#[derive(Clone, Debug)]
pub struct CloudstackNetwork {
Expand Down Expand Up @@ -38,7 +36,7 @@ impl CloudstackNetwork {
#[cfg(test)]
return Ok(mockito::server_url());
}
let server = util::dns_lease_key_lookup(SERVER_ADDRESS)?;
let server = DhcpOption::DhcpServerId.get_value()?;
let ip = server
.parse::<IpAddr>()
.with_context(|| format!("failed to parse server ip address: {}", server))?;
Expand Down
10 changes: 5 additions & 5 deletions src/providers/microsoft/azure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,12 @@ impl Azure {

#[cfg(not(test))]
fn get_fabric_address_from_dhcp() -> Result<IpAddr> {
let v = crate::util::dns_lease_key_lookup("OPTION_245")?;
// value is an 8 digit hex value. convert it to u32 and
// then parse that into an ip. Ipv4Addr::from(u32)
// performs conversion from big-endian
let v = crate::util::DhcpOption::AzureFabricAddress.get_value()?;
bgilbert marked this conversation as resolved.
Show resolved Hide resolved
// value is an 8 digit hex value, with colons if it came from
// NetworkManager. Convert it to u32 and then parse that into an
// IP. Ipv4Addr::from(u32) performs conversion from big-endian.
slog_scope::trace!("found fabric address in hex - {:?}", v);
let dec = u32::from_str_radix(&v, 16)
let dec = u32::from_str_radix(&v.replace(':', ""), 16)
.with_context(|| format!("failed to convert '{}' from hex", v))?;
Ok(IpAddr::V4(dec.into()))
}
Expand Down
10 changes: 5 additions & 5 deletions src/providers/microsoft/azurestack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,12 @@ impl AzureStack {

#[cfg(not(test))]
fn get_fabric_address_from_dhcp() -> Result<IpAddr> {
let v = crate::util::dns_lease_key_lookup("OPTION_245")?;
// value is an 8 digit hex value. convert it to u32 and
// then parse that into an ip. Ipv4Addr::from(u32)
// performs conversion from big-endian
let v = crate::util::DhcpOption::AzureFabricAddress.get_value()?;
// value is an 8 digit hex value, with colons if it came from
// NetworkManager. Convert it to u32 and then parse that into an
// IP. Ipv4Addr::from(u32) performs conversion from big-endian.
slog_scope::trace!("found fabric address in hex - {:?}", v);
let dec = u32::from_str_radix(&v, 16)
let dec = u32::from_str_radix(&v.replace(':', ""), 16)
.with_context(|| format!("failed to convert '{}' from hex", v))?;
Ok(IpAddr::V4(dec.into()))
}
Expand Down
167 changes: 167 additions & 0 deletions src/util/dhcp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Copyright 2017 CoreOS, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! DHCP lease option lookup

use anyhow::{anyhow, Context, Result};
use slog_scope::{debug, trace};
use std::collections::HashMap;
use std::fs::File;
use std::path::Path;
use std::time::Duration;
use zbus::{dbus_proxy, zvariant};

use super::key_lookup;
use crate::retry;

pub enum DhcpOption {
DhcpServerId,
// avoid dead code warnings with cfg(test)
#[allow(dead_code)]
AzureFabricAddress,
}

impl DhcpOption {
pub fn get_value(&self) -> Result<String> {
retry::Retry::new()
.initial_backoff(Duration::from_millis(50))
.max_backoff(Duration::from_millis(500))
.max_retries(60)
.retry(|_| {
match self.try_nm() {
Ok(res) => return Ok(res),
Err(e) => trace!("failed querying NetworkManager: {e:#}"),
}
match self.try_networkd() {
Ok(res) => return Ok(res),
Err(e) => trace!("failed querying networkd: {e:#}"),
}
Err(anyhow!("failed to acquire DHCP option"))
})
}

fn try_nm(&self) -> Result<String> {
let key = match *self {
Self::DhcpServerId => "dhcp_server_identifier",
Self::AzureFabricAddress => "private_245",
};

// We set up everything from scratch on every attempt. This isn't
// super-efficient but is simple and clear.
//
// We'd like to set both `property` and `object` attributes on the
// trait methods, but that fails to compile, so we create proxies by
// hand.

// query NM for active connections
let bus = zbus::blocking::Connection::system().context("connecting to D-Bus")?;
let nm = NetworkManagerProxyBlocking::new(&bus).context("creating NetworkManager proxy")?;
let conn_paths = nm
.active_connections()
.context("listing active connections")?;

// walk active connections
for conn_path in conn_paths {
if conn_path == "/" {
continue;
}
trace!("found NetworkManager connection: {conn_path}");
let conn = NMActiveConnectionProxyBlocking::builder(&bus)
.path(conn_path)
.context("setting connection path")?
.build()
.context("creating connection proxy")?;

// get DHCP options
let dhcp_path = conn.dhcp4_config().context("getting DHCP config")?;
if dhcp_path == "/" {
continue;
}
debug!("checking DHCP config: {dhcp_path}");
let dhcp = NMDhcp4ConfigProxyBlocking::builder(&bus)
.path(dhcp_path)
.context("setting DHCP config path")?
.build()
.context("creating DHCP config proxy")?;
let options = dhcp.options().context("getting DHCP options")?;

// check for option
if let Some(value) = options.get(key) {
return value.try_into().context("reading DHCP option as string");
}
}

// not found
Err(anyhow!("failed to acquire DHCP option {key}"))
}

fn try_networkd(&self) -> Result<String> {
let key = match *self {
Self::DhcpServerId => "SERVER_ADDRESS",
Self::AzureFabricAddress => "OPTION_245",
};

let interfaces = pnet_datalink::interfaces();
trace!("interfaces - {:?}", interfaces);

for interface in interfaces {
trace!("looking at interface {:?}", interface);
let lease_path = format!("/run/systemd/netif/leases/{}", interface.index);
let lease_path = Path::new(&lease_path);
if lease_path.exists() {
debug!("found lease file - {:?}", lease_path);
let lease = File::open(lease_path)
.with_context(|| format!("failed to open lease file ({:?})", lease_path))?;

if let Some(v) = key_lookup('=', key, lease)? {
return Ok(v);
}

debug!(
"failed to get value from existing lease file '{:?}'",
lease_path
);
}
}
Err(anyhow!("failed to acquire DHCP option {key}"))
}
}

#[dbus_proxy(
default_service = "org.freedesktop.NetworkManager",
default_path = "/org/freedesktop/NetworkManager",
interface = "org.freedesktop.NetworkManager"
)]
trait NetworkManager {
#[dbus_proxy(property)]
fn active_connections(&self) -> zbus::Result<Vec<zvariant::ObjectPath>>;
}

#[dbus_proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager.Connection.Active"
)]
trait NMActiveConnection {
#[dbus_proxy(property)]
fn dhcp4_config(&self) -> zbus::Result<zvariant::ObjectPath>;
}

#[dbus_proxy(
default_service = "org.freedesktop.NetworkManager",
interface = "org.freedesktop.NetworkManager.DHCP4Config"
)]
trait NMDhcp4Config {
#[dbus_proxy(property)]
fn options(&self) -> Result<HashMap<String, zvariant::Value>>;
}
42 changes: 4 additions & 38 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,15 @@

//! utility functions

use crate::retry;
use anyhow::{anyhow, Context, Result};
use slog_scope::{debug, trace};
use std::fs::File;
use anyhow::Result;
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use std::time::Duration;

mod cmdline;
pub use self::cmdline::{get_platform, has_network_kargs};

mod dhcp;
pub use self::dhcp::DhcpOption;

mod mount;
pub(crate) use mount::{mount_ro, unmount};

Expand Down Expand Up @@ -54,38 +52,6 @@ pub fn key_lookup<R: Read>(delim: char, key: &str, reader: R) -> Result<Option<S
Ok(None)
}

pub fn dns_lease_key_lookup(key: &str) -> Result<String> {
let interfaces = pnet_datalink::interfaces();
trace!("interfaces - {:?}", interfaces);

retry::Retry::new()
.initial_backoff(Duration::from_millis(50))
.max_backoff(Duration::from_millis(500))
.max_retries(60)
.retry(|_| {
for interface in interfaces.clone() {
trace!("looking at interface {:?}", interface);
let lease_path = format!("/run/systemd/netif/leases/{}", interface.index);
let lease_path = Path::new(&lease_path);
if lease_path.exists() {
debug!("found lease file - {:?}", lease_path);
let lease = File::open(lease_path)
.with_context(|| format!("failed to open lease file ({:?})", lease_path))?;

if let Some(v) = key_lookup('=', key, lease)? {
return Ok(v);
}

debug!(
"failed to get value from existing lease file '{:?}'",
lease_path
);
}
}
Err(anyhow!("failed to retrieve fabric address"))
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down