Skip to content

Commit

Permalink
Support for captive URLs (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivmarkov authored Oct 6, 2024
1 parent 1ba77ef commit 6a0451b
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 40 deletions.
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

0 comments on commit 6a0451b

Please sign in to comment.