Skip to content

Commit 280302a

Browse files
committed
Support for captive URLs
1 parent 6b85743 commit 280302a

File tree

3 files changed

+97
-40
lines changed

3 files changed

+97
-40
lines changed

edge-dhcp/src/io/client.rs

+67-37
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@ use crate::{Options, Packet};
1616

1717
/// Represents the additional network-related information that might be returned by the DHCP server.
1818
#[derive(Debug, Clone)]
19-
pub struct NetworkInfo {
19+
#[non_exhaustive]
20+
pub struct NetworkInfo<'a> {
2021
pub gateway: Option<Ipv4Addr>,
2122
pub subnet: Option<Ipv4Addr>,
2223
pub dns1: Option<Ipv4Addr>,
2324
pub dns2: Option<Ipv4Addr>,
25+
pub captive_url: Option<&'a str>,
2426
}
2527

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

5560
let now = Instant::now();
5661

57-
if let Some(settings) = Self::request(
58-
client,
59-
socket,
60-
buf,
61-
offer.server_ip.unwrap(),
62-
offer.ip,
63-
true,
64-
Duration::from_secs(3),
65-
3,
66-
)
67-
.await?
6862
{
69-
break Ok((
70-
Self {
71-
ip: settings.ip,
72-
server_ip: settings.server_ip.unwrap(),
73-
duration: Duration::from_secs(settings.lease_time_secs.unwrap_or(7200) as _),
74-
acquired: now,
75-
},
76-
NetworkInfo {
77-
gateway: settings.gateway,
78-
subnet: settings.subnet,
79-
dns1: settings.dns1,
80-
dns2: settings.dns2,
81-
},
82-
));
63+
// Nasty but necessary to avoid Rust's borrow checker not dealing
64+
// with the non-lexical lifetimes involved here
65+
let buf = unsafe { Self::unsafe_reborrow(buf) };
66+
67+
if let Some(settings) = Self::request(
68+
client,
69+
socket,
70+
buf,
71+
server_ip,
72+
ip,
73+
true,
74+
Duration::from_secs(3),
75+
3,
76+
)
77+
.await?
78+
{
79+
break Ok((
80+
Self {
81+
ip: settings.ip,
82+
server_ip: settings.server_ip.unwrap(),
83+
duration: Duration::from_secs(
84+
settings.lease_time_secs.unwrap_or(7200) as _
85+
),
86+
acquired: now,
87+
},
88+
NetworkInfo {
89+
gateway: settings.gateway,
90+
subnet: settings.subnet,
91+
dns1: settings.dns1,
92+
dns2: settings.dns2,
93+
captive_url: settings.captive_url,
94+
},
95+
));
96+
}
8397
}
8498
}
8599
}
@@ -173,12 +187,12 @@ impl Lease {
173187
Ok(())
174188
}
175189

