diff --git a/src/asynch/dhcp.rs b/src/asynch/dhcp.rs index 8b5b5c8..61e59a0 100644 --- a/src/asynch/dhcp.rs +++ b/src/asynch/dhcp.rs @@ -248,8 +248,17 @@ pub mod client { } } + /// A simple asynchronous DHCP client. + /// + /// The client takes a socket factory (either operating on raw sockets or UDP datagrams) and + /// then takes care of the all the negotiations with the DHCP server, as in discovering servers, + /// negotiating initial IP, and then keeping the lease of that IP up to date. + /// + /// Note that it is unlikely that a non-raw socket factory would actually even work, due to the peculiarities of the + /// DHCP protocol, where a lot of UDP packets are send (and often broasdcasted_) by the client before the client actually has an assigned IP. pub struct Client { - client: dhcp::client::Client, + rng: T, + mac: [u8; 6], timeout: Duration, settings: Option<(Settings, Instant)>, } @@ -262,12 +271,30 @@ pub mod client { info!("Creating DHCP client with configuration {conf:?}"); Self { - client: dhcp::client::Client { rng, mac: conf.mac }, + rng, + mac: conf.mac, timeout: conf.timeout, settings: None, } } + /// Runs the DHCP client with the supplied socket factory, and takes care of + /// all aspects of negotiating an IP with the first DHCP server that replies to the discovery requests. + /// + /// From the POV of the user, this method will return only in two cases, which are exactly the cases where the user is expected to take an action: + /// - When an initial/new IP lease was negotiated; in that case, `Some(Settings)` is returned, and the user should assign the returned IP settings + /// to the network interface using platform-specific means + /// - When the IP lease was lost; in that case, `None` is returned, and the user should de-assign all IP settings from the network interface using + /// platform-specific means + /// + /// In both cases, user is expected to call `run` again, so that the IP lease is kept up to date / a new lease is re-negotiated + /// + /// Note that dropping this future is also safe in that it won't remove the current lease, so the user can renew + /// the operation of the server by just calling `run` later on. Of course, if the future is not polled, the client + /// would be unable - during that time - to check for lease timeout and the lease might not be renewed on time. + /// + /// But in any case, if the lease is expired or the DHCP server does not acknowledge the lease renewal, the client will + /// automatically restart the DHCP servers' discovery from the very beginning. pub async fn run( &mut self, mut f: F, @@ -314,18 +341,20 @@ pub mod client { } } + /// This method allows the user to inform the DHCP server that the currently leased IP (if any) is no longer used + /// by the client. + /// + /// Useful when the program runnuing the DHCP client is about to exit. pub async fn release( &mut self, f: F, buf: &mut [u8], ) -> Result<(), Error> { - if let Some((settings, _)) = self.settings.as_ref() { + if let Some((settings, _)) = self.settings.as_ref().cloned() { let mut socket = f.connect().await.map_err(Error::Io)?; - let packet = self.client.release( + let packet = self.client(&f).encode_release( buf, - f.raw_ports().0, - f.raw_ports().1, 0, settings.server_ip.unwrap(), settings.ip, @@ -346,37 +375,31 @@ pub mod client { ) -> Result> { info!("Discovering DHCP servers..."); + let timeout = self.timeout; + let mut client = self.client(&f); + let start = Instant::now(); loop { let mut socket = f.connect().await.map_err(Error::Io)?; - let (packet, xid) = self.client.discover( - buf, - f.raw_ports().0, - f.raw_ports().1, - (Instant::now() - start).as_secs() as _, - None, - )?; + let (packet, xid) = + client.encode_discover(buf, (Instant::now() - start).as_secs() as _, None)?; socket.send(packet).await.map_err(Error::Io)?; let offer_start = Instant::now(); - while Instant::now() - offer_start < self.timeout { + while Instant::now() - offer_start < timeout { let timer = Timer::after(Duration::from_secs(3)); if let Either::First(result) = select(socket.recv(buf), timer).await { let len = result.map_err(Error::Io)?; let packet = &buf[..len]; - if let Some(reply) = self.client.recv( - packet, - f.raw_ports().0, - f.raw_ports().1, - xid, - Some(&[MessageType::Offer]), - )? { + if let Some(reply) = + client.decode_bootp_reply(packet, xid, Some(&[MessageType::Offer]))? + { let settings = reply.settings().unwrap().1; info!( @@ -404,6 +427,9 @@ pub mod client { server_ip: Ipv4Addr, ip: Ipv4Addr, ) -> Result, Error> { + let timeout = self.timeout; + let mut client = self.client(&f); + for _ in 0..3 { info!("Requesting IP {ip} from DHCP server {server_ip}"); @@ -411,10 +437,8 @@ pub mod client { let start = Instant::now(); - let (packet, xid) = self.client.request( + let (packet, xid) = client.encode_request( buf, - f.raw_ports().0, - f.raw_ports().1, (Instant::now() - start).as_secs() as _, server_ip, ip, @@ -424,17 +448,15 @@ pub mod client { let request_start = Instant::now(); - while Instant::now() - request_start < self.timeout { + while Instant::now() - request_start < timeout { let timer = Timer::after(Duration::from_secs(10)); if let Either::First(result) = select(socket.recv(buf), timer).await { let len = result.map_err(Error::Io)?; let packet = &buf[..len]; - if let Some(reply) = self.client.recv( + if let Some(reply) = client.decode_bootp_reply( packet, - f.raw_ports().0, - f.raw_ports().1, xid, Some(&[MessageType::Ack, MessageType::Nak]), )? { @@ -460,6 +482,15 @@ pub mod client { Ok(None) } + + fn client(&mut self, f: F) -> dhcp::client::Client<&mut T> { + dhcp::client::Client { + rng: &mut self.rng, + mac: self.mac, + rp_udp_client_port: f.raw_ports().0, + rp_udp_server_port: f.raw_ports().1, + } + } } } @@ -484,8 +515,12 @@ pub mod server { pub lease_duration_secs: u32, } + /// A simple asynchronous DHCP server. + /// + /// The client takes a socket factory (either operating on raw sockets or UDP datagrams) and + /// then processes all incoming BOOTP requests, by updating its internal simple database of leases, and issuing replies. pub struct Server { - server: dhcp::server::Server, + pub server: dhcp::server::Server, } impl Server { @@ -504,6 +539,10 @@ pub mod server { } } + /// Runs the DHCP server wth the supplied socket factory, processing incoming DHCP requests. + /// + /// Note that dropping this future is safe in that it won't remove the internal leases' database, + /// so users are free to drop the future in case they would like to take a snapshot of the leases or inspect them otherwise. pub async fn run( &mut self, f: F, @@ -514,7 +553,10 @@ pub mod server { loop { let len = socket.recv(buf).await.map_err(Error::Io)?; - if let Some(reply) = self.server.handle(f.raw_ports().1, buf, len)? { + if let Some(reply) = self + .server + .handle_bootp_request(f.raw_ports().1, buf, len)? + { socket.send(reply).await.map_err(Error::Io)?; } } diff --git a/src/dhcp.rs b/src/dhcp.rs index 0183b8b..3a64b9d 100644 --- a/src/dhcp.rs +++ b/src/dhcp.rs @@ -802,51 +802,50 @@ pub mod client { use super::*; + /// A simple DHCP client. + /// The client is unaware of the IP/UDP transport layer and operates purely in terms of packets + /// represented as Rust slices. + /// + /// As such, the client can generate all BOOTP requests and parse BOOTP replies. + /// + /// The client supports both raw IP as well as regular UDP payloads, where the raw payloads are + /// automatically prefixed/unprefixed with the IP and UDP header, which allows this client to be used with a raw sockets' transport layer. + /// + /// Note that it is unlikely that a non-raw socket transport would actually even work, due to the peculiarities of the + /// DHCP protocol, where a lot of UDP packets are send (and often broasdcasted_) by the client before the client actually has an assigned IP. pub struct Client { pub rng: T, pub mac: [u8; 6], + pub rp_udp_client_port: Option, + pub rp_udp_server_port: Option, } impl Client where T: RngCore, { - pub fn discover<'o>( + pub fn encode_discover<'o>( &mut self, buf: &'o mut [u8], - rp_udp_client_port: Option, - rp_udp_server_port: Option, secs: u16, ip: Option, ) -> Result<(&'o [u8], u32), Error> { let mut opt_buf = Options::buf(); - self.send( - buf, - rp_udp_client_port, - rp_udp_server_port, - secs, - None, - None, - Options::discover(ip, &mut opt_buf), - ) + self.encode_bootp_request(buf, secs, None, None, Options::discover(ip, &mut opt_buf)) } - pub fn request<'o>( + pub fn encode_request<'o>( &mut self, buf: &'o mut [u8], - rp_udp_client_port: Option, - rp_udp_server_port: Option, secs: u16, server_ip: Ipv4Addr, our_ip: Ipv4Addr, ) -> Result<(&'o [u8], u32), Error> { let mut opt_buf = Options::buf(); - self.send( + self.encode_bootp_request( buf, - rp_udp_client_port, - rp_udp_server_port, secs, Some(server_ip), None, @@ -854,21 +853,17 @@ pub mod client { ) } - pub fn release<'o>( + pub fn encode_release<'o>( &mut self, buf: &'o mut [u8], - rp_udp_client_port: Option, - rp_udp_server_port: Option, secs: u16, server_ip: Ipv4Addr, our_ip: Ipv4Addr, ) -> Result<&'o [u8], Error> { let mut opt_buf = Options::buf(); - self.send( + self.encode_bootp_request( buf, - rp_udp_client_port, - rp_udp_server_port, secs, Some(server_ip), Some(our_ip), @@ -877,21 +872,17 @@ pub mod client { .map(|r| r.0) } - pub fn decline<'o>( + pub fn encode_decline<'o>( &mut self, buf: &'o mut [u8], - rp_udp_client_port: Option, - rp_udp_server_port: Option, secs: u16, server_ip: Ipv4Addr, our_ip: Ipv4Addr, ) -> Result<&'o [u8], Error> { let mut opt_buf = Options::buf(); - self.send( + self.encode_bootp_request( buf, - rp_udp_client_port, - rp_udp_server_port, secs, Some(server_ip), Some(our_ip), @@ -901,11 +892,9 @@ pub mod client { } #[allow(clippy::too_many_arguments)] - pub fn send<'o>( + pub fn encode_bootp_request<'o>( &mut self, buf: &'o mut [u8], - rp_udp_client_port: Option, - rp_udp_server_port: Option, secs: u16, server_ip: Option, our_ip: Option, @@ -915,12 +904,12 @@ pub mod client { let request = Packet::new_request(self.mac, xid, secs, our_ip, options.clone()); - let data = if rp_udp_server_port.is_some() || rp_udp_client_port.is_some() { + let data = if self.rp_udp_server_port.is_some() || self.rp_udp_client_port.is_some() { request.encode_raw( our_ip, - rp_udp_client_port.unwrap_or(68), + self.rp_udp_client_port.unwrap_or(68), server_ip, - rp_udp_server_port.unwrap_or(67), + self.rp_udp_server_port.unwrap_or(67), buf, )? } else { @@ -930,16 +919,15 @@ pub mod client { Ok((data, xid)) } - pub fn recv<'o>( + pub fn decode_bootp_reply<'o>( &self, data: &'o [u8], - rp_udp_client_port: Option, - rp_udp_server_port: Option, xid: u32, expected_message_types: Option<&[MessageType]>, ) -> Result>, Error> { - let reply = if rp_udp_server_port.is_some() || rp_udp_client_port.is_some() { - Packet::decode_raw(data, rp_udp_server_port, rp_udp_client_port)?.map(|r| r.2) + let reply = if self.rp_udp_server_port.is_some() || self.rp_udp_client_port.is_some() { + Packet::decode_raw(data, self.rp_udp_server_port, self.rp_udp_client_port)? + .map(|r| r.2) } else { Some(Packet::decode(data)?) }; @@ -978,6 +966,12 @@ pub mod server { expires: Instant, } + /// A simple DHCP server. + /// The server is unaware of the IP/UDP transport layer and operates purely in terms of packets + /// represented as Rust slices. + /// + /// The server supports both raw IP as well as regular UDP payloads, where the raw payloads are + /// automatically prefixed/unprefixed with the IP and UDP header, which allows this server to be used with a raw sockets' transport layer. #[derive(Clone, Debug)] pub struct Server { pub ip: Ipv4Addr, @@ -991,7 +985,7 @@ pub mod server { } impl Server { - pub fn handle<'o>( + pub fn handle_bootp_request<'o>( &mut self, rp_udp_server_port: Option, buf: &'o mut [u8],