Skip to content
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
104 changes: 67 additions & 37 deletions edge-dhcp/src/io/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ use crate::{Options, Packet};

/// Represents the additional network-related information that might be returned by the DHCP server.
#[derive(Debug, Clone)]
pub struct NetworkInfo {
#[non_exhaustive]
pub struct NetworkInfo<'a> {
pub gateway: Option<Ipv4Addr>,
pub subnet: Option<Ipv4Addr>,
pub dns1: Option<Ipv4Addr>,
pub dns2: Option<Ipv4Addr>,
pub captive_url: Option<&'a str>,
}

/// Represents a DHCP IP lease.
///
/// This structure has a set of asynchronous methods that can utilize a supplied DHCP client instance and UDP socket to
/// transparently implement all aspects of negotiating an IP with the DHCP server and then keeping the lease of that IP up to date.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Lease {
pub ip: Ipv4Addr,
pub server_ip: Ipv4Addr,
Expand All @@ -40,46 +43,57 @@ impl Lease {
/// This is done by utilizing the supplied DHCP client instance and UDP socket.
///
/// Note that the supplied UDP socket should be capable of sending and receiving broadcast UDP packets.
pub async fn new<T, S>(
pub async fn new<'a, T, S>(
client: &mut dhcp::client::Client<T>,
socket: &mut S,
buf: &mut [u8],
) -> Result<(Self, NetworkInfo), Error<S::Error>>
buf: &'a mut [u8],
) -> Result<(Self, NetworkInfo<'a>), Error<S::Error>>
where
T: RngCore,
S: UdpReceive + UdpSend,
{
loop {
let offer = Self::discover(client, socket, buf, Duration::from_secs(3)).await?;
let server_ip = offer.server_ip.unwrap();
let ip = offer.ip;

let now = Instant::now();

if let Some(settings) = Self::request(
client,
socket,
buf,
offer.server_ip.unwrap(),
offer.ip,
true,
Duration::from_secs(3),
3,
)
.await?
{
break Ok((
Self {
ip: settings.ip,
server_ip: settings.server_ip.unwrap(),
duration: Duration::from_secs(settings.lease_time_secs.unwrap_or(7200) as _),
acquired: now,
},
NetworkInfo {
gateway: settings.gateway,
subnet: settings.subnet,
dns1: settings.dns1,
dns2: settings.dns2,
},
));
// Nasty but necessary to avoid Rust's borrow checker not dealing
// with the non-lexical lifetimes involved here
let buf = unsafe { Self::unsafe_reborrow(buf) };

if let Some(settings) = Self::request(
client,
socket,
buf,
server_ip,
ip,
true,
Duration::from_secs(3),
3,
)
.await?
{
break Ok((
Self {
ip: settings.ip,
server_ip: settings.server_ip.unwrap(),
duration: Duration::from_secs(
settings.lease_time_secs.unwrap_or(7200) as _
),
acquired: now,
},
NetworkInfo {
gateway: settings.gateway,
subnet: settings.subnet,
dns1: settings.dns1,
dns2: settings.dns2,
captive_url: settings.captive_url,
},
));
}
}
}
}
Expand Down Expand Up @@ -173,12 +187,12 @@ impl Lease {
Ok(())
}

async fn discover<T, S>(
async fn discover<'a, T, S>(
client: &mut dhcp::client::Client<T>,
socket: &mut S,
buf: &mut [u8],
buf: &'a mut [u8],
timeout: Duration,
) -> Result<Settings, Error<S::Error>>
) -> Result<Settings<'a>, Error<S::Error>>
where
T: RngCore,
S: UdpReceive + UdpSend,
Expand All @@ -203,11 +217,15 @@ impl Lease {

if let Either::First(result) = select(socket.receive(buf), Timer::after(timeout)).await
{
// Nasty but necessary to avoid Rust's borrow checker not dealing
// with the non-lexical lifetimes involved here
let buf = unsafe { Self::unsafe_reborrow(buf) };

let (len, _remote) = result.map_err(Error::Io)?;
let reply = Packet::decode(&buf[..len])?;

if client.is_offer(&reply, xid) {
let settings: Settings = (&reply).into();
let settings = Settings::new(&reply);

info!(
"IP {} offered by DHCP server {}",
Expand All @@ -224,16 +242,16 @@ impl Lease {
}

#[allow(clippy::too_many_arguments)]
async fn request<T, S>(
async fn request<'a, T, S>(
client: &mut dhcp::client::Client<T>,
socket: &mut S,
buf: &mut [u8],
buf: &'a mut [u8],
server_ip: Ipv4Addr,
ip: Ipv4Addr,
broadcast: bool,
timeout: Duration,
retries: usize,
) -> Result<Option<Settings>, Error<S::Error>>
) -> Result<Option<Settings<'a>>, Error<S::Error>>
where
T: RngCore,
S: UdpReceive + UdpSend,
Expand Down Expand Up @@ -270,12 +288,17 @@ impl Lease {
if let Either::First(result) = select(socket.receive(buf), Timer::after(timeout)).await
{
let (len, _remote) = result.map_err(Error::Io)?;

// Nasty but necessary to avoid Rust's borrow checker not dealing
// with the non-lexical lifetimes involved here
let buf = unsafe { Self::unsafe_reborrow(buf) };

let packet = &buf[..len];

let reply = Packet::decode(packet)?;

if client.is_ack(&reply, xid) {
let settings = (&reply).into();
let settings = Settings::new(&reply);

info!("IP {} leased successfully", ip);

Expand All @@ -292,4 +315,11 @@ impl Lease {

Ok(None)
}

// Useful when Rust's borrow-checker still cannot handle some NLLs
// https://rust-lang.github.io/rfcs/2094-nll.html
unsafe fn unsafe_reborrow<'a>(buf: &mut [u8]) -> &'a mut [u8] {
let len = buf.len();
unsafe { core::slice::from_raw_parts_mut(buf.as_mut_ptr(), len) }
}
}
29 changes: 26 additions & 3 deletions edge-dhcp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -287,18 +287,20 @@ impl<'a> Packet<'a> {
}

#[derive(Clone, Debug)]
pub struct Settings {
#[non_exhaustive]
pub struct Settings<'a> {
pub ip: Ipv4Addr,
pub server_ip: Option<Ipv4Addr>,
pub lease_time_secs: Option<u32>,
pub gateway: Option<Ipv4Addr>,
pub subnet: Option<Ipv4Addr>,
pub dns1: Option<Ipv4Addr>,
pub dns2: Option<Ipv4Addr>,
pub captive_url: Option<&'a str>,
}

impl From<&Packet<'_>> for Settings {
fn from(packet: &Packet) -> Self {
impl<'a> Settings<'a> {
pub fn new(packet: &Packet<'a>) -> Self {
Self {
ip: packet.yiaddr,
server_ip: packet.options.iter().find_map(|option| {
Expand Down Expand Up @@ -343,6 +345,13 @@ impl From<&Packet<'_>> for Settings {
None
}
}),
captive_url: packet.options.iter().find_map(|option| {
if let DhcpOption::CaptiveUrl(url) = option {
Some(url)
} else {
None
}
}),
}
}
}
Expand Down Expand Up @@ -408,6 +417,7 @@ impl<'a> Options<'a> {
gateways: &'b [Ipv4Addr],
subnet: Option<Ipv4Addr>,
dns: &'b [Ipv4Addr],
captive_url: Option<&'b str>,
buf: &'b mut [DhcpOption<'b>],
) -> Options<'b> {
let requested = self.iter().find_map(|option| {
Expand All @@ -426,6 +436,7 @@ impl<'a> Options<'a> {
gateways,
subnet,
dns,
captive_url,
buf,
)
}
Expand All @@ -439,6 +450,7 @@ impl<'a> Options<'a> {
gateways: &'a [Ipv4Addr],
subnet: Option<Ipv4Addr>,
dns: &'a [Ipv4Addr],
captive_url: Option<&'a str>,
buf: &'a mut [DhcpOption<'a>],
) -> Self {
buf[0] = DhcpOption::MessageType(mt);
Expand All @@ -457,6 +469,7 @@ impl<'a> Options<'a> {
DhcpOption::CODE_DNS => (!dns.is_empty())
.then_some(DhcpOption::DomainNameServer(Ipv4Addrs::new(dns))),
DhcpOption::CODE_SUBNET => subnet.map(DhcpOption::SubnetMask),
DhcpOption::CODE_CAPTIVE_URL => captive_url.map(DhcpOption::CaptiveUrl),
_ => None,
};

Expand Down Expand Up @@ -570,13 +583,17 @@ pub enum DhcpOption<'a> {
MaximumMessageSize(u16),
/// 61: Client-identifier
ClientIdentifier(&'a [u8]),
/// 114: Captive-portal URL
CaptiveUrl(&'a str),
// Other (unrecognized)
Unrecognized(u8, &'a [u8]),
}

impl DhcpOption<'_> {
pub const CODE_ROUTER: u8 = DhcpOption::Router(Ipv4Addrs::new(&[])).code();
pub const CODE_DNS: u8 = DhcpOption::DomainNameServer(Ipv4Addrs::new(&[])).code();
pub const CODE_SUBNET: u8 = DhcpOption::SubnetMask(Ipv4Addr::new(0, 0, 0, 0)).code();
pub const CODE_CAPTIVE_URL: u8 = DhcpOption::CaptiveUrl("").code();

fn decode<'o>(bytes: &mut BytesIn<'o>) -> Result<Option<DhcpOption<'o>>, Error> {
let code = bytes.byte()?;
Expand Down Expand Up @@ -624,6 +641,9 @@ impl DhcpOption<'_> {

DhcpOption::ClientIdentifier(bytes.remaining())
}
CAPTIVE_URL => DhcpOption::HostName(
core::str::from_utf8(bytes.remaining()).map_err(Error::InvalidUtf8Str)?,
),
_ => DhcpOption::Unrecognized(code, bytes.remaining()),
};

Expand Down Expand Up @@ -656,6 +676,7 @@ impl DhcpOption<'_> {
Self::MaximumMessageSize(_) => MAXIMUM_DHCP_MESSAGE_SIZE,
Self::Message(_) => MESSAGE,
Self::ClientIdentifier(_) => CLIENT_IDENTIFIER,
Self::CaptiveUrl(_) => CAPTIVE_URL,
Self::Unrecognized(code, _) => *code,
}
}
Expand All @@ -679,6 +700,7 @@ impl DhcpOption<'_> {
Self::Message(msg) => f(msg.as_bytes()),
Self::MaximumMessageSize(size) => f(&size.to_be_bytes()),
Self::ClientIdentifier(id) => f(id),
Self::CaptiveUrl(name) => f(name.as_bytes()),
Self::Unrecognized(_, data) => f(data),
}
}
Expand Down Expand Up @@ -753,3 +775,4 @@ const PARAMETER_REQUEST_LIST: u8 = 55;
const MESSAGE: u8 = 56;
const MAXIMUM_DHCP_MESSAGE_SIZE: u8 = 57;
const CLIENT_IDENTIFIER: u8 = 61;
const CAPTIVE_URL: u8 = 114;
4 changes: 4 additions & 0 deletions edge-dhcp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ pub enum Action<'a> {
}

#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct ServerOptions<'a> {
pub ip: Ipv4Addr,
pub gateways: &'a [Ipv4Addr],
pub subnet: Option<Ipv4Addr>,
pub dns: &'a [Ipv4Addr],
pub captive_url: Option<&'a str>,
pub lease_duration_secs: u32,
}

Expand All @@ -43,6 +45,7 @@ impl<'a> ServerOptions<'a> {
gateways,
subnet: Some(Ipv4Addr::new(255, 255, 255, 0)),
dns: &[],
captive_url: None,
lease_duration_secs: 7200,
}
}
Expand Down Expand Up @@ -150,6 +153,7 @@ impl<'a> ServerOptions<'a> {
self.gateways,
self.subnet,
self.dns,
self.captive_url,
buf,
),
);
Expand Down
Loading