176-
async fn discover<T, S>(
190+
async fn discover<'a, T, S>(
177191
client: &mut dhcp::client::Client<T>,
178192
socket: &mut S,
179-
buf: &mut [u8],
193+
buf: &'a mut [u8],
180194
timeout: Duration,
181-
) -> Result<Settings, Error<S::Error>>
195+
) -> Result<Settings<'a>, Error<S::Error>>
182196
where
183197
T: RngCore,
184198
S: UdpReceive + UdpSend,
@@ -203,11 +217,15 @@ impl Lease {
203217

204218
if let Either::First(result) = select(socket.receive(buf), Timer::after(timeout)).await
205219
{
220+
// Nasty but necessary to avoid Rust's borrow checker not dealing
221+
// with the non-lexical lifetimes involved here
222+
let buf = unsafe { Self::unsafe_reborrow(buf) };
223+
206224
let (len, _remote) = result.map_err(Error::Io)?;
207225
let reply = Packet::decode(&buf[..len])?;
208226

209227
if client.is_offer(&reply, xid) {
210-
let settings: Settings = (&reply).into();
228+
let settings = Settings::new(&reply);
211229

212230
info!(
213231
"IP {} offered by DHCP server {}",
@@ -224,16 +242,16 @@ impl Lease {
224242
}
225243

226244
#[allow(clippy::too_many_arguments)]
227-
async fn request<T, S>(
245+
async fn request<'a, T, S>(
228246
client: &mut dhcp::client::Client<T>,
229247
socket: &mut S,
230-
buf: &mut [u8],
248+
buf: &'a mut [u8],
231249
server_ip: Ipv4Addr,
232250
ip: Ipv4Addr,
233251
broadcast: bool,
234252
timeout: Duration,
235253
retries: usize,
236-
) -> Result<Option<Settings>, Error<S::Error>>
254+
) -> Result<Option<Settings<'a>>, Error<S::Error>>
237255
where
238256
T: RngCore,
239257
S: UdpReceive + UdpSend,
@@ -270,12 +288,17 @@ impl Lease {
270288
if let Either::First(result) = select(socket.receive(buf), Timer::after(timeout)).await
271289
{
272290
let (len, _remote) = result.map_err(Error::Io)?;
291+
292+
// Nasty but necessary to avoid Rust's borrow checker not dealing
293+
// with the non-lexical lifetimes involved here
294+
let buf = unsafe { Self::unsafe_reborrow(buf) };
295+
273296
let packet = &buf[..len];
274297

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

277300
if client.is_ack(&reply, xid) {
278-
let settings = (&reply).into();
301+
let settings = Settings::new(&reply);
279302

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

@@ -292,4 +315,11 @@ impl Lease {
292315

293316
Ok(None)
294317
}
318+
319+
// Useful when Rust's borrow-checker still cannot handle some NLLs
320+
// https://rust-lang.github.io/rfcs/2094-nll.html
321+
unsafe fn unsafe_reborrow<'a>(buf: &mut [u8]) -> &'a mut [u8] {
322+
let len = buf.len();
323+
unsafe { core::slice::from_raw_parts_mut(buf.as_mut_ptr(), len) }
324+
}
295325
}

edge-dhcp/src/lib.rs

+26-3
Original file line numberDiff line numberDiff line change
@@ -287,18 +287,20 @@ impl<'a> Packet<'a> {
287287
}
288288

289289
#[derive(Clone, Debug)]
290-
pub struct Settings {
290+
#[non_exhaustive]
291+
pub struct Settings<'a> {
291292
pub ip: Ipv4Addr,
292293
pub server_ip: Option<Ipv4Addr>,
293294
pub lease_time_secs: Option<u32>,
294295
pub gateway: Option<Ipv4Addr>,
295296
pub subnet: Option<Ipv4Addr>,
296297
pub dns1: Option<Ipv4Addr>,
297298
pub dns2: Option<Ipv4Addr>,
299+
pub captive_url: Option<&'a str>,
298300
}
299301

300-
impl From<&Packet<'_>> for Settings {
301-
fn from(packet: &Packet) -> Self {
302+
impl<'a> Settings<'a> {
303+
pub fn new(packet: &Packet<'a>) -> Self {
302304
Self {
303305
ip: packet.yiaddr,
304306
server_ip: packet.options.iter().find_map(|option| {
@@ -343,6 +345,13 @@ impl From<&Packet<'_>> for Settings {
343345
None
344346
}
345347
}),
348+
captive_url: packet.options.iter().find_map(|option| {
349+
if let DhcpOption::CaptiveUrl(url) = option {
350+
Some(url)
351+
} else {
352+
None
353+
}
354+
}),
346355
}
347356
}
348357
}
@@ -408,6 +417,7 @@ impl<'a> Options<'a> {
408417
gateways: &'b [Ipv4Addr],
409418
subnet: Option<Ipv4Addr>,
410419
dns: &'b [Ipv4Addr],
420+
captive_url: Option<&'b str>,
411421
buf: &'b mut [DhcpOption<'b>],
412422
) -> Options<'b> {
413423
let requested = self.iter().find_map(|option| {
@@ -426,6 +436,7 @@ impl<'a> Options<'a> {
426436
gateways,
427437
subnet,
428438
dns,
439+
captive_url,
429440
buf,
430441
)
431442
}
@@ -439,6 +450,7 @@ impl<'a> Options<'a> {
439450
gateways: &'a [Ipv4Addr],
440451
subnet: Option<Ipv4Addr>,
441452
dns: &'a [Ipv4Addr],
453+
captive_url: Option<&'a str>,
442454
buf: &'a mut [DhcpOption<'a>],
443455
) -> Self {
444456
buf[0] = DhcpOption::MessageType(mt);
@@ -457,6 +469,7 @@ impl<'a> Options<'a> {
457469
DhcpOption::CODE_DNS => (!dns.is_empty())
458470
.then_some(DhcpOption::DomainNameServer(Ipv4Addrs::new(dns))),
459471
DhcpOption::CODE_SUBNET => subnet.map(DhcpOption::SubnetMask),
472+
DhcpOption::CODE_CAPTIVE_URL => captive_url.map(DhcpOption::CaptiveUrl),
460473
_ => None,
461474
};
462475

@@ -570,13 +583,17 @@ pub enum DhcpOption<'a> {
570583
MaximumMessageSize(u16),
571584
/// 61: Client-identifier
572585
ClientIdentifier(&'a [u8]),
586+
/// 114: Captive-portal URL
587+
CaptiveUrl(&'a str),
588+
// Other (unrecognized)
573589
Unrecognized(u8, &'a [u8]),
574590
}
575591

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

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

625642
DhcpOption::ClientIdentifier(bytes.remaining())
626643
}
644+
CAPTIVE_URL => DhcpOption::HostName(
645+
core::str::from_utf8(bytes.remaining()).map_err(Error::InvalidUtf8Str)?,
646+
),
627647
_ => DhcpOption::Unrecognized(code, bytes.remaining()),
628648
};
629649

@@ -656,6 +676,7 @@ impl DhcpOption<'_> {
656676
Self::MaximumMessageSize(_) => MAXIMUM_DHCP_MESSAGE_SIZE,
657677
Self::Message(_) => MESSAGE,
658678
Self::ClientIdentifier(_) => CLIENT_IDENTIFIER,
679+
Self::CaptiveUrl(_) => CAPTIVE_URL,
659680
Self::Unrecognized(code, _) => *code,
660681
}
661682
}
@@ -679,6 +700,7 @@ impl DhcpOption<'_> {
679700
Self::Message(msg) => f(msg.as_bytes()),
680701
Self::MaximumMessageSize(size) => f(&size.to_be_bytes()),
681702
Self::ClientIdentifier(id) => f(id),
703+
Self::CaptiveUrl(name) => f(name.as_bytes()),
682704
Self::Unrecognized(_, data) => f(data),
683705
}
684706
}
@@ -753,3 +775,4 @@ const PARAMETER_REQUEST_LIST: u8 = 55;
753775
const MESSAGE: u8 = 56;
754776
const MAXIMUM_DHCP_MESSAGE_SIZE: u8 = 57;
755777
const CLIENT_IDENTIFIER: u8 = 61;
778+
const CAPTIVE_URL: u8 = 114;

edge-dhcp/src/server.rs

+4
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ pub enum Action<'a> {
2121
}
2222

2323
#[derive(Clone, Debug)]
24+
#[non_exhaustive]
2425
pub struct ServerOptions<'a> {
2526
pub ip: Ipv4Addr,
2627
pub gateways: &'a [Ipv4Addr],
2728
pub subnet: Option<Ipv4Addr>,
2829
pub dns: &'a [Ipv4Addr],
30+
pub captive_url: Option<&'a str>,
2931
pub lease_duration_secs: u32,
3032
}
3133

@@ -43,6 +45,7 @@ impl<'a> ServerOptions<'a> {
4345
gateways,
4446
subnet: Some(Ipv4Addr::new(255, 255, 255, 0)),
4547
dns: &[],
48+
captive_url: None,
4649
lease_duration_secs: 7200,
4750
}
4851
}
@@ -150,6 +153,7 @@ impl<'a> ServerOptions<'a> {
150153
self.gateways,
151154
self.subnet,
152155
self.dns,
156+
self.captive_url,
153157
buf,
154158
),
155159
);

0 commit comments

Comments
 (0)