diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0a0a5..1f44148 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -No unreleased changes yet +### New features +* dhcp: Updated DHCP client to respect lease times provided by the server. ## [0.7.0] - 2021-01-20 diff --git a/src/dhcp/clientv4.rs b/src/dhcp/clientv4.rs index a2d3342..30efa8e 100644 --- a/src/dhcp/clientv4.rs +++ b/src/dhcp/clientv4.rs @@ -13,8 +13,7 @@ use super::{UDP_SERVER_PORT, UDP_CLIENT_PORT}; const DISCOVER_TIMEOUT: u64 = 10; const REQUEST_TIMEOUT: u64 = 1; const REQUEST_RETRIES: u16 = 15; -const RENEW_INTERVAL: u64 = 60; -const RENEW_RETRIES: u16 = 3; +const DEFAULT_RENEW_INTERVAL: u32 = 60; const PARAMETER_REQUEST_LIST: &[u8] = &[ dhcpv4_field::OPT_SUBNET_MASK, dhcpv4_field::OPT_ROUTER, @@ -39,7 +38,6 @@ struct RequestState { #[derive(Debug)] struct RenewState { - retry: u16, endpoint_ip: Ipv4Address, server_identifier: Ipv4Address, } @@ -59,6 +57,8 @@ pub struct Client { raw_handle: SocketHandle, /// When to send next request next_egress: Instant, + /// When any existing DHCP address will expire. + lease_expiration: Option, transaction_id: u32, } @@ -104,6 +104,7 @@ impl Client { raw_handle, next_egress: now, transaction_id: 1, + lease_expiration: None, } } @@ -199,12 +200,18 @@ impl Client { // once we receive the ack, we can pass the config to the user let config = if dhcp_repr.message_type == DhcpMessageType::Ack { - let address = dhcp_repr.subnet_mask - .and_then(|mask| IpAddress::Ipv4(mask).to_prefix_len()) - .map(|prefix_len| Ipv4Cidr::new(dhcp_repr.your_ip, prefix_len)); - let router = dhcp_repr.router; - let dns_servers = dhcp_repr.dns_servers - .unwrap_or([None; 3]); + let lease_duration = dhcp_repr.lease_duration.unwrap_or(DEFAULT_RENEW_INTERVAL * 2); + self.lease_expiration = Some(now + Duration::from_secs(lease_duration.into())); + + // RFC 2131 indicates clients should renew a lease halfway through its expiration. + self.next_egress = now + Duration::from_secs((lease_duration / 2).into()); + + let address = dhcp_repr.subnet_mask + .and_then(|mask| IpAddress::Ipv4(mask).to_prefix_len()) + .map(|prefix_len| Ipv4Cidr::new(dhcp_repr.your_ip, prefix_len)); + let router = dhcp_repr.router; + let dns_servers = dhcp_repr.dns_servers + .unwrap_or([None; 3]); Some(Config { address, router, dns_servers }) } else { None @@ -227,22 +234,12 @@ impl Client { if dhcp_repr.message_type == DhcpMessageType::Ack && server_identifier == r_state.server_identifier => { - self.next_egress = now + Duration::from_secs(RENEW_INTERVAL); let p_state = RenewState { - retry: 0, endpoint_ip: *src_ip, server_identifier, }; Some(ClientState::Renew(p_state)) } - ClientState::Renew(ref mut p_state) - if dhcp_repr.message_type == DhcpMessageType::Ack && - server_identifier == p_state.server_identifier => - { - self.next_egress = now + Duration::from_secs(RENEW_INTERVAL); - p_state.retry = 0; - None - } _ => None }.map(|new_state| self.state = new_state); @@ -256,13 +253,12 @@ impl Client { net_debug!("DHCP request retries exceeded, restarting discovery"); true } - ClientState::Renew(ref mut r_state) if r_state.retry >= RENEW_RETRIES => { - net_debug!("DHCP renew retries exceeded, restarting discovery"); - true - } _ => false }; - if retries_exceeded { + + let lease_expired = self.lease_expiration.map_or(false, |expiration| now >= expiration); + + if lease_expired || retries_exceeded { self.reset(now); // Return a config now so that user code assigns the // 0.0.0.0/0 address, which will be used sending a DHCP @@ -294,6 +290,7 @@ impl Client { server_identifier: None, parameter_request_list: None, max_size: Some(raw_socket.payload_recv_capacity() as u16), + lease_duration: None, dns_servers: None, }; let mut send_packet = |iface, endpoint, dhcp_repr| { @@ -329,8 +326,7 @@ impl Client { send_packet(iface, endpoint, dhcp_repr) } ClientState::Renew(ref mut p_state) => { - p_state.retry += 1; - self.next_egress = now + Duration::from_secs(RENEW_INTERVAL); + self.next_egress = now + Duration::from_secs(DEFAULT_RENEW_INTERVAL.into()); let endpoint = IpEndpoint { addr: p_state.endpoint_ip.into(), diff --git a/src/wire/dhcpv4.rs b/src/wire/dhcpv4.rs index 64e9a9c..37f6d94 100644 --- a/src/wire/dhcpv4.rs +++ b/src/wire/dhcpv4.rs @@ -50,6 +50,7 @@ pub enum DhcpOption<'a> { RequestedIp(Ipv4Address), ClientIdentifier(EthernetAddress), ServerIdentifier(Ipv4Address), + IpLeaseTime(u32), Router(Ipv4Address), SubnetMask(Ipv4Address), MaximumDhcpMessageSize(u16), @@ -103,6 +104,9 @@ impl<'a> DhcpOption<'a> { (field::OPT_MAX_DHCP_MESSAGE_SIZE, 2) => { option = DhcpOption::MaximumDhcpMessageSize(u16::from_be_bytes([data[0], data[1]])); } + (field::OPT_IP_LEASE_TIME, 4) => { + option = DhcpOption::IpLeaseTime(u32::from_be_bytes([data[0], data[1], data[2], data[3]])) + } (_, _) => { option = DhcpOption::Other { kind: kind, data: data }; } @@ -129,6 +133,7 @@ impl<'a> DhcpOption<'a> { &DhcpOption::MaximumDhcpMessageSize(_) => { 4 } + &DhcpOption::IpLeaseTime(_) => 6, &DhcpOption::Other { data, .. } => 2 + data.len() } } @@ -178,6 +183,10 @@ impl<'a> DhcpOption<'a> { buffer[0] = field::OPT_MAX_DHCP_MESSAGE_SIZE; buffer[2..4].copy_from_slice(&size.to_be_bytes()[..]); } + DhcpOption::IpLeaseTime(lease_time) => { + buffer[0] = field::OPT_IP_LEASE_TIME; + buffer[2..6].copy_from_slice(&lease_time.to_be_bytes()[..]); + } DhcpOption::Other { kind, data: provided } => { buffer[0] = kind; buffer[2..skip_length].copy_from_slice(provided); @@ -674,6 +683,8 @@ pub struct Repr<'a> { pub dns_servers: Option<[Option; 3]>, /// The maximum size dhcp packet the interface can receive pub max_size: Option, + /// The DHCP IP lease duration, specified in seconds. + pub lease_duration: Option } impl<'a> Repr<'a> { @@ -725,6 +736,7 @@ impl<'a> Repr<'a> { let mut parameter_request_list = None; let mut dns_servers = None; let mut max_size = None; + let mut lease_duration = None; let mut options = packet.options()?; while !options.is_empty() { @@ -755,6 +767,9 @@ impl<'a> Repr<'a> { DhcpOption::MaximumDhcpMessageSize(size) => { max_size = Some(size); } + DhcpOption::IpLeaseTime(duration) => { + lease_duration = Some(duration); + } DhcpOption::Other {kind: field::OPT_PARAMETER_REQUEST_LIST, data} => { parameter_request_list = Some(data); } @@ -776,6 +791,7 @@ impl<'a> Repr<'a> { transaction_id, client_hardware_address, client_ip, your_ip, server_ip, relay_agent_ip, broadcast, requested_ip, server_identifier, router, subnet_mask, client_identifier, parameter_request_list, dns_servers, max_size, + lease_duration, message_type: message_type?, }) } @@ -820,6 +836,9 @@ impl<'a> Repr<'a> { if let Some(size) = self.max_size { let tmp = options; options = DhcpOption::MaximumDhcpMessageSize(size).emit(tmp); } + if let Some(duration) = self.lease_duration { + let tmp = options; options = DhcpOption::IpLeaseTime(duration).emit(tmp); + } if let Some(list) = self.parameter_request_list { let option = DhcpOption::Other{ kind: field::OPT_PARAMETER_REQUEST_LIST, data: list }; let tmp = options; options = option.emit(tmp); @@ -858,7 +877,7 @@ mod test { 0x00, 0x00, 0x39, 0x2, 0x5, 0xdc, 0x37, 0x04, 0x01, 0x03, 0x06, 0x2a, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ]; - static ACK_BYTES: &[u8] = &[ + static ACK_DNS_SERVER_BYTES: &[u8] = &[ 0x02, 0x01, 0x06, 0x00, 0xcc, 0x34, 0x75, 0xab, 0x00, 0x00, 0x80, 0x00, 0x0a, 0xff, 0x06, 0x91, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xff, 0x06, 0xfe, 0x34, 0x17, 0xeb, 0xc9, 0xaa, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -882,6 +901,28 @@ mod test { 0x01, 0x4a, 0x06, 0xa3, 0x01, 0x4a, 0x07, 0x2e, 0x01, 0x08, 0xff ]; + static ACK_LEASE_TIME_BYTES: &[u8] = &[ + 0x02, 0x01, 0x06, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x0a, 0x22, 0x10, 0x0b, 0x0a, 0x22, 0x10, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x04, 0x91, 0x62, 0xd2, + 0xa8, 0x6f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x82, 0x53, 0x63, + 0x35, 0x01, 0x05, 0x36, 0x04, 0x0a, 0x22, 0x10, 0x0a, 0x33, 0x04, 0x00, 0x00, 0x02, 0x56, 0x01, + 0x04, 0xff, 0xff, 0xff, 0x00, 0x03, 0x04, 0x0a, 0x22, 0x10, 0x0a, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]; + const IP_NULL: Ipv4Address = Ipv4Address([0, 0, 0, 0]); const CLIENT_MAC: EthernetAddress = EthernetAddress([0x0, 0x0b, 0x82, 0x01, 0xfc, 0x42]); const DHCP_SIZE: u16 = 1500; @@ -984,6 +1025,7 @@ mod test { relay_agent_ip: IP_NULL, broadcast: false, max_size: Some(DHCP_SIZE), + lease_duration: None, requested_ip: Some(IP_NULL), client_identifier: Some(CLIENT_MAC), server_identifier: None, @@ -1031,8 +1073,9 @@ mod test { #[test] fn test_parse_ack_dns_servers() { - let packet = Packet::new_unchecked(ACK_BYTES); + let packet = Packet::new_unchecked(ACK_DNS_SERVER_BYTES); let repr = Repr::parse(&packet).unwrap(); + // The packet described by ACK_BYTES advertises 4 DNS servers // Here we ensure that we correctly parse the first 3 into our fixed // length-3 array (see issue #305) @@ -1041,4 +1084,14 @@ mod test { Some(Ipv4Address([163, 1, 74, 7])), Some(Ipv4Address([163, 1, 74, 3]))])); } + + #[test] + fn test_parse_ack_lease_duration() { + let packet = Packet::new_unchecked(ACK_LEASE_TIME_BYTES); + let repr = Repr::parse(&packet).unwrap(); + + // Verify that the lease time in the ACK is properly parsed. The packet contains a lease + // duration of 598s. + assert_eq!(repr.lease_duration, Some(598)); + } }