From 6a0451b23eb9c70b55987539afaa4a4dfd8973e0 Mon Sep 17 00:00:00 2001 From: ivmarkov Date: Sun, 6 Oct 2024 10:27:27 +0300 Subject: [PATCH] Support for captive URLs (#31) --- edge-dhcp/src/io/client.rs | 104 ++++++++++++++++++++++++------------- edge-dhcp/src/lib.rs | 29 +++++++++-- edge-dhcp/src/server.rs | 4 ++ 3 files changed, 97 insertions(+), 40 deletions(-) diff --git a/edge-dhcp/src/io/client.rs b/edge-dhcp/src/io/client.rs index fc8e30f..a03c6bd 100644 --- a/edge-dhcp/src/io/client.rs +++ b/edge-dhcp/src/io/client.rs @@ -16,11 +16,13 @@ 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, pub subnet: Option, pub dns1: Option, pub dns2: Option, + pub captive_url: Option<&'a str>, } /// Represents a DHCP IP lease. @@ -28,6 +30,7 @@ pub struct NetworkInfo { /// 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, @@ -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( + pub async fn new<'a, T, S>( client: &mut dhcp::client::Client, socket: &mut S, - buf: &mut [u8], - ) -> Result<(Self, NetworkInfo), Error> + buf: &'a mut [u8], + ) -> Result<(Self, NetworkInfo<'a>), 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, + }, + )); + } } } } @@ -173,12 +187,12 @@ impl Lease { Ok(()) } - async fn discover( + async fn discover<'a, T, S>( client: &mut dhcp::client::Client, socket: &mut S, - buf: &mut [u8], + buf: &'a mut [u8], timeout: Duration, - ) -> Result> + ) -> Result, Error> where T: RngCore, S: UdpReceive + UdpSend, @@ -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 {}", @@ -224,16 +242,16 @@ impl Lease { } #[allow(clippy::too_many_arguments)] - async fn request( + async fn request<'a, T, S>( client: &mut dhcp::client::Client, socket: &mut S, - buf: &mut [u8], + buf: &'a mut [u8], server_ip: Ipv4Addr, ip: Ipv4Addr, broadcast: bool, timeout: Duration, retries: usize, - ) -> Result, Error> + ) -> Result>, Error> where T: RngCore, S: UdpReceive + UdpSend, @@ -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); @@ -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) } + } } diff --git a/edge-dhcp/src/lib.rs b/edge-dhcp/src/lib.rs index 3154593..a98cb36 100644 --- a/edge-dhcp/src/lib.rs +++ b/edge-dhcp/src/lib.rs @@ -287,7 +287,8 @@ impl<'a> Packet<'a> { } #[derive(Clone, Debug)] -pub struct Settings { +#[non_exhaustive] +pub struct Settings<'a> { pub ip: Ipv4Addr, pub server_ip: Option, pub lease_time_secs: Option, @@ -295,10 +296,11 @@ pub struct Settings { pub subnet: Option, pub dns1: Option, pub dns2: Option, + 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| { @@ -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 + } + }), } } } @@ -408,6 +417,7 @@ impl<'a> Options<'a> { gateways: &'b [Ipv4Addr], subnet: Option, dns: &'b [Ipv4Addr], + captive_url: Option<&'b str>, buf: &'b mut [DhcpOption<'b>], ) -> Options<'b> { let requested = self.iter().find_map(|option| { @@ -426,6 +436,7 @@ impl<'a> Options<'a> { gateways, subnet, dns, + captive_url, buf, ) } @@ -439,6 +450,7 @@ impl<'a> Options<'a> { gateways: &'a [Ipv4Addr], subnet: Option, dns: &'a [Ipv4Addr], + captive_url: Option<&'a str>, buf: &'a mut [DhcpOption<'a>], ) -> Self { buf[0] = DhcpOption::MessageType(mt); @@ -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, }; @@ -570,6 +583,9 @@ 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]), } @@ -577,6 +593,7 @@ 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>, Error> { let code = bytes.byte()?; @@ -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()), }; @@ -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, } } @@ -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), } } @@ -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; diff --git a/edge-dhcp/src/server.rs b/edge-dhcp/src/server.rs index f77afe8..07c8fb7 100644 --- a/edge-dhcp/src/server.rs +++ b/edge-dhcp/src/server.rs @@ -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, pub dns: &'a [Ipv4Addr], + pub captive_url: Option<&'a str>, pub lease_duration_secs: u32, } @@ -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, } } @@ -150,6 +153,7 @@ impl<'a> ServerOptions<'a> { self.gateways, self.subnet, self.dns, + self.captive_url, buf, ), );