diff --git a/.cargo/config.toml b/.cargo/config.toml index ca6c72f..8ece6b8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,5 @@ [target.x86_64-unknown-linux-gnu] runner = 'sudo -E' + +[env] +RUST_TEST_THREADS = "1" diff --git a/Cargo.toml b/Cargo.toml index a3fe42a..6772920 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ path = "src/lib.rs" rand = { version = "0.8.5", default-features = false } libc = "0.2.132" byteorder = "1.4.3" -dhcproto = "0.9.0" +dhcproto = "0.12.0" log = "0.4.17" etherparse = "0.13.0" nix = { version = "0.29.0", features = ["poll", "time", "event"] } diff --git a/README.md b/README.md index 932174d..bb7a708 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,12 @@ TODO: * Handle vendor difference: https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/848 * Support multiple DHCP servers with `DHCPNAK` reply. * Support DHCPNAK + * Support `DHCPDECLINE`: Client to server indicating network address is + already in use. + * Support `DHCPINFORM`: Client to server, asking only for local configuration + parameters; client already has externally configured network address. + * Rate control -- Token bucket (RFC 2698) + * Initial sleep before discovery/solicit(need check RFC) # Try out @@ -24,5 +30,6 @@ TODO: # The `eth1.ep` is DHCP server interface running dnsmasq in `mozim` network # namespace. sudo ./utils/test_env_mozim & -cargo run --example mozim_async +cargo run --example mozim_dhcpv4_sync +cargo run --example mozim_dhcpv6_sync ``` diff --git a/doc/dhcpv6_notes.md b/doc/dhcpv6_notes.md new file mode 100644 index 0000000..6112223 --- /dev/null +++ b/doc/dhcpv6_notes.md @@ -0,0 +1,165 @@ +## RFC 8415 + +Clients and servers exchange DHCP messages using UDP (see [RFC768] +and BCP 145 [RFC8085]). The client uses a link-local address or +addresses determined through other mechanisms for transmitting and +receiving DHCP messages. + +`All_DHCP_Relay_Agents_and_Servers`: ff02::1:2 +`All_DHCP_Servers`: ff05::1:3 + +Clients listen for DHCP messages on UDP port 546. Servers and relay +agents listen for DHCP messages on UDP port 547. + + + + Server Server + (not selected) Client (selected) + + v v v + | | | + | Begins initialization | + | | | + start of | _____________/|\_____________ | + 4-message |/ Solicit | Solicit \| + exchange | | | + Determines | Determines + configuration | configuration + | | | + |\ | ____________/| + | \________ | /Advertise | + | Advertise\ |/ | + | \ | | + | Collects Advertises | + | \ | | + | Selects configuration | + | | | + | _____________/|\_____________ | + |/ Request | Request \| + | | | + | | Commits configuration + | | | + end of | | _____________/| + 4-message | |/ Reply | + exchange | | | + | Initialization complete | + | | | + . . . + . . . + | T1 (renewal) timer expires | + | | | + 2-message | _____________/|\_____________ | + exchange |/ Renew | Renew \| + | | | + | | Commits extended lease(s) + | | | + | | _____________/| + | |/ Reply | + . . . + . . . + | | | + | Graceful shutdown | + | | | + 2-message | _____________/|\_____________ | + exchange |/ Release | Release \| + | | | + | | Discards lease(s) + | | | + | | _____________/| + | |/ Reply | + | | | + v v v + + +The IAID uniquely identifies the IA and MUST be chosen to be unique +among the IAIDs for that IA type on the client (e.g., an IA_NA with +an IAID of 0 and an IA_PD with an IAID of 0 are each considered +unique). The IAID is chosen by the client. For any given use of an +IA by the client, the IAID for that IA MUST be consistent across +restarts of the DHCP client. + + + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | msg-type | transaction-id | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + . options . + . (variable number and length) . + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + Figure 2: Client/Server Message Format + + +16.2. Solicit Message + + Clients MUST discard any received Solicit messages. + + Servers MUST discard any Solicit messages that do not include a + Client Identifier option or that do include a Server Identifier + option. + +16.3. Advertise Message + + Clients MUST discard any received Advertise message that meets any of + the following conditions: + + - the message does not include a Server Identifier option (see + Section 21.3). + + - the message does not include a Client Identifier option (see + Section 21.2). + + - the contents of the Client Identifier option do not match the + client's DUID. + + - the "transaction-id" field value does not match the value the + client used in its Solicit message. + + Servers and relay agents MUST discard any received Advertise + messages. + + +16.4. Request Message + + Clients MUST discard any received Request messages. + + Servers MUST discard any received Request message that meets any of + the following conditions: + + - the message does not include a Server Identifier option (see + Section 21.3). + + - the contents of the Server Identifier option do not match the + server's DUID. + + - the message does not include a Client Identifier option (see + Section 21.2). + + +16.6. Renew Message + + Clients MUST discard any received Renew messages. + + Servers MUST discard any received Renew message that meets any of the + following conditions: + + - the message does not include a Server Identifier option (see + Section 21.3). + + - the contents of the Server Identifier option do not match the + server's identifier. + + - the message does not include a Client Identifier option (see + Section 21.2). + +16.7. Rebind Message + + Clients MUST discard any received Rebind messages. + + Servers MUST discard any received Rebind messages that do not include + a Client Identifier option (see Section 21.2) or that do include a + Server Identifier option (see Section 21.3). diff --git a/examples/mozim_async.rs b/examples/mozim_dhcpv4_sync.rs similarity index 100% rename from examples/mozim_async.rs rename to examples/mozim_dhcpv4_sync.rs diff --git a/examples/mozim_dhcpv6_sync.rs b/examples/mozim_dhcpv6_sync.rs new file mode 100644 index 0000000..3b146d7 --- /dev/null +++ b/examples/mozim_dhcpv6_sync.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 + +use mozim::{DhcpV6Client, DhcpV6Config, DhcpV6IaType}; + +const TEST_NIC: &str = "dhcpcli"; +const POLL_WAIT_TIME: u32 = 5; + +fn main() -> Result<(), Box> { + enable_log(); + let mut config = + DhcpV6Config::new(TEST_NIC, DhcpV6IaType::NonTemporaryAddresses); + config.set_timeout(60); + let mut cli = DhcpV6Client::init(config, None).unwrap(); + + loop { + for event in cli.poll(POLL_WAIT_TIME)? { + if let Some(lease) = cli.process(event)? { + println!("Got DHCPv6 lease {:?}", lease); + } + } + } +} + +fn enable_log() { + env_logger::Builder::new() + .filter(Some("nispor"), log::LevelFilter::Debug) + .filter(Some("mozim"), log::LevelFilter::Debug) + .init(); +} diff --git a/src/client_async.rs b/src/client_async.rs index 919e8dd..8161d35 100644 --- a/src/client_async.rs +++ b/src/client_async.rs @@ -11,7 +11,10 @@ use futures::{ }; use nix::poll::{PollFd, PollFlags}; -use crate::{DhcpError, DhcpV4Client, DhcpV4Config, DhcpV4Lease, ErrorKind}; +use crate::{ + DhcpError, DhcpV4Client, DhcpV4Config, DhcpV4Lease, DhcpV6Client, + DhcpV6Config, DhcpV6Lease, ErrorKind, +}; const POLL_TIMEOUT: u16 = 1000; // milliseconds @@ -156,3 +159,85 @@ fn poll_thread(fd: RawFd, share_state: Arc>) { } } } + +#[derive(Debug)] +pub struct DhcpV6ClientAsync { + client: DhcpV6Client, + share_state: Arc>, +} + +impl Stream for DhcpV6ClientAsync { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + // Poll without wait + match self.client.poll(0) { + Ok(events) => { + for event in events { + match self.client.process(event) { + Ok(Some(lease)) => { + return Poll::Ready(Some(Ok(lease))); + } + Ok(None) => (), + Err(e) => { + return Poll::Ready(Some(Err(e))); + } + } + } + } + Err(e) => { + log::error!("DHCP client poll error: {e}"); + return Poll::Ready(Some(Err(e))); + } + } + + let mut share_state = match self.share_state.lock() { + Ok(s) => s, + Err(e) => { + return Poll::Ready(Some(Err(DhcpError::new( + ErrorKind::Bug, + format!( + "BUG: DhcpV6ClientAsync::poll_next() \ + Failed to acquire lock on share_state {e}", + ), + )))); + } + }; + if share_state.waker.is_none() { + share_state.waker = Some(cx.waker().clone()); + drop(share_state); + let fd = self.client.as_raw_fd(); + let share_state = self.share_state.clone(); + std::thread::spawn(move || poll_thread(fd, share_state)); + } else { + share_state.waker = Some(cx.waker().clone()); + drop(share_state); + } + + Poll::Pending + } +} + +impl DhcpV6ClientAsync { + pub fn init( + config: DhcpV6Config, + lease: Option, + ) -> Result { + Ok(Self { + client: DhcpV6Client::init(config, lease)?, + share_state: Arc::new(Mutex::new(ShareState { waker: None })), + }) + } +} + +impl std::ops::Drop for DhcpV6ClientAsync { + fn drop(&mut self) { + if let Ok(mut s) = self.share_state.lock() { + // Signal `poll_thread()` to quit + s.waker = None; + } + } +} diff --git a/src/client.rs b/src/dhcpv4/client.rs similarity index 91% rename from src/client.rs rename to src/dhcpv4/client.rs index 9f56e71..18229cb 100644 --- a/src/client.rs +++ b/src/dhcpv4/client.rs @@ -1,13 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 use std::os::unix::io::{AsRawFd, RawFd}; +use std::time::Duration; use rand::Rng; +use super::{ + event::DhcpV4Event, + time::{gen_dhcp_request_delay, gen_renew_rebind_times}, +}; use crate::{ - event::{DhcpEventPool, DhcpV4Event}, + event::DhcpEventPool, socket::{DhcpRawSocket, DhcpSocket, DhcpUdpSocket}, - time::{gen_dhcp_request_delay, gen_renew_rebind_times}, DhcpError, DhcpV4Config, DhcpV4Lease, DhcpV4Message, DhcpV4MessageType, ErrorKind, }; @@ -37,7 +41,7 @@ impl Default for DhcpV4Phase { #[derive(Debug)] pub struct DhcpV4Client { config: DhcpV4Config, - event_pool: DhcpEventPool, + event_pool: DhcpEventPool, lease: Option, phase: DhcpV4Phase, raw_socket: Option, @@ -59,7 +63,10 @@ impl DhcpV4Client { ) -> Result { config.init()?; let mut event_pool = DhcpEventPool::new()?; - event_pool.add_timer(config.timeout, DhcpV4Event::Timeout)?; + event_pool.add_timer( + Duration::from_secs(config.timeout.into()), + DhcpV4Event::Timeout, + )?; let raw_socket = DhcpRawSocket::new(&config)?; event_pool .add_socket(raw_socket.as_raw_fd(), DhcpV4Event::RawPackageIn)?; @@ -68,7 +75,7 @@ impl DhcpV4Client { let (dhcp_msg, phase) = if let Some(lease) = &lease { event_pool.add_timer( - gen_dhcp_request_delay(0), + Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::RequestTimeout, )?; let mut dhcp_msg = @@ -77,7 +84,7 @@ impl DhcpV4Client { (dhcp_msg, DhcpV4Phase::Request) } else { event_pool.add_timer( - gen_dhcp_request_delay(0), + Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::DiscoveryTimeout, )?; ( @@ -156,12 +163,22 @@ impl DhcpV4Client { lease: &DhcpV4Lease, ) -> Result<(), DhcpError> { let t = gen_renew_rebind_times(lease.t1, lease.t2, lease.lease_time); - self.event_pool.add_timer(t[0], DhcpV4Event::Renew)?; - self.event_pool.add_timer(t[1], DhcpV4Event::RenewRetry)?; - self.event_pool.add_timer(t[2], DhcpV4Event::Rebind)?; - self.event_pool.add_timer(t[3], DhcpV4Event::RebindRetry)?; self.event_pool - .add_timer(lease.lease_time, DhcpV4Event::LeaseExpired)?; + .add_timer(Duration::from_secs(t[0].into()), DhcpV4Event::Renew)?; + self.event_pool.add_timer( + Duration::from_secs(t[1].into()), + DhcpV4Event::RenewRetry, + )?; + self.event_pool + .add_timer(Duration::from_secs(t[2].into()), DhcpV4Event::Rebind)?; + self.event_pool.add_timer( + Duration::from_secs(t[3].into()), + DhcpV4Event::RebindRetry, + )?; + self.event_pool.add_timer( + Duration::from_secs(lease.lease_time.into()), + DhcpV4Event::LeaseExpired, + )?; Ok(()) } @@ -202,7 +219,9 @@ impl DhcpV4Client { self.retry_count = 0; self.phase = DhcpV4Phase::Discovery; self.event_pool.add_timer( - gen_dhcp_request_delay(self.retry_count), + Duration::from_secs( + gen_dhcp_request_delay(self.retry_count).into(), + ), DhcpV4Event::DiscoveryTimeout, )?; if let Some(raw_socket) = &self.raw_socket { @@ -219,7 +238,9 @@ impl DhcpV4Client { } else { self.retry_count += 1; self.event_pool.add_timer( - gen_dhcp_request_delay(self.retry_count), + Duration::from_secs( + gen_dhcp_request_delay(self.retry_count).into(), + ), DhcpV4Event::RequestTimeout, )?; if let Some(raw_socket) = &self.raw_socket { @@ -252,7 +273,9 @@ impl DhcpV4Client { self.event_pool.del_timer(DhcpV4Event::RequestTimeout)?; self.retry_count += 1; self.event_pool.add_timer( - gen_dhcp_request_delay(self.retry_count), + Duration::from_secs( + gen_dhcp_request_delay(self.retry_count).into(), + ), DhcpV4Event::DiscoveryTimeout, )?; if let Some(raw_socket) = &self.raw_socket { @@ -439,13 +462,15 @@ impl DhcpV4Client { &mut self, ) -> Result, DhcpError> { self.clean_up(); - self.event_pool - .add_timer(self.config.timeout, DhcpV4Event::Timeout)?; + self.event_pool.add_timer( + Duration::from_secs(self.config.timeout.into()), + DhcpV4Event::Timeout, + )?; let raw_socket = DhcpRawSocket::new(&self.config)?; self.event_pool .add_socket(raw_socket.as_raw_fd(), DhcpV4Event::RawPackageIn)?; self.event_pool.add_timer( - gen_dhcp_request_delay(0), + Duration::from_secs(gen_dhcp_request_delay(0).into()), DhcpV4Event::DiscoveryTimeout, )?; let dhcp_msg = DhcpV4Message::new( diff --git a/src/config.rs b/src/dhcpv4/config.rs similarity index 63% rename from src/config.rs rename to src/dhcpv4/config.rs index bf5acb1..6736a00 100644 --- a/src/config.rs +++ b/src/dhcpv4/config.rs @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 -use crate::{mac::mac_str_to_u8_array, DhcpError, ErrorKind}; - -use nispor::{NetState, NetStateFilter, NetStateIfaceFilter}; +use crate::{ + mac::mac_str_to_u8_array, nispor::get_nispor_iface, + socket::DEFAULT_SOCKET_TIMEOUT, DhcpError, +}; // https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml#arp-parameters-2 const ARP_HW_TYPE_ETHERNET: u8 = 1; const DEFAULT_TIMEOUT: u32 = 120; -const DEFAULT_SOCKET_TIMEOUT: u32 = 5; #[derive(Debug, PartialEq, Eq, Clone)] pub struct DhcpV4Config { @@ -48,22 +48,7 @@ impl DhcpV4Config { // Check whether interface exists and resolve iface_index and MAC pub(crate) fn init(&mut self) -> Result<(), DhcpError> { - // We use thread to invoke nispor which has `tokio::block_on` which - // stop our async usage - let iface_name = self.iface_name.clone(); - let np_iface = match std::thread::spawn(move || { - get_nispor_iface(iface_name.as_str()) - }) - .join() - { - Ok(n) => n?, - Err(e) => { - return Err(DhcpError::new( - ErrorKind::Bug, - format!("Failed to invoke nispor thread: {e:?}"), - )); - } - }; + let np_iface = get_nispor_iface(self.iface_name.as_str(), false)?; self.iface_index = np_iface.index; if !self.is_proxy { self.src_mac = np_iface.mac_address; @@ -121,36 +106,3 @@ impl DhcpV4Config { self } } - -fn get_nispor_iface(iface_name: &str) -> Result { - if iface_name.is_empty() { - let e = DhcpError::new( - ErrorKind::InvalidArgument, - "Interface name not defined".to_string(), - ); - log::error!("{}", e); - return Err(e); - } - let mut filter = NetStateFilter::minimum(); - let mut iface_filter = NetStateIfaceFilter::minimum(); - iface_filter.iface_name = Some(iface_name.to_string()); - filter.iface = Some(iface_filter); - - let net_state = match NetState::retrieve_with_filter(&filter) { - Ok(s) => s, - Err(e) => { - return Err(DhcpError::new( - ErrorKind::Bug, - format!("Faild to retrieve network state: {e}"), - )) - } - }; - if let Some(iface) = net_state.ifaces.get(iface_name) { - Ok(iface.clone()) - } else { - Err(DhcpError::new( - ErrorKind::InvalidArgument, - format!("Interface {iface_name} not found"), - )) - } -} diff --git a/src/dhcpv4/event.rs b/src/dhcpv4/event.rs new file mode 100644 index 0000000..94c53f0 --- /dev/null +++ b/src/dhcpv4/event.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::convert::TryFrom; + +use crate::{event::DhcpEvent, DhcpError, ErrorKind}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +pub enum DhcpV4Event { + RawPackageIn = 1, + UdpPackageIn, + DiscoveryTimeout, + RequestTimeout, + Timeout, + Renew, + RenewRetry, + Rebind, + RebindRetry, + LeaseExpired, +} + +impl From for u64 { + fn from(v: DhcpV4Event) -> u64 { + v as u64 + } +} + +impl TryFrom for DhcpV4Event { + type Error = DhcpError; + fn try_from(v: u64) -> Result { + match v { + x if x == Self::RawPackageIn as u64 => Ok(Self::RawPackageIn), + x if x == Self::UdpPackageIn as u64 => Ok(Self::UdpPackageIn), + x if x == Self::DiscoveryTimeout as u64 => { + Ok(Self::DiscoveryTimeout) + } + x if x == Self::RequestTimeout as u64 => Ok(Self::RequestTimeout), + x if x == Self::Timeout as u64 => Ok(Self::Timeout), + x if x == Self::Renew as u64 => Ok(Self::Renew), + x if x == Self::RenewRetry as u64 => Ok(Self::RenewRetry), + x if x == Self::Rebind as u64 => Ok(Self::Rebind), + x if x == Self::RebindRetry as u64 => Ok(Self::RebindRetry), + x if x == Self::LeaseExpired as u64 => Ok(Self::LeaseExpired), + _ => { + let e = DhcpError::new( + ErrorKind::Bug, + format!("Got unexpected event ID {v}"), + ); + log::error!("{}", e); + Err(e) + } + } + } +} + +impl std::fmt::Display for DhcpV4Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::RawPackageIn => "RawPackageIn", + Self::UdpPackageIn => "UdpPackageIn", + Self::DiscoveryTimeout => "DiscoveryTimeout", + Self::RequestTimeout => "RequestTimeout", + Self::Timeout => "Timeout", + Self::Renew => "Renew", + Self::RenewRetry => "RenewRetry", + Self::Rebind => "Rebind", + Self::RebindRetry => "RebindRetry", + Self::LeaseExpired => "LeaseExpired", + } + ) + } +} + +impl DhcpEvent for DhcpV4Event {} diff --git a/src/lease.rs b/src/dhcpv4/lease.rs similarity index 97% rename from src/lease.rs rename to src/dhcpv4/lease.rs index 6f8fac3..e717c1a 100644 --- a/src/lease.rs +++ b/src/dhcpv4/lease.rs @@ -1,3 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 + use std::net::Ipv4Addr; use dhcproto::{v4, v4::DhcpOption}; @@ -86,7 +88,7 @@ impl std::convert::TryFrom<&v4::Message> for DhcpV4Lease { DhcpOption::Router(v) => { ret.gateways = Some(v.clone()); } - DhcpOption::NTPServers(v) => { + DhcpOption::NtpServers(v) => { ret.ntp_srvs = Some(v.clone()); } DhcpOption::Hostname(v) => { diff --git a/src/dhcpv4/mod.rs b/src/dhcpv4/mod.rs new file mode 100644 index 0000000..a32361d --- /dev/null +++ b/src/dhcpv4/mod.rs @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 + +mod client; +mod config; +mod event; +mod lease; +mod msg; +mod time; + +pub use self::client::DhcpV4Client; +pub use self::config::DhcpV4Config; +pub use self::event::DhcpV4Event; +pub use self::lease::DhcpV4Lease; +pub use self::msg::{DhcpV4Message, DhcpV4MessageType}; diff --git a/src/msg.rs b/src/dhcpv4/msg.rs similarity index 99% rename from src/msg.rs rename to src/dhcpv4/msg.rs index 6ebf88d..0e4bb07 100644 --- a/src/msg.rs +++ b/src/dhcpv4/msg.rs @@ -114,7 +114,7 @@ impl DhcpV4Message { v4::OptionCode::DomainNameServer, v4::OptionCode::DomainName, v4::OptionCode::InterfaceMtu, - v4::OptionCode::NTPServers, + v4::OptionCode::NtpServers, ])); } else if self.msg_type == DhcpV4MessageType::Request { dhcp_msg @@ -156,7 +156,7 @@ impl DhcpV4Message { v4::OptionCode::DomainNameServer, v4::OptionCode::DomainName, v4::OptionCode::InterfaceMtu, - v4::OptionCode::NTPServers, + v4::OptionCode::NtpServers, ])); } else if self.msg_type == DhcpV4MessageType::Release { if let Some(lease) = self.lease.as_ref() { diff --git a/src/dhcpv4/time.rs b/src/dhcpv4/time.rs new file mode 100644 index 0000000..f14a8c4 --- /dev/null +++ b/src/dhcpv4/time.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +use rand::Rng; + +// The T1/T2 randomization is done by server side according to RFC 2131: +// Times T1 and T2 SHOULD be chosen with some random "fuzz" around a fixed +// value, to avoid synchronization of client reacquisition. +pub(crate) fn gen_renew_rebind_times(t1: u32, t2: u32, lease: u32) -> [u32; 4] { + [t1, t1 + (t2 - t1) / 2, t2, t2 + (lease - t2) / 2] +} + +// RFC 2131, section 4.1 "Constructing and sending DHCP messages" has +// retransmission guideline. +// It should be starting with 4 seconds and double of previous delay, up to 64 +// seconds. Delay should be randomized from range -1 to 1; +pub(crate) fn gen_dhcp_request_delay(retry_count: u32) -> u32 { + let mut base = 2u64.pow(retry_count + 2) - 1; + if base > 62 { + base = 62; + } + let ms: u64 = rand::thread_rng().gen_range(0..2000); + (Duration::from_secs(base) + Duration::from_millis(ms)) + .as_secs() + .try_into() + .unwrap_or(u32::MAX) +} diff --git a/src/dhcpv6/client.rs b/src/dhcpv6/client.rs new file mode 100644 index 0000000..60d148a --- /dev/null +++ b/src/dhcpv6/client.rs @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::Ipv6Addr; +use std::os::fd::{AsRawFd, RawFd}; +use std::time::{Duration, Instant}; + +use rand::Rng; + +use super::{ + msg::{DhcpV6Message, DhcpV6MessageType}, + time::{ + gen_rebind_wait_time, gen_renew_wait_time, gen_request_wait_time, + gen_solicit_wait_time, + }, +}; +use crate::{ + event::DhcpEventPool, + socket::{DhcpSocket, DhcpUdpSocket}, + DhcpError, DhcpV6Config, DhcpV6Event, DhcpV6IaType, DhcpV6Lease, ErrorKind, +}; + +const DHCPV6_REPLAY_AND_SRVS: Ipv6Addr = + Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 1, 2); + +#[derive(Debug, PartialEq, Clone, Copy)] +enum DhcpV6Phase { + Done, + PreSolicit, + Solicit, + PreRequest, + Request, + Renew, + Rebind, +} + +impl std::fmt::Display for DhcpV6Phase { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::Done => "done", + Self::PreSolicit => "pre_solicit", + Self::PreRequest => "pre_request", + Self::Solicit => "solicit", + Self::Request => "request", + Self::Renew => "renew", + Self::Rebind => "rebind", + } + ) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub struct DhcpV6Client { + config: DhcpV6Config, + event_pool: DhcpEventPool, + lease: Option, + phase: DhcpV6Phase, + udp_socket: Option, + xid: [u8; 3], + retrans_timeout: Duration, + retrans_count: u32, + trans_begin_time: Option, + trans_dhcp_msg: Option, +} + +impl AsRawFd for DhcpV6Client { + fn as_raw_fd(&self) -> RawFd { + self.event_pool.epoll.as_raw_fd() + } +} + +impl DhcpV6Client { + pub fn init( + mut config: DhcpV6Config, + lease: Option, + ) -> Result { + config.init()?; + let mut event_pool = DhcpEventPool::new()?; + event_pool.add_timer( + Duration::from_secs(config.timeout.into()), + DhcpV6Event::Timeout, + )?; + + // In RFC 8415, the `transaction-id` is a 3-octet field + let mut xid: [u8; 3] = [0; 3]; + xid.copy_from_slice( + &rand::thread_rng().gen::().to_le_bytes()[..3], + ); + let mut ret = Self { + config, + event_pool, + lease, + phase: DhcpV6Phase::Done, + xid, + udp_socket: None, + retrans_timeout: Duration::new(0, 0), + retrans_count: 0, + trans_begin_time: None, + trans_dhcp_msg: None, + }; + if ret.lease.is_some() { + ret.process_renew()?; + } else { + ret.process_solicit()?; + } + + Ok(ret) + } + + fn clean_trans_counters(&mut self) { + self.trans_dhcp_msg = None; + self.retrans_count = 0; + self.retrans_timeout = Duration::new(0, 0); + self.trans_begin_time = None; + } + + pub fn poll(&self, wait_time: u32) -> Result, DhcpError> { + self.event_pool.poll(wait_time) + } + + pub fn process( + &mut self, + event: DhcpV6Event, + ) -> Result, DhcpError> { + log::debug!("Processing event {:?}", event); + match event { + DhcpV6Event::TransmitWait => { + self.process_transmit()?; + Ok(None) + } + DhcpV6Event::UdpPackageIn => match self.phase { + DhcpV6Phase::Solicit => { + self.process_advertise()?; + Ok(None) + } + DhcpV6Phase::Request + | DhcpV6Phase::Renew + | DhcpV6Phase::Rebind => self.process_reply(), + _ => Err(DhcpError::new( + ErrorKind::Bug, + format!( + "Cannot process unsupported phase {} in \ + UdpPackageIn", + self.phase + ), + )), + }, + DhcpV6Event::Renew => { + self.process_renew()?; + Ok(None) + } + DhcpV6Event::LeaseExpired => { + self.process_solicit()?; + Ok(None) + } + DhcpV6Event::Rebind => { + self.process_rebind()?; + Ok(None) + } + _ => Err(DhcpError::new( + ErrorKind::Bug, + format!("Cannot process unsupported event {}", event), + )), + } + } + + fn process_solicit(&mut self) -> Result<(), DhcpError> { + self.phase = DhcpV6Phase::PreSolicit; + self.lease = None; + self.retrans_timeout = + gen_solicit_wait_time(Instant::now(), 0, Duration::new(0, 0))?; + self.trans_dhcp_msg = Some(DhcpV6Message::new( + &self.config, + DhcpV6MessageType::SOLICIT, + self.xid, + )); + self.event_pool + .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) + } + + fn process_advertise(&mut self) -> Result<(), DhcpError> { + self.event_pool.del_timer(DhcpV6Event::Timeout)?; + let socket = match self.udp_socket.as_ref() { + Some(s) => s, + None => { + return Err(DhcpError::new( + ErrorKind::Bug, + format!("Got NULL socket for process_solicit {:?}", self), + )); + } + }; + let lease = match recv_dhcp_msg( + socket, + DhcpV6MessageType::ADVERTISE, + self.xid, + )? { + Some(l) => l, + None => return Ok(()), + }; + + let mut dhcp_msg = DhcpV6Message::new( + &self.config, + DhcpV6MessageType::REQUEST, + self.xid, + ); + if let Err(e) = dhcp_msg.load_lease(lease.clone()) { + log::warn!("Invalid DHCPv6 lease: {e}, will retry later"); + return Ok(()); + } + self.event_pool.del_timer(DhcpV6Event::TransmitWait)?; + self.clean_trans_counters(); + self.retrans_timeout = + gen_request_wait_time(Instant::now(), 0, Duration::new(0, 0))?; + self.trans_dhcp_msg = Some(dhcp_msg); + self.event_pool + .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait)?; + self.phase = DhcpV6Phase::PreRequest; + Ok(()) + } + + // TODO: Handle sever reply with valid_life with 0(indicate requested + // IA is invalid) + fn process_reply(&mut self) -> Result, DhcpError> { + let socket = match self.udp_socket.as_ref() { + Some(s) => s, + None => { + return Err(DhcpError::new( + ErrorKind::Bug, + format!("Got NULL socket for process_solicit {:?}", self), + )); + } + }; + let lease = + match recv_dhcp_msg(socket, DhcpV6MessageType::REPLY, self.xid)? { + Some(l) => l, + None => return Ok(None), + }; + + self.phase = DhcpV6Phase::Done; + self.event_pool.del_socket(DhcpV6Event::UdpPackageIn)?; + self.udp_socket = None; + self.event_pool.del_timer(DhcpV6Event::TransmitWait)?; + self.lease = Some(lease.clone()); + self.clean_trans_counters(); + self.schedule_renew_rebind_restart()?; + + Ok(Some(lease)) + } + + // TODO: rate control + fn process_transmit(&mut self) -> Result<(), DhcpError> { + self.event_pool.del_timer(DhcpV6Event::TransmitWait)?; + self.schedule_next_retransmit()?; + + // The RFC 8415 said + // A client is not expected to listen for a response during the + // entire RT period and may turn off listening capabilities after + // waiting at least the shorter of RT and MAX_WAIT_TIME due to + // power consumption saving or other reasons. Of course, a client + // MUST listen for a Reconfigure if it has negotiated for its use + // with the server. + // Hence it is OK to create UDP socket when actual transmitting happens. + if self.udp_socket.is_none() { + let socket = DhcpUdpSocket::new_v6( + self.config.iface_index, + &self.config.src_ip, + self.config.socket_timeout, + )?; + self.event_pool + .add_socket(socket.as_raw_fd(), DhcpV6Event::UdpPackageIn)?; + self.udp_socket = Some(socket); + } + let socket = self.udp_socket.as_ref().unwrap(); + let dhcp_msg = match self.trans_dhcp_msg.as_mut() { + Some(p) => p, + None => { + return Err(DhcpError::new( + ErrorKind::Bug, + format!( + "Got NULL DHCP package for process_transmit {:?}", + self + ), + )); + } + }; + if self.retrans_count > 1 { + // We are safe to use unwrap as `schedule_next_retransmit()` + // already confirmed so. + dhcp_msg.add_elapsed_time(self.trans_begin_time.unwrap()); + } + // TODO Support unicast to server + socket.send_to_v6(&DHCPV6_REPLAY_AND_SRVS, &dhcp_msg.to_dhcp_pkg()?)?; + match self.phase { + DhcpV6Phase::PreSolicit => self.phase = DhcpV6Phase::Solicit, + DhcpV6Phase::PreRequest => self.phase = DhcpV6Phase::Request, + _ => (), + } + Ok(()) + } + + fn schedule_next_retransmit(&mut self) -> Result<(), DhcpError> { + self.retrans_count += 1; + if self.trans_begin_time.is_none() { + self.trans_begin_time = Some(Instant::now()); + } + self.retrans_timeout = match self.phase { + DhcpV6Phase::PreSolicit | DhcpV6Phase::Solicit => { + gen_solicit_wait_time( + self.trans_begin_time.unwrap(), + self.retrans_count, + self.retrans_timeout, + )? + } + DhcpV6Phase::PreRequest | DhcpV6Phase::Request => { + gen_request_wait_time( + self.trans_begin_time.unwrap(), + self.retrans_count, + self.retrans_timeout, + )? + } + DhcpV6Phase::Renew => { + if let Some(lease) = self.lease.as_ref() { + gen_rebind_wait_time( + self.trans_begin_time.unwrap(), + self.retrans_count, + self.retrans_timeout, + Duration::from_secs(lease.t2.into()), + )? + } else { + return Err(DhcpError::new( + ErrorKind::Bug, + format!( + "Got NULL lease for DhcpV6Phase::Rebind in \ + schedule_next_retransmit(): {:?}", + self + ), + )); + } + } + DhcpV6Phase::Rebind => { + if let Some(lease) = self.lease.as_ref() { + gen_rebind_wait_time( + self.trans_begin_time.unwrap(), + self.retrans_count, + self.retrans_timeout, + Duration::from_secs(lease.valid_life.into()), + )? + } else { + return Err(DhcpError::new( + ErrorKind::Bug, + format!( + "Got NULL lease for DhcpV6Phase::Rebind in \ + schedule_next_retransmit(): {:?}", + self + ), + )); + } + } + _ => { + return Err(DhcpError::new( + ErrorKind::Bug, + format!( + "Got invalid phase {:?} for \ + `schedule_next_retransmit()`: {:?}", + self.phase, self + ), + )); + } + }; + self.event_pool + .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) + } + + fn schedule_renew_rebind_restart(&mut self) -> Result<(), DhcpError> { + if let Some(lease) = self.lease.as_ref() { + self.event_pool.add_timer( + Duration::from_secs(lease.valid_life.into()), + DhcpV6Event::LeaseExpired, + )?; + if lease.ia_type != DhcpV6IaType::TemporaryAddresses { + self.event_pool.add_timer( + Duration::from_secs(lease.t1.into()), + DhcpV6Event::Renew, + )?; + self.event_pool.add_timer( + Duration::from_secs(lease.t2.into()), + DhcpV6Event::Rebind, + )?; + } + Ok(()) + } else { + Err(DhcpError::new( + ErrorKind::Bug, + format!( + "Got NULL lease for `schedule_renew_rebind()`: {:?}", + self + ), + )) + } + } + + fn process_renew(&mut self) -> Result<(), DhcpError> { + self.event_pool.del_timer(DhcpV6Event::Renew)?; + self.phase = DhcpV6Phase::Renew; + if let Some(lease) = self.lease.as_ref() { + self.retrans_timeout = gen_renew_wait_time( + Instant::now(), + 0, + Duration::new(0, 0), + Duration::from_secs(lease.t2.into()), + )?; + let mut dhcp_msg = DhcpV6Message::new( + &self.config, + DhcpV6MessageType::RENEW, + self.xid, + ); + dhcp_msg.load_lease(lease.clone())?; + self.trans_dhcp_msg = Some(dhcp_msg); + self.event_pool + .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) + } else { + Err(DhcpError::new( + ErrorKind::Bug, + format!("Got NULL lease for `process_renew()`: {:?}", self), + )) + } + } + + fn process_rebind(&mut self) -> Result<(), DhcpError> { + self.event_pool.del_timer(DhcpV6Event::Rebind)?; + self.phase = DhcpV6Phase::Rebind; + if let Some(lease) = self.lease.as_ref() { + self.retrans_timeout = gen_rebind_wait_time( + Instant::now(), + 0, + Duration::new(0, 0), + Duration::from_secs(lease.valid_life.into()), + )?; + let mut dhcp_msg = DhcpV6Message::new( + &self.config, + DhcpV6MessageType::REBIND, + self.xid, + ); + dhcp_msg.load_lease(lease.clone())?; + self.trans_dhcp_msg = Some(dhcp_msg); + self.event_pool + .add_timer(self.retrans_timeout, DhcpV6Event::TransmitWait) + } else { + Err(DhcpError::new( + ErrorKind::Bug, + format!("Got NULL lease for `process_renew()`: {:?}", self), + )) + } + } +} + +fn recv_dhcp_msg( + socket: &DhcpUdpSocket, + expected: DhcpV6MessageType, + xid: [u8; 3], +) -> Result, DhcpError> { + let buffer: Vec = socket.recv()?; + let reply_dhcp_msg = DhcpV6Message::from_dhcp_pkg(&buffer)?; + if reply_dhcp_msg.xid != xid { + log::debug!( + "Dropping DHCP message due to xid miss-match. \ + Expecting {:?}, got {:?}", + xid, + reply_dhcp_msg.xid + ); + return Ok(None); + } + if reply_dhcp_msg.msg_type != expected { + log::debug!( + "Dropping DHCP message due to type miss-match. + Expecting {}, got {}", + expected, + reply_dhcp_msg.msg_type + ); + return Ok(None); + } + if let Some(lease) = reply_dhcp_msg.lease { + Ok(Some(lease)) + } else { + log::debug!( + "No lease found in the reply from DHCP server {:?}", + reply_dhcp_msg + ); + Ok(None) + } +} diff --git a/src/dhcpv6/config.rs b/src/dhcpv6/config.rs new file mode 100644 index 0000000..b2abf6a --- /dev/null +++ b/src/dhcpv6/config.rs @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::Ipv6Addr; +use std::time::{Duration, SystemTime}; + +use rand::RngCore; + +use crate::{ + mac::mac_str_to_u8_array, + nispor::{get_ipv6_addr_of_iface, get_nispor_iface}, + socket::DEFAULT_SOCKET_TIMEOUT, + DhcpError, +}; + +// https://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml +const ARP_HW_TYPE_ETHERNET: u16 = 1; + +const OPTION_IA_NA: u16 = 3; +const OPTION_IA_TA: u16 = 4; +const OPTION_IA_PD: u16 = 5; + +// RFC 8415 11.2. DUID Based on Link-Layer Address Plus Time (DUID-LLT) +// Indicate the base time is midnight (UTC), January 1, 2000 +// This is calculated value by chrono: +// chrono::Utc.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap() +// - chrono::Utc.with_ymd_and_hms(1970, 1, 1, 0, 0, 0).unwrap() +const BASE_TIME: Duration = Duration::new(946684800, 0); + +const DHCPV6_DUID_TYPE_LLT: u16 = 1; +const DHCPV6_DUID_TYPE_EN: u16 = 2; +const DHCPV6_DUID_TYPE_LL: u16 = 3; +const DHCPV6_DUID_TYPE_UUID: u16 = 4; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub enum DhcpV6IaType { + NonTemporaryAddresses, + TemporaryAddresses, + PrefixDelegation, +} + +impl Default for DhcpV6IaType { + fn default() -> Self { + Self::NonTemporaryAddresses + } +} + +impl std::fmt::Display for DhcpV6IaType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::NonTemporaryAddresses => "IANA", + Self::TemporaryAddresses => "IATA", + Self::PrefixDelegation => "IAPD", + } + ) + } +} + +impl From for u16 { + fn from(v: DhcpV6IaType) -> Self { + match v { + DhcpV6IaType::NonTemporaryAddresses => OPTION_IA_NA, + DhcpV6IaType::TemporaryAddresses => OPTION_IA_TA, + DhcpV6IaType::PrefixDelegation => OPTION_IA_PD, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct DhcpV6Config { + pub(crate) iface_name: String, + pub(crate) iface_index: u32, + pub(crate) duid: Dhcpv6Duid, + pub(crate) timeout: u32, + pub(crate) ia_type: DhcpV6IaType, + pub(crate) src_ip: Ipv6Addr, + pub(crate) socket_timeout: u32, +} + +impl Default for DhcpV6Config { + fn default() -> Self { + Self { + iface_name: String::new(), + iface_index: 0, + duid: Dhcpv6Duid::Other(Vec::new()), + timeout: 0, + ia_type: DhcpV6IaType::default(), + src_ip: Ipv6Addr::UNSPECIFIED, + socket_timeout: DEFAULT_SOCKET_TIMEOUT, + } + } +} + +impl DhcpV6Config { + pub fn new(iface_name: &str, ia_type: DhcpV6IaType) -> Self { + Self { + iface_name: iface_name.to_string(), + ia_type, + ..Default::default() + } + } + + /// Set timeout in seconds + pub fn set_timeout(&mut self, timeout: u32) -> &mut Self { + self.timeout = timeout; + self + } + + /// Set arbitrary DUID + pub fn set_duid(&mut self, duid: Dhcpv6Duid) -> &mut Self { + self.duid = duid; + self + } + + // Check whether interface exists and resolve iface_index and MAC + pub(crate) fn init(&mut self) -> Result<(), DhcpError> { + let np_iface = get_nispor_iface(self.iface_name.as_str(), true)?; + self.iface_index = np_iface.index; + self.src_ip = get_ipv6_addr_of_iface(&np_iface)?; + self.duid = if np_iface.mac_address.is_empty() { + Dhcpv6Duid::default() + } else { + Dhcpv6Duid::LL(Dhcpv6DuidLl::new( + ARP_HW_TYPE_ETHERNET, + &mac_str_to_u8_array(np_iface.mac_address.as_str()), + )) + }; + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub enum Dhcpv6Duid { + LLT(Dhcpv6DuidLlt), + EN(Dhcpv6DuidEn), + LL(Dhcpv6DuidLl), + UUID(Dhcpv6DuidUuid), + Other(Vec), +} + +impl Default for Dhcpv6Duid { + fn default() -> Self { + let mut rand_data = [0u8; 16]; + rand::thread_rng().fill_bytes(&mut rand_data); + Self::Other(rand_data.to_vec()) + } +} + +impl Dhcpv6Duid { + pub fn to_vec(&self) -> Vec { + match self { + Self::LLT(v) => v.to_vec(), + Self::EN(v) => v.to_vec(), + Self::LL(v) => v.to_vec(), + Self::UUID(v) => v.to_vec(), + Self::Other(v) => v.clone(), + } + } +} + +// Type 1 +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct Dhcpv6DuidLlt { + pub hardware_type: u16, + pub time: u32, + pub link_layer_address: Vec, +} + +impl Dhcpv6DuidLlt { + pub fn new(hardware_type: u16, link_layer_address: &[u8]) -> Self { + let time: u32 = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .ok() + .and_then(|s| s.checked_sub(BASE_TIME)) + .map(|t| t.as_secs()) + .map(|t| t as u32) + .unwrap_or_default(); + + Self { + hardware_type, + time, + link_layer_address: link_layer_address.to_vec(), + } + } + + pub fn to_vec(&self) -> Vec { + let mut ret: Vec = Vec::new(); + ret.extend_from_slice(&DHCPV6_DUID_TYPE_LLT.to_be_bytes()); + ret.extend_from_slice(&self.hardware_type.to_be_bytes()); + ret.extend_from_slice(&self.time.to_be_bytes()); + ret.extend_from_slice(self.link_layer_address.as_slice()); + ret + } +} + +// Type 2 +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct Dhcpv6DuidEn { + pub enterprise_number: u32, + pub identifier: Vec, +} + +impl Dhcpv6DuidEn { + pub fn new(enterprise_number: u32, identifier: &[u8]) -> Self { + Self { + enterprise_number, + identifier: identifier.to_vec(), + } + } + + pub fn to_vec(&self) -> Vec { + let mut ret: Vec = Vec::new(); + ret.extend_from_slice(&DHCPV6_DUID_TYPE_EN.to_be_bytes()); + ret.extend_from_slice(&self.enterprise_number.to_be_bytes()); + ret.extend_from_slice(self.identifier.as_slice()); + ret + } +} + +// Type 3 +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct Dhcpv6DuidLl { + hardware_type: u16, + link_layer_address: Vec, +} + +impl Dhcpv6DuidLl { + pub fn new(hardware_type: u16, link_layer_address: &[u8]) -> Self { + Self { + hardware_type, + link_layer_address: link_layer_address.to_vec(), + } + } + + pub fn to_vec(&self) -> Vec { + let mut ret: Vec = Vec::new(); + ret.extend_from_slice(&DHCPV6_DUID_TYPE_LL.to_be_bytes()); + ret.extend_from_slice(&self.hardware_type.to_be_bytes()); + ret.extend_from_slice(self.link_layer_address.as_slice()); + ret + } +} + +// Type 4 +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[non_exhaustive] +pub struct Dhcpv6DuidUuid { + uuid: u128, +} + +impl Dhcpv6DuidUuid { + pub fn new(uuid: u128) -> Self { + Self { uuid } + } + + pub fn to_vec(&self) -> Vec { + let mut ret: Vec = Vec::new(); + ret.extend_from_slice(&DHCPV6_DUID_TYPE_UUID.to_be_bytes()); + ret.extend_from_slice(&self.uuid.to_be_bytes()); + ret + } +} diff --git a/src/dhcpv6/event.rs b/src/dhcpv6/event.rs new file mode 100644 index 0000000..8dbd8ae --- /dev/null +++ b/src/dhcpv6/event.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::convert::TryFrom; + +use crate::{event::DhcpEvent, DhcpError, ErrorKind}; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +#[non_exhaustive] +pub enum DhcpV6Event { + UdpPackageIn = 1, + TransmitWait, + Timeout, + Renew, + Rebind, + LeaseExpired, +} + +impl From for u64 { + fn from(v: DhcpV6Event) -> u64 { + v as u64 + } +} + +impl TryFrom for DhcpV6Event { + type Error = DhcpError; + fn try_from(v: u64) -> Result { + match v { + x if x == Self::UdpPackageIn as u64 => Ok(Self::UdpPackageIn), + x if x == Self::TransmitWait as u64 => Ok(Self::TransmitWait), + x if x == Self::Timeout as u64 => Ok(Self::Timeout), + x if x == Self::Renew as u64 => Ok(Self::Renew), + x if x == Self::Rebind as u64 => Ok(Self::Rebind), + x if x == Self::LeaseExpired as u64 => Ok(Self::LeaseExpired), + _ => { + let e = DhcpError::new( + ErrorKind::Bug, + format!("Got unexpected event ID {v}"), + ); + log::error!("{}", e); + Err(e) + } + } + } +} + +impl std::fmt::Display for DhcpV6Event { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Self::UdpPackageIn => "UdpPackageIn", + Self::TransmitWait => "TransmitWait", + Self::Timeout => "Timeout", + Self::Renew => "Renew", + Self::Rebind => "Rebind", + Self::LeaseExpired => "LeaseExpired", + } + ) + } +} + +impl DhcpEvent for DhcpV6Event {} diff --git a/src/dhcpv6/lease.rs b/src/dhcpv6/lease.rs new file mode 100644 index 0000000..66949fc --- /dev/null +++ b/src/dhcpv6/lease.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::Ipv6Addr; + +use dhcproto::{ + v6, + v6::{DhcpOption, DhcpOptions}, +}; + +use crate::{DhcpError, DhcpV6IaType, ErrorKind}; + +#[derive(Debug, PartialEq, Eq, Clone)] +#[non_exhaustive] +pub struct DhcpV6Lease { + pub t1: u32, + pub t2: u32, + pub xid: [u8; 3], + pub iaid: u32, + pub ia_type: DhcpV6IaType, + pub addr: Ipv6Addr, + pub prefix_len: u8, + // TODO: OPTION_UNICAST + // For Request, Renew, Information-request, Release, and Decline + // messages, it is allowed only if the Server Unicast option is + // configured. + pub preferred_life: u32, + pub valid_life: u32, + pub cli_duid: Vec, + pub srv_duid: Vec, + pub dhcp_opts: Vec, +} + +impl Default for DhcpV6Lease { + fn default() -> Self { + Self { + t1: 0, + t2: 0, + xid: [0; 3], + iaid: 0, + ia_type: DhcpV6IaType::TemporaryAddresses, + addr: Ipv6Addr::UNSPECIFIED, + prefix_len: 128, + preferred_life: 0, + valid_life: 0, + cli_duid: Vec::new(), + srv_duid: Vec::new(), + dhcp_opts: Vec::new(), + } + } +} + +impl std::convert::TryFrom<&v6::Message> for DhcpV6Lease { + type Error = DhcpError; + fn try_from(v6_dhcp_msg: &v6::Message) -> Result { + let mut ret = Self { + xid: v6_dhcp_msg.xid(), + ..Default::default() + }; + for dhcp_opt in v6_dhcp_msg.opts().iter() { + match dhcp_opt { + DhcpOption::ClientId(v) => ret.cli_duid = v.clone(), + DhcpOption::ServerId(v) => ret.srv_duid = v.clone(), + DhcpOption::IANA(v) => { + ret.ia_type = DhcpV6IaType::NonTemporaryAddresses; + ret.iaid = v.id; + ret.t1 = v.t1; + ret.t2 = v.t2; + parse_dhcp_opt_iaadr(&v.opts, &mut ret); + } + DhcpOption::IATA(v) => { + ret.ia_type = DhcpV6IaType::TemporaryAddresses; + ret.iaid = v.id; + parse_dhcp_opt_iaadr(&v.opts, &mut ret); + } + DhcpOption::IAPD(v) => { + ret.ia_type = DhcpV6IaType::PrefixDelegation; + ret.iaid = v.id; + ret.t1 = v.t1; + ret.t2 = v.t2; + parse_dhcp_opt_iaadr(&v.opts, &mut ret); + } + DhcpOption::StatusCode(v) => { + if v.status != v6::Status::Success { + return Err(DhcpError::new( + ErrorKind::NoLease, + format!( + "DHCP server reply status code {}({:?}), \ + message {}", + u16::from(v.status), + v.status, + v.msg + ), + )); + } + } + v => { + log::debug!("Unsupported DHCPv6 opt {:?}", v); + } + } + } + ret.dhcp_opts = v6_dhcp_msg.opts().iter().cloned().collect(); + // TODO: Validate T1 < T2 < lease_time. + Ok(ret) + } +} + +fn parse_dhcp_opt_iaadr(opts: &DhcpOptions, lease: &mut DhcpV6Lease) { + if let Some(DhcpOption::IAPrefix(a)) = opts.get(v6::OptionCode::IAPrefix) { + lease.addr = a.prefix_ip; + lease.prefix_len = a.prefix_len; + lease.preferred_life = a.preferred_lifetime; + lease.valid_life = a.valid_lifetime; + } + if let Some(DhcpOption::IAAddr(a)) = opts.get(v6::OptionCode::IAAddr) { + lease.addr = a.addr; + lease.preferred_life = a.preferred_life; + lease.valid_life = a.valid_life; + lease.prefix_len = 128 + } +} diff --git a/src/dhcpv6/mod.rs b/src/dhcpv6/mod.rs new file mode 100644 index 0000000..a6cf0cb --- /dev/null +++ b/src/dhcpv6/mod.rs @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 + +mod client; +mod config; +mod event; +mod lease; +mod msg; +mod time; + +pub use self::client::DhcpV6Client; +pub use self::config::{ + DhcpV6Config, DhcpV6IaType, Dhcpv6Duid, Dhcpv6DuidEn, Dhcpv6DuidLl, + Dhcpv6DuidLlt, Dhcpv6DuidUuid, +}; +pub use self::event::DhcpV6Event; +pub use self::lease::DhcpV6Lease; +pub use self::msg::DhcpV6Message; diff --git a/src/dhcpv6/msg.rs b/src/dhcpv6/msg.rs new file mode 100644 index 0000000..d5fc1c1 --- /dev/null +++ b/src/dhcpv6/msg.rs @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::Ipv6Addr; +use std::time::Instant; + +use dhcproto::{ + v6, + v6::{DhcpOption, DhcpOptions}, + Decodable, Decoder, Encodable, +}; + +use crate::{DhcpError, DhcpV6Config, DhcpV6IaType, DhcpV6Lease, ErrorKind}; + +const DEFAULT_IAID: u32 = 0; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub(crate) struct DhcpV6MessageType(v6::MessageType); + +impl DhcpV6MessageType { + pub(crate) const SOLICIT: Self = + DhcpV6MessageType(v6::MessageType::Solicit); + + pub(crate) const ADVERTISE: Self = + DhcpV6MessageType(v6::MessageType::Advertise); + + pub(crate) const REQUEST: Self = + DhcpV6MessageType(v6::MessageType::Request); + + pub(crate) const REPLY: Self = DhcpV6MessageType(v6::MessageType::Reply); + pub(crate) const RENEW: Self = DhcpV6MessageType(v6::MessageType::Renew); + pub(crate) const REBIND: Self = DhcpV6MessageType(v6::MessageType::Rebind); +} + +impl Default for DhcpV6MessageType { + fn default() -> Self { + Self(v6::MessageType::Unknown(0)) + } +} + +impl std::fmt::Display for DhcpV6MessageType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{:?}", + match self.0 { + v6::MessageType::Solicit => "solicit", + v6::MessageType::Advertise => "advertise", + v6::MessageType::Request => "request", + v6::MessageType::Confirm => "confirm", + v6::MessageType::Decline => "decline", + v6::MessageType::Renew => "renew", + v6::MessageType::Rebind => "rebind", + v6::MessageType::Release => "release", + v6::MessageType::Reply => "reply", + _ => { + log::warn!("Got unknown message type {:?}", self.0); + "unknown" + } + } + ) + } +} + +impl From for v6::MessageType { + fn from(v: DhcpV6MessageType) -> Self { + v.0 + } +} + +impl From for DhcpV6MessageType { + fn from(v: v6::MessageType) -> Self { + Self(v) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +#[non_exhaustive] +pub struct DhcpV6Message { + pub(crate) msg_type: DhcpV6MessageType, + pub(crate) lease: Option, + pub(crate) config: DhcpV6Config, + pub(crate) xid: [u8; 3], + elapsed_time: u16, +} + +impl DhcpV6Message { + pub(crate) fn new( + config: &DhcpV6Config, + msg_type: DhcpV6MessageType, + xid: [u8; 3], + ) -> Self { + Self { + msg_type, + config: config.clone(), + lease: None, + xid, + elapsed_time: 0, + } + } + + pub(crate) fn load_lease( + &mut self, + lease: DhcpV6Lease, + ) -> Result<(), DhcpError> { + validate_lease(&self.config, &lease)?; + self.lease = Some(lease); + Ok(()) + } + + pub(crate) fn to_dhcp_pkg(&self) -> Result, DhcpError> { + let mut dhcp_msg = + v6::Message::new_with_id(self.msg_type.into(), self.xid); + + dhcp_msg + .opts_mut() + .insert(DhcpOption::ClientId(self.config.duid.to_vec())); + + match self.config.ia_type { + DhcpV6IaType::NonTemporaryAddresses => { + dhcp_msg.opts_mut().insert(DhcpOption::IANA(v6::IANA { + id: self + .lease + .as_ref() + .map(|l| l.iaid) + .unwrap_or(DEFAULT_IAID), + // Required by RFC 8415 section 21.4 + t1: 0, + // Required by RFC 8415 section 21.4 + t2: 0, + opts: self + .lease + .as_ref() + .map(gen_iaadr_dhcp_opt) + .unwrap_or_default(), + })) + } + DhcpV6IaType::TemporaryAddresses => { + dhcp_msg.opts_mut().insert(DhcpOption::IATA(v6::IATA { + id: self + .lease + .as_ref() + .map(|l| l.iaid) + .unwrap_or(DEFAULT_IAID), + opts: self + .lease + .as_ref() + .map(gen_iaadr_dhcp_opt) + .unwrap_or_default(), + })) + } + DhcpV6IaType::PrefixDelegation => { + dhcp_msg.opts_mut().insert(DhcpOption::IAPD(v6::IAPD { + id: self + .lease + .as_ref() + .map(|l| l.iaid) + .unwrap_or(DEFAULT_IAID), + // Required by RFC 8415 section 21.21 + t1: 0, + // Required by RFC 8415 section 21.21 + t2: 0, + opts: self + .lease + .as_ref() + .map(gen_iaadr_dhcp_opt) + .unwrap_or_default(), + })) + } + } + + match self.msg_type { + DhcpV6MessageType::SOLICIT | DhcpV6MessageType::REBIND => (), + DhcpV6MessageType::REQUEST | DhcpV6MessageType::RENEW => { + if let Some(lease) = self.lease.as_ref() { + dhcp_msg + .opts_mut() + .insert(DhcpOption::ServerId(lease.srv_duid.clone())); + } else { + return Err(DhcpError::new( + ErrorKind::InvalidArgument, + "No DHCP lease found for DHCP request, please run \ + DhcpV6Message::load_lease() first" + .to_string(), + )); + } + } + _ => { + log::error!( + "BUG: Invalid DhcpV6MessageType {:?}", + self.msg_type + ); + } + } + + if self.elapsed_time > 0 { + dhcp_msg + .opts_mut() + .insert(DhcpOption::ElapsedTime(self.elapsed_time)); + } + + log::debug!("DHCP message {:?}", dhcp_msg); + + let mut dhcp_msg_buff = Vec::new(); + let mut e = v6::Encoder::new(&mut dhcp_msg_buff); + dhcp_msg.encode(&mut e)?; + Ok(dhcp_msg_buff) + } + + pub(crate) fn from_dhcp_pkg(payload: &[u8]) -> Result { + let v6_dhcp_msg = v6::Message::decode(&mut Decoder::new(payload)) + .map_err(|decode_error| { + let e = DhcpError::new( + ErrorKind::InvalidDhcpServerReply, + format!( + "Failed to parse DHCPv6 message from payload of pkg \ + {payload:?}: {decode_error}" + ), + ); + log::error!("{}", e); + e + })?; + + let ret = Self { + lease: Some(DhcpV6Lease::try_from(&v6_dhcp_msg)?), + msg_type: v6_dhcp_msg.msg_type().into(), + xid: v6_dhcp_msg.xid(), + ..Default::default() + }; + log::debug!("Got reply DHCP message {:?}", ret); + Ok(ret) + } + + pub(crate) fn add_elapsed_time(&mut self, trans_begin_time: Instant) { + self.elapsed_time = + match u16::try_from(trans_begin_time.elapsed().as_secs() / 100) { + Ok(i) => i, + Err(_) => u16::MAX, + }; + } +} + +fn validate_lease( + config: &DhcpV6Config, + lease: &DhcpV6Lease, +) -> Result<(), DhcpError> { + if lease.ia_type != config.ia_type { + return Err(DhcpError::new( + ErrorKind::InvalidArgument, + format!( + "DHCPv6 lease contains different IA type({}) with config({}) \ + DhcpV6Message::load_lease() with correct lease", + lease.ia_type, config.ia_type + ), + )); + } + if lease.srv_duid.is_empty() { + return Err(DhcpError::new( + ErrorKind::InvalidArgument, + "DHCPv6 lease contains empty server DUID, please run \ + DhcpV6Message::load_lease() with correct lease" + .to_string(), + )); + } + if lease.addr == Ipv6Addr::UNSPECIFIED { + return Err(DhcpError::new( + ErrorKind::InvalidArgument, + "DHCPv6 lease contains invalid all zero lease \ + IPv6 address, please run DhcpV6Message::load_lease() + with correct lease" + .to_string(), + )); + } + Ok(()) +} + +fn gen_iaadr_dhcp_opt(lease: &DhcpV6Lease) -> DhcpOptions { + let mut ret = DhcpOptions::new(); + match lease.ia_type { + DhcpV6IaType::TemporaryAddresses + | DhcpV6IaType::NonTemporaryAddresses => { + ret.insert(DhcpOption::IAAddr(v6::IAAddr { + addr: lease.addr, + // Set to 0 per RFC 8415 section 21.6 + preferred_life: 0, + // Set to 0 per RFC 8415 section 21.6 + valid_life: 0, + opts: DhcpOptions::new(), + })); + } + DhcpV6IaType::PrefixDelegation => { + ret.insert(DhcpOption::IAPrefix(v6::IAPrefix { + prefix_len: lease.prefix_len, + prefix_ip: lease.addr, + // Set to 0 per RFC 8415 section 21.6 + preferred_lifetime: 0, + // Set to 0 per RFC 8415 section 21.6 + valid_lifetime: 0, + opts: DhcpOptions::new(), + })); + } + } + ret +} diff --git a/src/dhcpv6/time.rs b/src/dhcpv6/time.rs new file mode 100644 index 0000000..012fb3a --- /dev/null +++ b/src/dhcpv6/time.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::time::{Duration, Instant}; + +use rand::Rng; + +use crate::{DhcpError, ErrorKind}; + +// RFC 8415 section 7.6 Transmission and Retransmission Parameters +const SOL_TIMEOUT: Duration = Duration::from_secs(1); +const SOL_MAX_RT: Duration = Duration::from_secs(3600); +const REQ_TIMEOUT: Duration = Duration::from_secs(1); +const REQ_MAX_RT: Duration = Duration::from_secs(30); +const REQ_MAX_RC: u32 = 10; +const REN_TIMEOUT: Duration = Duration::from_secs(10); +const REN_MAX_RT: Duration = Duration::from_secs(600); +const REB_TIMEOUT: Duration = Duration::from_secs(10); +const REB_MAX_RT: Duration = Duration::from_secs(600); + +// RFC 8415 section 15. Reliability of Client-Initiated Message Exchanges +// RT Retransmission timeout +// IRT Initial retransmission time +// MRC Maximum retransmission count +// MRT Maximum retransmission time +// MRD Maximum retransmission duration +// RAND Randomization factor +fn gen_retransmit_time( + trans_begin_time: Instant, + retransmit_count: u32, + rt: Duration, + irt: Duration, + mrt: Duration, + mrc: u32, + mrd: Duration, +) -> Option { + if mrc != 0 && mrc < retransmit_count { + return None; + } + if mrd != Duration::new(0, 0) && mrd < trans_begin_time.elapsed() { + return None; + } + + let rt = if rt == Duration::new(0, 0) { + Duration::from_millis( + (irt.as_millis() * rand::thread_rng().gen_range(900..1100) / 1000) + .try_into() + .unwrap_or(u64::MAX), + ) + } else { + Duration::from_millis( + (rt.as_millis() * rand::thread_rng().gen_range(1900..2100) / 1000) + .try_into() + .unwrap_or(u64::MAX), + ) + }; + + if mrt != Duration::new(0, 0) && rt > mrt { + Some(Duration::from_millis( + (mrt.as_millis() * rand::thread_rng().gen_range(900..1100) / 1000) + .try_into() + .unwrap_or(u64::MAX), + )) + } else { + Some(rt) + } +} + +pub(crate) fn gen_solicit_wait_time( + trans_begin_time: Instant, + retransmit_count: u32, + previous_wait_time: Duration, +) -> Result { + match gen_retransmit_time( + trans_begin_time, + retransmit_count, + previous_wait_time, + SOL_TIMEOUT, + SOL_MAX_RT, + 0, + Duration::new(0, 0), + ) { + Some(rt) => Ok(rt), + None => Err(DhcpError::new( + ErrorKind::Timeout, + "Timeout on waiting DHCPv6 reply on SOLICIT message".to_string(), + )), + } +} + +pub(crate) fn gen_request_wait_time( + trans_begin_time: Instant, + retransmit_count: u32, + previous_wait_time: Duration, +) -> Result { + match gen_retransmit_time( + trans_begin_time, + retransmit_count, + previous_wait_time, + REQ_TIMEOUT, + REQ_MAX_RT, + REQ_MAX_RC, + Duration::new(0, 0), + ) { + Some(rt) => Ok(rt), + None => Err(DhcpError::new( + ErrorKind::Timeout, + "Timeout on waiting DHCPv6 reply on REQUEST message".to_string(), + )), + } +} + +pub(crate) fn gen_renew_wait_time( + trans_begin_time: Instant, + retransmit_count: u32, + previous_wait_time: Duration, + t2: Duration, +) -> Result { + match gen_retransmit_time( + trans_begin_time, + retransmit_count, + previous_wait_time, + REN_TIMEOUT, + REN_MAX_RT, + 0, + t2, + ) { + Some(rt) => Ok(rt), + None => Err(DhcpError::new( + ErrorKind::Timeout, + "Timeout on waiting DHCPv6 reply on RENEW message".to_string(), + )), + } +} + +pub(crate) fn gen_rebind_wait_time( + trans_begin_time: Instant, + retransmit_count: u32, + previous_wait_time: Duration, + valid_life: Duration, +) -> Result { + match gen_retransmit_time( + trans_begin_time, + retransmit_count, + previous_wait_time, + REB_TIMEOUT, + REB_MAX_RT, + 0, + valid_life, + ) { + Some(rt) => Ok(rt), + None => Err(DhcpError::new( + ErrorKind::Timeout, + "Timeout on waiting DHCPv6 reply on REBIND message".to_string(), + )), + } +} diff --git a/src/error.rs b/src/error.rs index 63c1689..8df61f2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -30,6 +30,8 @@ impl DhcpError { } } +impl std::error::Error for DhcpError {} + impl std::fmt::Display for ErrorKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") diff --git a/src/event.rs b/src/event.rs index 0c34a92..e04ea60 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::os::fd::BorrowedFd; use std::os::unix::io::{AsRawFd, RawFd}; +use std::time::Duration; use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags}; @@ -10,157 +11,19 @@ use crate::{time::DhcpTimerFd, DhcpError, ErrorKind}; const EVENT_BUFFER_COUNT: usize = 64; -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -pub enum DhcpV4Event { - RawPackageIn = 1, - UdpPackageIn, - DiscoveryTimeout, - RequestTimeout, - Timeout, - Renew, - RenewRetry, - Rebind, - RebindRetry, - LeaseExpired, -} - -impl TryFrom for DhcpV4Event { - type Error = DhcpError; - fn try_from(v: u64) -> Result { - match v { - x if x == Self::RawPackageIn as u64 => Ok(Self::RawPackageIn), - x if x == Self::UdpPackageIn as u64 => Ok(Self::UdpPackageIn), - x if x == Self::DiscoveryTimeout as u64 => { - Ok(Self::DiscoveryTimeout) - } - x if x == Self::RequestTimeout as u64 => Ok(Self::RequestTimeout), - x if x == Self::Timeout as u64 => Ok(Self::Timeout), - x if x == Self::Renew as u64 => Ok(Self::Renew), - x if x == Self::RenewRetry as u64 => Ok(Self::RenewRetry), - x if x == Self::Rebind as u64 => Ok(Self::Rebind), - x if x == Self::RebindRetry as u64 => Ok(Self::RebindRetry), - x if x == Self::LeaseExpired as u64 => Ok(Self::LeaseExpired), - _ => { - let e = DhcpError::new( - ErrorKind::Bug, - format!("Got unexpected event ID {v}"), - ); - log::error!("{}", e); - Err(e) - } - } - } -} - -impl std::fmt::Display for DhcpV4Event { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::RawPackageIn => "RawPackageIn", - Self::UdpPackageIn => "UdpPackageIn", - Self::DiscoveryTimeout => "DiscoveryTimeout", - Self::RequestTimeout => "RequestTimeout", - Self::Timeout => "Timeout", - Self::Renew => "Renew", - Self::RenewRetry => "RenewRetry", - Self::Rebind => "Rebind", - Self::RebindRetry => "RebindRetry", - Self::LeaseExpired => "LeaseExpired", - } - ) - } -} - -#[derive(Debug)] -pub(crate) struct DhcpEventPool { - timer_fds: HashMap, - socket_fds: HashMap, - pub(crate) epoll: DhcpEpoll, -} - -impl Drop for DhcpEventPool { - fn drop(&mut self) { - self.remove_all_event(); - } -} - -impl DhcpEventPool { - pub(crate) fn remove_all_event(&mut self) { - for (_, timer_fd) in self.timer_fds.drain() { - self.epoll.del_fd(timer_fd.as_raw_fd()).ok(); - } - for (_, fd) in self.socket_fds.drain() { - self.epoll.del_fd(fd).ok(); - } - } - - pub(crate) fn new() -> Result { - Ok(Self { - timer_fds: HashMap::new(), - socket_fds: HashMap::new(), - epoll: DhcpEpoll::new()?, - }) - } - - pub(crate) fn add_socket( - &mut self, - fd: RawFd, - event: DhcpV4Event, - ) -> Result<(), DhcpError> { - log::debug!("Adding socket {} with event {} to event pool", fd, event); - self.socket_fds.insert(event, fd); - self.epoll.add_fd(fd, event) - } - - pub(crate) fn add_timer( - &mut self, - timeout: u32, - event: DhcpV4Event, - ) -> Result<(), DhcpError> { - log::debug!( - "Adding timer {} seconds with event {} to event pool", - timeout, - event - ); - let timer_fd = DhcpTimerFd::new(timeout)?; - self.epoll.add_fd(timer_fd.as_raw_fd(), event)?; - self.timer_fds.insert(event, timer_fd); - Ok(()) - } - - pub(crate) fn del_timer( - &mut self, - event: DhcpV4Event, - ) -> Result<(), DhcpError> { - if let Some(timer_fd) = self.timer_fds.remove(&event) { - self.epoll.del_fd(timer_fd.as_raw_fd())?; - } - Ok(()) - } - - pub(crate) fn poll( - &self, - wait_time: u32, - ) -> Result, DhcpError> { - match isize::try_from(wait_time) { - Ok(i) => self.epoll.poll(i), - Err(_) => Err(DhcpError::new( - ErrorKind::InvalidArgument, - format!( - "Invalid timeout, should be in the range of \ - 0 - {}", - isize::MAX - ), - )), - } - } +pub(crate) trait DhcpEvent: + std::fmt::Display + + Into + + Eq + + std::hash::Hash + + TryFrom + + Copy +{ } #[derive(Debug)] pub(crate) struct DhcpEpoll { - fd: Epoll, + pub(crate) fd: Epoll, } impl AsRawFd for DhcpEpoll { @@ -170,7 +33,7 @@ impl AsRawFd for DhcpEpoll { } impl DhcpEpoll { - fn new() -> Result { + pub(crate) fn new() -> Result { Ok(Self { fd: Epoll::new(EpollCreateFlags::empty()).map_err(|e| { let e = DhcpError::new( @@ -183,7 +46,10 @@ impl DhcpEpoll { }) } - fn add_fd(&self, fd: RawFd, event: DhcpV4Event) -> Result<(), DhcpError> { + pub(crate) fn add_fd(&self, fd: RawFd, event: T) -> Result<(), DhcpError> + where + T: DhcpEvent, + { let fd = unsafe { BorrowedFd::borrow_raw(fd) }; log::debug!( "Adding fd {} to Epoll {}, event {}", @@ -191,7 +57,7 @@ impl DhcpEpoll { self.fd.0.as_raw_fd(), event ); - let event = EpollEvent::new(EpollFlags::EPOLLIN, event as u64); + let event = EpollEvent::new(EpollFlags::EPOLLIN, event.into()); self.fd.add(fd, event).map_err(|e| { let e = DhcpError::new( ErrorKind::Bug, @@ -207,7 +73,7 @@ impl DhcpEpoll { }) } - fn del_fd(&self, fd: RawFd) -> Result<(), DhcpError> { + pub(crate) fn del_fd(&self, fd: RawFd) -> Result<(), DhcpError> { let fd = unsafe { BorrowedFd::borrow_raw(fd) }; log::debug!( "Removing fd {} from Epoll {}", @@ -228,7 +94,10 @@ impl DhcpEpoll { }) } - fn poll(&self, wait_time: isize) -> Result, DhcpError> { + pub(crate) fn poll(&self, wait_time: isize) -> Result, DhcpError> + where + T: DhcpEvent, + { let mut events: [EpollEvent; EVENT_BUFFER_COUNT] = [EpollEvent::empty(); EVENT_BUFFER_COUNT]; @@ -237,7 +106,7 @@ impl DhcpEpoll { Ok(c) => { let mut ret = Vec::new(); for i in &events[..c] { - ret.push(DhcpV4Event::try_from(i.data())?); + ret.push(T::try_from(i.data())?); } return Ok(ret); } @@ -258,3 +127,88 @@ impl DhcpEpoll { } } } + +#[derive(Debug)] +pub(crate) struct DhcpEventPool { + timer_fds: HashMap, + socket_fds: HashMap, + pub(crate) epoll: DhcpEpoll, +} + +impl Drop for DhcpEventPool { + fn drop(&mut self) { + self.remove_all_event(); + } +} + +impl DhcpEventPool { + pub(crate) fn remove_all_event(&mut self) { + for (_, timer_fd) in self.timer_fds.drain() { + self.epoll.del_fd(timer_fd.as_raw_fd()).ok(); + } + for (_, fd) in self.socket_fds.drain() { + self.epoll.del_fd(fd).ok(); + } + } + + pub(crate) fn new() -> Result { + Ok(Self { + timer_fds: HashMap::new(), + socket_fds: HashMap::new(), + epoll: DhcpEpoll::new()?, + }) + } + + pub(crate) fn add_socket( + &mut self, + fd: RawFd, + event: T, + ) -> Result<(), DhcpError> { + log::debug!("Adding socket {} with event {} to event pool", fd, event); + self.socket_fds.insert(event, fd); + self.epoll.add_fd(fd, event) + } + + pub(crate) fn del_socket(&mut self, event: T) -> Result<(), DhcpError> { + if let Some(fd) = self.socket_fds.remove(&event) { + self.epoll.del_fd(fd)?; + } + Ok(()) + } + + pub(crate) fn add_timer( + &mut self, + timeout: Duration, + event: T, + ) -> Result<(), DhcpError> { + log::debug!( + "Adding timer {} milliseconds with event {} to event pool", + timeout.as_millis(), + event + ); + let timer_fd = DhcpTimerFd::new(timeout)?; + self.epoll.add_fd(timer_fd.as_raw_fd(), event)?; + self.timer_fds.insert(event, timer_fd); + Ok(()) + } + + pub(crate) fn del_timer(&mut self, event: T) -> Result<(), DhcpError> { + if let Some(timer_fd) = self.timer_fds.remove(&event) { + self.epoll.del_fd(timer_fd.as_raw_fd())?; + } + Ok(()) + } + + pub(crate) fn poll(&self, wait_time: u32) -> Result, DhcpError> { + match isize::try_from(wait_time) { + Ok(i) => self.epoll.poll(i), + Err(_) => Err(DhcpError::new( + ErrorKind::InvalidArgument, + format!( + "Invalid timeout, should be in the range of 0 - {}", + isize::MAX + ), + )), + } + } +} diff --git a/src/integ_tests/dhcpv6.rs b/src/integ_tests/dhcpv6.rs new file mode 100644 index 0000000..924dfb8 --- /dev/null +++ b/src/integ_tests/dhcpv6.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 + +use crate::{DhcpV6Client, DhcpV6Config, DhcpV6IaType, DhcpV6Lease}; + +use super::env::{with_dhcp_env, FOO1_STATIC_IPV6, TEST_NIC_CLI}; + +const POLL_WAIT_TIME: u32 = 5; + +#[test] +fn test_dhcpv6_use_default_client_id() { + with_dhcp_env(|| { + let config = DhcpV6Config::new( + TEST_NIC_CLI, + DhcpV6IaType::NonTemporaryAddresses, + ); + let mut cli = DhcpV6Client::init(config, None).unwrap(); + + let lease = get_lease(&mut cli); + println!("Got lease {:?}", lease); + assert!(lease.is_some()); + if let Some(lease) = lease { + assert_eq!(lease.addr, FOO1_STATIC_IPV6); + } + }) +} + +fn get_lease(cli: &mut DhcpV6Client) -> Option { + while let Ok(events) = cli.poll(POLL_WAIT_TIME) { + for event in events { + match cli.process(event) { + Ok(Some(lease)) => { + return Some(lease); + } + Ok(None) => (), + Err(_) => { + return None; + } + } + } + } + None +} diff --git a/src/integ_tests/dhcpv6_async.rs b/src/integ_tests/dhcpv6_async.rs new file mode 100644 index 0000000..f8fa205 --- /dev/null +++ b/src/integ_tests/dhcpv6_async.rs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 + +use futures::StreamExt; + +use crate::{DhcpV6ClientAsync, DhcpV6Config, DhcpV6IaType, DhcpV6Lease}; + +use super::env::{with_dhcp_env, FOO1_STATIC_IPV6, TEST_NIC_CLI}; + +#[test] +fn test_dhcpv6_async() { + with_dhcp_env(|| { + let config = DhcpV6Config::new( + TEST_NIC_CLI, + DhcpV6IaType::NonTemporaryAddresses, + ); + + let mut cli = DhcpV6ClientAsync::init(config, None).unwrap(); + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .build() + .unwrap(); + + let lease = rt.block_on(get_lease(&mut cli)); + assert!(lease.is_some()); + if let Some(lease) = lease { + // If the client id was set correctly to FOO1_HOSTNAME via the + // call to use_host_name_as_client_id(), then the server should + // return FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID. + assert_eq!(lease.addr, FOO1_STATIC_IPV6); + } + }) +} + +async fn get_lease(cli: &mut DhcpV6ClientAsync) -> Option { + cli.next().await.unwrap().ok() +} diff --git a/src/integ_tests/env.rs b/src/integ_tests/env.rs index 6f427c8..bdea32d 100644 --- a/src/integ_tests/env.rs +++ b/src/integ_tests/env.rs @@ -1,27 +1,31 @@ // SPDX-License-Identifier: Apache-2.0 use std::io::Read; +use std::net::{Ipv4Addr, Ipv6Addr}; use std::process::Command; use std::str::FromStr; const PID_FILE_PATH: &str = "/tmp/mozim_test_dnsmasq_pid"; const TEST_DHCPD_NETNS: &str = "mozim_test"; +const LOG_FILE: &str = "/tmp/mozim_test_dnsmasq_log"; pub(crate) const TEST_NIC_CLI: &str = "dhcpcli"; +const TEST_NIC_CLI_MAC: &str = "00:23:45:67:89:1a"; pub(crate) const TEST_PROXY_MAC1: &str = "00:11:22:33:44:55"; const TEST_NIC_SRV: &str = "dhcpsrv"; const TEST_DHCP_SRV_IP: &str = "192.0.2.1"; +const TEST_DHCP_SRV_IPV6: &str = "2001:db8:a::1"; pub(crate) const FOO1_HOSTNAME: &str = "foo1"; pub(crate) const FOO1_CLIENT_ID: &str = "0123456789123456012345678912345601234567891234560123456789123456"; -pub(crate) const FOO1_STATIC_IP: std::net::Ipv4Addr = - std::net::Ipv4Addr::new(192, 0, 2, 99); -pub(crate) const FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID: std::net::Ipv4Addr = - std::net::Ipv4Addr::new(192, 0, 2, 96); -pub(crate) const TEST_PROXY_IP1: std::net::Ipv4Addr = - std::net::Ipv4Addr::new(192, 0, 2, 51); +pub(crate) const FOO1_STATIC_IP: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 99); +pub(crate) const FOO1_STATIC_IPV6: Ipv6Addr = + Ipv6Addr::new(0x2001, 0xdb8, 0xa, 0x0, 0x0, 0x0, 0x0, 0x99); +pub(crate) const FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID: Ipv4Addr = + Ipv4Addr::new(192, 0, 2, 96); +pub(crate) const TEST_PROXY_IP1: Ipv4Addr = Ipv4Addr::new(192, 0, 2, 51); fn create_test_net_namespace() { run_cmd(&format!("ip netns add {TEST_DHCPD_NETNS}")); @@ -33,7 +37,8 @@ fn remove_test_net_namespace() { fn create_test_veth_nics() { run_cmd(&format!( - "ip link add {TEST_NIC_CLI} type veth peer name {TEST_NIC_SRV}" + "ip link add {TEST_NIC_CLI} \ + address {TEST_NIC_CLI_MAC} type veth peer name {TEST_NIC_SRV}" )); run_cmd(&format!("ip link set {TEST_NIC_CLI} up")); run_cmd(&format!( @@ -46,6 +51,12 @@ fn create_test_veth_nics() { "ip netns exec {TEST_DHCPD_NETNS} \ ip addr add {TEST_DHCP_SRV_IP}/24 dev {TEST_NIC_SRV}", )); + run_cmd(&format!( + "ip netns exec {TEST_DHCPD_NETNS} \ + ip addr add {TEST_DHCP_SRV_IPV6}/64 dev {TEST_NIC_SRV}", + )); + // Need to wait 2 seconds for IPv6 duplicate address detection + std::thread::sleep(std::time::Duration::from_secs(2)); } fn remove_test_veth_nics() { @@ -53,14 +64,22 @@ fn remove_test_veth_nics() { } fn start_dhcp_server() { + run_cmd(&format!("rm {LOG_FILE}")); + run_cmd(&format!("touch {LOG_FILE}")); + run_cmd(&format!("chmod 666 {LOG_FILE}")); + let dnsmasq_opts = format!( r#" --pid-file={PID_FILE_PATH} + --log-queries --log-dhcp + --log-debug + --log-facility=/tmp/mozim_test_dnsmasq_log --conf-file=/dev/null --dhcp-leasefile=/tmp/mozim_test_dhcpd_lease --no-hosts --dhcp-host=id:{FOO1_CLIENT_ID},{FOO1_STATIC_IP},{FOO1_HOSTNAME} + --dhcp-host=id:00:03:00:01:{TEST_NIC_CLI_MAC},[{FOO1_STATIC_IPV6}],{FOO1_HOSTNAME} --dhcp-host=id:{FOO1_HOSTNAME},{FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID} --dhcp-host={TEST_PROXY_MAC1},{TEST_PROXY_IP1} --dhcp-option=option:dns-server,8.8.8.8,1.1.1.1 @@ -72,6 +91,7 @@ fn start_dhcp_server() { --clear-on-reload --interface=dhcpsrv --dhcp-range=192.0.2.2,192.0.2.50,60 + --dhcp-range=2001:db8:a::2,2001:db8:a::ff,64,2m --no-ping "# ); @@ -89,9 +109,14 @@ fn start_dhcp_server() { .expect("Failed to start DHCP server") .wait() .ok(); + // Need to wait 1 seconds for dnsmasq to finish its start + std::thread::sleep(std::time::Duration::from_secs(1)); } fn stop_dhcp_server() { + if !std::path::Path::new(PID_FILE_PATH).exists() { + return; + } let mut fd = std::fs::File::open(PID_FILE_PATH) .unwrap_or_else(|_| panic!("Failed to open {PID_FILE_PATH} file")); let mut contents = String::new(); @@ -134,6 +159,7 @@ where { create_test_net_namespace(); create_test_veth_nics(); + stop_dhcp_server(); start_dhcp_server(); let result = std::panic::catch_unwind(|| { diff --git a/src/integ_tests/mod.rs b/src/integ_tests/mod.rs index d1cc8e3..41cd5f1 100644 --- a/src/integ_tests/mod.rs +++ b/src/integ_tests/mod.rs @@ -6,5 +6,9 @@ mod dhcpv4; mod dhcpv4_async; #[cfg(test)] mod dhcpv4_proxy; +#[cfg(test)] +mod dhcpv6; +#[cfg(test)] +mod dhcpv6_async; mod env; diff --git a/src/lib.rs b/src/lib.rs index 62557b0..624e11a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,14 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 mod bpf; -mod client; mod client_async; -mod config; +mod dhcpv4; +mod dhcpv6; mod error; mod event; -mod lease; mod mac; -mod msg; +mod nispor; mod proiscuous; mod socket; mod time; @@ -16,10 +15,14 @@ mod time; #[cfg(test)] mod integ_tests; -pub use crate::client::DhcpV4Client; -pub use crate::client_async::DhcpV4ClientAsync; -pub use crate::config::DhcpV4Config; +pub use crate::client_async::{DhcpV4ClientAsync, DhcpV6ClientAsync}; +pub use crate::dhcpv4::{ + DhcpV4Client, DhcpV4Config, DhcpV4Event, DhcpV4Lease, DhcpV4Message, + DhcpV4MessageType, +}; +pub use crate::dhcpv6::{ + DhcpV6Client, DhcpV6Config, DhcpV6Event, DhcpV6IaType, DhcpV6Lease, + DhcpV6Message, Dhcpv6Duid, Dhcpv6DuidEn, Dhcpv6DuidLl, Dhcpv6DuidLlt, + Dhcpv6DuidUuid, +}; pub use crate::error::{DhcpError, ErrorKind}; -pub use crate::event::DhcpV4Event; -pub use crate::lease::DhcpV4Lease; -pub use crate::msg::{DhcpV4Message, DhcpV4MessageType}; diff --git a/src/nispor.rs b/src/nispor.rs new file mode 100644 index 0000000..0d77d24 --- /dev/null +++ b/src/nispor.rs @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::net::Ipv6Addr; +use std::str::FromStr; + +use nispor::{Ipv6AddrFlag, NetState, NetStateFilter, NetStateIfaceFilter}; + +use crate::{DhcpError, ErrorKind}; + +// We use thread to invoke nispor which has `tokio::block_on` which +// stop our async usage +pub(crate) fn get_nispor_iface( + iface_name: &str, + with_ip: bool, +) -> Result { + let iface_name = iface_name.to_string(); + match std::thread::spawn(move || { + if iface_name.is_empty() { + let e = DhcpError::new( + ErrorKind::InvalidArgument, + "Interface name not defined".to_string(), + ); + log::error!("{}", e); + return Err(e); + } + let mut filter = NetStateFilter::minimum(); + let mut iface_filter = NetStateIfaceFilter::minimum(); + iface_filter.iface_name = Some(iface_name.to_string()); + iface_filter.include_ip_address = with_ip; + filter.iface = Some(iface_filter); + + let net_state = match NetState::retrieve_with_filter(&filter) { + Ok(s) => s, + Err(e) => { + return Err(DhcpError::new( + ErrorKind::Bug, + format!("Failed to retrieve network state: {e}"), + )) + } + }; + if let Some(iface) = net_state.ifaces.get(iface_name.as_str()) { + Ok(iface.clone()) + } else { + Err(DhcpError::new( + ErrorKind::InvalidArgument, + format!("Interface {iface_name} not found"), + )) + } + }) + .join() + { + Ok(n) => Ok(n?), + Err(e) => Err(DhcpError::new( + ErrorKind::Bug, + format!("Failed to invoke nispor thread: {e:?}"), + )), + } +} + +// Search link-local address or global address: +// * prefer link-local address over global +// * Not allow address with tentative flag. +pub(crate) fn get_ipv6_addr_of_iface( + iface: &nispor::Iface, +) -> Result { + if let Some(addrs) = iface.ipv6.as_ref().map(|i| i.addresses.as_slice()) { + if let Some(addr) = addrs + .iter() + .filter_map(|a| { + if !a.flags.contains(&Ipv6AddrFlag::Tentative) { + Ipv6Addr::from_str(a.address.as_str()).ok() + } else { + None + } + }) + .find(is_ipv6_unicast_link_local) + .or_else(|| { + addrs + .iter() + .filter_map(|a| { + if !a.flags.contains(&Ipv6AddrFlag::Tentative) { + Ipv6Addr::from_str(a.address.as_str()).ok() + } else { + None + } + }) + .find(is_ipv6_unicast) + }) + { + Ok(addr) + } else { + Err(DhcpError::new( + ErrorKind::InvalidArgument, + format!( + "Failed to find unicast IPv6 address on \ + interface {} which is required for DHCPv6", + iface.name + ), + )) + } + } else { + Err(DhcpError::new( + ErrorKind::InvalidArgument, + format!( + "Interface {} has no IPv6 address to start DHCPv6", + iface.name + ), + )) + } +} + +// Copy from Rust official std::net::Ipv6Addr::is_unicast_link_local() which +// is experimental. +fn is_ipv6_unicast_link_local(ip: &Ipv6Addr) -> bool { + (ip.segments()[0] & 0xffc0) == 0xfe80 +} + +// Copy from Rust official std::net::Ipv6Addr::is_multicast() which is +// experimental. +fn is_ipv6_unicast(ip: &Ipv6Addr) -> bool { + (ip.segments()[0] & 0xff00) != 0xff00 +} diff --git a/src/socket.rs b/src/socket.rs index 357acdd..0a1e1e2 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 use std::ffi::CString; -use std::net::{Ipv4Addr, UdpSocket}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV6, UdpSocket}; use std::os::unix::io::AsRawFd; use std::os::unix::io::RawFd; @@ -14,6 +14,8 @@ use crate::{ DhcpError, DhcpV4Config, ErrorKind, }; +pub(crate) const DEFAULT_SOCKET_TIMEOUT: u32 = 5; + const PACKET_HOST: u8 = 0; // a packet addressed to the local host pub(crate) trait DhcpSocket { @@ -164,8 +166,8 @@ impl DhcpSocket for DhcpRawSocket { return Err(e); } log::debug!("Raw socket received {:?}", &buffer[..rc as usize]); + Ok(buffer[..rc as usize].to_vec()) } - Ok(buffer.to_vec()) } } @@ -264,6 +266,40 @@ impl DhcpUdpSocket { Ok(Self { socket }) } + + pub(crate) fn new_v6( + iface_index: u32, + src_ip: &Ipv6Addr, + socket_timeout: u32, + ) -> Result { + let socket = UdpSocket::bind(SocketAddrV6::new( + *src_ip, + dhcproto::v6::CLIENT_PORT, + 0, + iface_index, + ))?; + log::debug!("UDP socket bind to {:?}", socket); + socket.set_read_timeout(Some(std::time::Duration::from_secs( + socket_timeout.into(), + )))?; + socket.set_write_timeout(Some(std::time::Duration::from_secs( + socket_timeout.into(), + )))?; + + Ok(Self { socket }) + } + + pub(crate) fn send_to_v6( + &self, + dst_ip: &Ipv6Addr, + buff: &[u8], + ) -> Result<(), DhcpError> { + self.socket.send_to( + buff, + SocketAddrV6::new(*dst_ip, dhcproto::v6::SERVER_PORT, 0, 0), + )?; + Ok(()) + } } impl DhcpSocket for DhcpUdpSocket { @@ -279,8 +315,8 @@ impl DhcpSocket for DhcpUdpSocket { fn recv(&self) -> Result, DhcpError> { // TODO: Add support of `Maximum DHCP Message Size` option let mut buffer = [0u8; 1500]; - self.socket.recv(&mut buffer)?; - Ok(buffer.to_vec()) + let received = self.socket.recv(&mut buffer)?; + Ok(buffer[..received].to_vec()) } } diff --git a/src/time.rs b/src/time.rs index 41a0b73..d69e985 100644 --- a/src/time.rs +++ b/src/time.rs @@ -4,11 +4,10 @@ use std::os::fd::AsFd; use std::os::unix::io::{AsRawFd, RawFd}; use std::time::Duration; -use nix::sys::time::{TimeSpec, TimeValLike}; +use nix::sys::time::TimeSpec; use nix::sys::timerfd::{ ClockId::CLOCK_BOOTTIME, Expiration, TimerFd, TimerFlags, TimerSetTimeFlags, }; -use rand::Rng; use crate::{DhcpError, ErrorKind}; @@ -24,7 +23,7 @@ impl AsRawFd for DhcpTimerFd { } impl DhcpTimerFd { - pub(crate) fn new(time: u32) -> Result { + pub(crate) fn new(time: Duration) -> Result { let fd = TimerFd::new(CLOCK_BOOTTIME, TimerFlags::empty()).map_err(|e| { let e = DhcpError::new( @@ -35,7 +34,7 @@ impl DhcpTimerFd { e })?; fd.set( - Expiration::OneShot(TimeSpec::seconds(time.into())), + Expiration::OneShot(TimeSpec::from_duration(time)), TimerSetTimeFlags::empty(), ) .map_err(|e| { @@ -46,30 +45,11 @@ impl DhcpTimerFd { log::error!("{}", e); e })?; - log::debug!("TimerFd created {:?} with {} seconds", fd, time); + log::debug!( + "TimerFd created {:?} with {} milliseconds", + fd, + time.as_millis() + ); Ok(Self { fd }) } } - -// The T1/T2 randomization is done by server side according to RFC 2131: -// Times T1 and T2 SHOULD be chosen with some random "fuzz" around a fixed -// value, to avoid synchronization of client reacquisition. -pub(crate) fn gen_renew_rebind_times(t1: u32, t2: u32, lease: u32) -> [u32; 4] { - [t1, t1 + (t2 - t1) / 2, t2, t2 + (lease - t2) / 2] -} - -// RFC 2131, section 4.1 "Constructing and sending DHCP messages" has -// retransmission guideline. -// It should be starting with 4 seconds and double of previous delay, up to 64 -// seconds. Delay should be randomized from range -1 to 1; -pub(crate) fn gen_dhcp_request_delay(retry_count: u32) -> u32 { - let mut base = 2u64.pow(retry_count + 2) - 1; - if base > 62 { - base = 62; - } - let ms: u64 = rand::thread_rng().gen_range(0..2000); - (Duration::from_secs(base) + Duration::from_millis(ms)) - .as_secs() - .try_into() - .unwrap_or(u32::MAX) -}