From 0d53163c556223c751cc1334361dd80115de848a Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Wed, 7 Apr 2021 01:31:53 +0200 Subject: [PATCH] dhcp: convert to socket --- Cargo.toml | 5 +- examples/dhcp_client.rs | 103 +++++----- src/dhcp/clientv4.rs | 422 ---------------------------------------- src/dhcp/mod.rs | 5 - src/iface/interface.rs | 62 +++++- src/lib.rs | 2 - src/socket/dhcpv4.rs | 422 ++++++++++++++++++++++++++++++++++++++++ src/socket/mod.rs | 11 ++ src/socket/ref_.rs | 18 +- src/socket/set.rs | 3 + src/wire/dhcpv4.rs | 3 + src/wire/mod.rs | 7 +- src/wire/udp.rs | 2 + 13 files changed, 555 insertions(+), 510 deletions(-) delete mode 100644 src/dhcp/clientv4.rs delete mode 100644 src/dhcp/mod.rs create mode 100644 src/socket/dhcpv4.rs diff --git a/Cargo.toml b/Cargo.toml index ae47165..c0d2166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,13 +39,14 @@ verbose = [] "phy-tuntap_interface" = ["std", "libc", "medium-ethernet"] "proto-ipv4" = [] "proto-igmp" = ["proto-ipv4"] -"proto-dhcpv4" = ["proto-ipv4", "socket-raw", "medium-ethernet"] +"proto-dhcpv4" = ["proto-ipv4"] "proto-ipv6" = [] "socket" = [] "socket-raw" = ["socket"] "socket-udp" = ["socket"] "socket-tcp" = ["socket"] "socket-icmp" = ["socket"] +"socket-dhcpv4" = ["socket", "medium-ethernet", "proto-dhcpv4"] "async" = [] defmt-trace = [] @@ -59,7 +60,7 @@ default = [ "medium-ethernet", "medium-ip", "phy-raw_socket", "phy-tuntap_interface", "proto-ipv4", "proto-igmp", "proto-dhcpv4", "proto-ipv6", - "socket-raw", "socket-icmp", "socket-udp", "socket-tcp", + "socket-raw", "socket-icmp", "socket-udp", "socket-tcp", "socket-dhcpv4", "async" ] diff --git a/examples/dhcp_client.rs b/examples/dhcp_client.rs index 81d685f..06b3080 100644 --- a/examples/dhcp_client.rs +++ b/examples/dhcp_client.rs @@ -3,12 +3,13 @@ mod utils; use std::collections::BTreeMap; use std::os::unix::io::AsRawFd; +use log::*; + use smoltcp::phy::{Device, Medium, wait as phy_wait}; use smoltcp::wire::{EthernetAddress, Ipv4Address, IpCidr, Ipv4Cidr}; -use smoltcp::iface::{NeighborCache, InterfaceBuilder, Routes}; -use smoltcp::socket::{SocketSet, RawSocketBuffer, RawPacketMetadata}; +use smoltcp::iface::{NeighborCache, InterfaceBuilder, Interface, Routes}; +use smoltcp::socket::{SocketSet, Dhcpv4Socket, Dhcpv4Event}; use smoltcp::time::Instant; -use smoltcp::dhcp::Dhcpv4Client; fn main() { #[cfg(feature = "log")] @@ -41,65 +42,53 @@ fn main() { let mut iface = builder.finalize(); let mut sockets = SocketSet::new(vec![]); - let dhcp_rx_buffer = RawSocketBuffer::new( - [RawPacketMetadata::EMPTY; 1], - vec![0; 900] - ); - let dhcp_tx_buffer = RawSocketBuffer::new( - [RawPacketMetadata::EMPTY; 1], - vec![0; 600] - ); - let mut dhcp = Dhcpv4Client::new(&mut sockets, dhcp_rx_buffer, dhcp_tx_buffer, Instant::now()); - let mut prev_cidr = Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0); + let dhcp_handle = sockets.add(Dhcpv4Socket::new()); + loop { let timestamp = Instant::now(); - iface.poll(&mut sockets, timestamp) - .map(|_| ()) - .unwrap_or_else(|e| println!("Poll: {:?}", e)); - let config = dhcp.poll(&mut iface, &mut sockets, timestamp) - .unwrap_or_else(|e| { - println!("DHCP: {:?}", e); - None - }); - config.map(|config| { - println!("DHCP config: {:?}", config); - if let Some(cidr) = config.address { - if cidr != prev_cidr { - iface.update_ip_addrs(|addrs| { - addrs.iter_mut().next() - .map(|addr| { - *addr = IpCidr::Ipv4(cidr); - }); - }); - prev_cidr = cidr; - println!("Assigned a new IPv4 address: {}", cidr); + if let Err(e) = iface.poll(&mut sockets, timestamp) { + debug!("poll error: {}", e); + } + + match sockets.get::(dhcp_handle).poll() { + Dhcpv4Event::NoChange => {} + Dhcpv4Event::Configured(config) => { + debug!("DHCP config acquired!"); + + debug!("IP address: {}", config.address); + set_ipv4_addr(&mut iface, config.address); + + if let Some(router) = config.router { + debug!("Default gateway: {}", router); + iface.routes_mut().add_default_ipv4_route(router).unwrap(); + } else { + debug!("Default gateway: None"); + iface.routes_mut().remove_default_ipv4_route(); + } + + for (i, s) in config.dns_servers.iter().enumerate() { + if let Some(s) = s { + debug!("DNS server {}: {}", i, s); + } } } - - config.router.map(|router| iface.routes_mut() - .add_default_ipv4_route(router) - .unwrap() - ); - iface.routes_mut() - .update(|routes_map| { - routes_map.get(&IpCidr::new(Ipv4Address::UNSPECIFIED.into(), 0)) - .map(|default_route| { - println!("Default gateway: {}", default_route.via_router); - }); - }); - - if config.dns_servers.iter().any(|s| s.is_some()) { - println!("DNS servers:"); - for dns_server in config.dns_servers.iter().filter_map(|s| *s) { - println!("- {}", dns_server); - } + Dhcpv4Event::Deconfigured => { + debug!("DHCP lost config!"); + set_ipv4_addr(&mut iface, Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0)); + iface.routes_mut().remove_default_ipv4_route(); } - }); + } - let mut timeout = dhcp.next_poll(timestamp); - iface.poll_delay(&sockets, timestamp) - .map(|sockets_timeout| timeout = sockets_timeout); - phy_wait(fd, Some(timeout)) - .unwrap_or_else(|e| println!("Wait: {:?}", e)); + phy_wait(fd, iface.poll_delay(&sockets, timestamp)).expect("wait error"); } } + +fn set_ipv4_addr(iface: &mut Interface<'_, DeviceT>, cidr: Ipv4Cidr) + where DeviceT: for<'d> Device<'d> +{ + iface.update_ip_addrs(|addrs| { + let dest = addrs.iter_mut().next().unwrap(); + *dest = IpCidr::Ipv4(cidr); + }); +} + diff --git a/src/dhcp/clientv4.rs b/src/dhcp/clientv4.rs deleted file mode 100644 index 1679413..0000000 --- a/src/dhcp/clientv4.rs +++ /dev/null @@ -1,422 +0,0 @@ -use crate::{Error, Result}; -use crate::wire::{IpVersion, IpProtocol, IpEndpoint, IpAddress, - Ipv4Cidr, Ipv4Address, Ipv4Packet, Ipv4Repr, - UdpPacket, UdpRepr, - DhcpPacket, DhcpRepr, DhcpMessageType}; -use crate::wire::dhcpv4::{field as dhcpv4_field, Packet as Dhcpv4Packet}; -use crate::socket::{SocketSet, SocketHandle, RawSocket, RawSocketBuffer}; -use crate::phy::{Device, ChecksumCapabilities}; -use crate::iface::Interface; -use crate::time::{Instant, Duration}; -use super::{UDP_SERVER_PORT, UDP_CLIENT_PORT}; - -const DISCOVER_TIMEOUT: u64 = 10; -const REQUEST_TIMEOUT: u64 = 1; -const REQUEST_RETRIES: u16 = 15; -const DEFAULT_RENEW_INTERVAL: u32 = 60; -const PARAMETER_REQUEST_LIST: &[u8] = &[ - dhcpv4_field::OPT_SUBNET_MASK, - dhcpv4_field::OPT_ROUTER, - dhcpv4_field::OPT_DOMAIN_NAME_SERVER, -]; - -/// IPv4 configuration data returned by `client.poll()` -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Config { - pub address: Option, - pub router: Option, - pub dns_servers: [Option; 3], -} - -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -struct RequestState { - retry: u16, - endpoint_ip: Ipv4Address, - server_identifier: Ipv4Address, - requested_ip: Ipv4Address, -} - -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -struct RenewState { - endpoint_ip: Ipv4Address, - server_identifier: Ipv4Address, -} - -#[derive(Debug)] -#[cfg_attr(feature = "defmt", derive(defmt::Format))] -enum ClientState { - /// Discovering the DHCP server - Discovering, - /// Requesting an address - Requesting(RequestState), - /// Having an address, refresh it periodically - Renew(RenewState), -} - -pub struct Client { - state: ClientState, - raw_handle: SocketHandle, - /// When to send next request - next_egress: Instant, - /// When any existing DHCP address will expire. - lease_expiration: Option, - transaction_id: u32, -} - -/// DHCP client with a RawSocket. -/// -/// To provide memory for the dynamic IP address, configure your -/// `Interface` with one of `ip_addrs` and the `ipv4_gateway` being -/// `Ipv4Address::UNSPECIFIED`. You must also assign this `0.0.0.0/0` -/// while the client's state is `Discovering`. Hence, the `poll()` -/// method returns a corresponding `Config` struct in this case. -/// -/// You must call `dhcp_client.poll()` after `iface.poll()` to send -/// and receive DHCP packets. -impl Client { - /// # Usage - /// ```rust - /// use smoltcp::socket::{SocketSet, RawSocketBuffer, RawPacketMetadata}; - /// use smoltcp::dhcp::Dhcpv4Client; - /// use smoltcp::time::Instant; - /// - /// let mut sockets = SocketSet::new(vec![]); - /// let dhcp_rx_buffer = RawSocketBuffer::new( - /// [RawPacketMetadata::EMPTY; 1], - /// vec![0; 600] - /// ); - /// let dhcp_tx_buffer = RawSocketBuffer::new( - /// [RawPacketMetadata::EMPTY; 1], - /// vec![0; 600] - /// ); - /// let mut dhcp = Dhcpv4Client::new( - /// &mut sockets, - /// dhcp_rx_buffer, dhcp_tx_buffer, - /// Instant::now() - /// ); - /// ``` - pub fn new<'a>(sockets: &mut SocketSet<'a>, rx_buffer: RawSocketBuffer<'a>, tx_buffer: RawSocketBuffer<'a>, now: Instant) -> Self - { - let raw_socket = RawSocket::new(IpVersion::Ipv4, IpProtocol::Udp, rx_buffer, tx_buffer); - let raw_handle = sockets.add(raw_socket); - - Client { - state: ClientState::Discovering, - raw_handle, - next_egress: now, - transaction_id: 1, - lease_expiration: None, - } - } - - /// When to send next packet - /// - /// Useful for suspending execution after polling. - pub fn next_poll(&self, now: Instant) -> Duration { - self.next_egress - now - } - - /// Process incoming packets on the contained RawSocket, and send - /// DHCP requests when timeouts are ready. - /// - /// Applying the obtained network configuration is left to the - /// user. - /// - /// A Config can be returned from any valid DHCP reply. The client - /// performs no bookkeeping on configuration or their changes. - pub fn poll(&mut self, - iface: &mut Interface, sockets: &mut SocketSet, - now: Instant - ) -> Result> - where - DeviceT: for<'d> Device<'d>, - { - let checksum_caps = iface.device().capabilities().checksum; - let mut raw_socket = sockets.get::(self.raw_handle); - - // Process incoming - let config = { - match raw_socket.recv() - .and_then(|packet| parse_udp(packet, &checksum_caps)) { - Ok((IpEndpoint { - addr: IpAddress::Ipv4(src_ip), - port: UDP_SERVER_PORT, - }, IpEndpoint { - addr: _, - port: UDP_CLIENT_PORT, - }, payload)) => - self.ingress(iface, now, payload, &src_ip), - Ok(_) => - return Err(Error::Unrecognized), - Err(Error::Exhausted) => - None, - Err(e) => - return Err(e), - } - }; - - if config.is_some() { - // Return a new config immediately so that addresses can - // be configured that are required by egress(). - Ok(config) - } else { - // Send requests - if raw_socket.can_send() && now >= self.next_egress { - self.egress(iface, &mut *raw_socket, &checksum_caps, now) - } else { - Ok(None) - } - } - } - - fn ingress(&mut self, - iface: &mut Interface, now: Instant, - data: &[u8], src_ip: &Ipv4Address - ) -> Option - where - DeviceT: for<'d> Device<'d>, - { - let dhcp_packet = match DhcpPacket::new_checked(data) { - Ok(dhcp_packet) => dhcp_packet, - Err(e) => { - net_debug!("DHCP invalid pkt from {}: {:?}", src_ip, e); - return None; - } - }; - let dhcp_repr = match DhcpRepr::parse(&dhcp_packet) { - Ok(dhcp_repr) => dhcp_repr, - Err(e) => { - net_debug!("DHCP error parsing pkt from {}: {:?}", src_ip, e); - return None; - } - }; - let mac = iface.ethernet_addr(); - if dhcp_repr.client_hardware_address != mac { return None } - if dhcp_repr.transaction_id != self.transaction_id { return None } - let server_identifier = match dhcp_repr.server_identifier { - Some(server_identifier) => server_identifier, - None => return None, - }; - net_debug!("DHCP recv {:?} from {} ({})", dhcp_repr.message_type, src_ip, server_identifier); - - // once we receive the ack, we can pass the config to the user - let config = if dhcp_repr.message_type == DhcpMessageType::Ack { - 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 - }; - - match self.state { - ClientState::Discovering - if dhcp_repr.message_type == DhcpMessageType::Offer => - { - self.next_egress = now; - let r_state = RequestState { - retry: 0, - endpoint_ip: *src_ip, - server_identifier, - requested_ip: dhcp_repr.your_ip // use the offered ip - }; - Some(ClientState::Requesting(r_state)) - } - ClientState::Requesting(ref r_state) - if dhcp_repr.message_type == DhcpMessageType::Ack && - server_identifier == r_state.server_identifier => - { - let p_state = RenewState { - endpoint_ip: *src_ip, - server_identifier, - }; - Some(ClientState::Renew(p_state)) - } - _ => None - }.map(|new_state| self.state = new_state); - - config - } - - fn egress Device<'d>>(&mut self, iface: &mut Interface, raw_socket: &mut RawSocket, checksum_caps: &ChecksumCapabilities, now: Instant) -> Result> { - // Reset after maximum amount of retries - let retries_exceeded = match self.state { - ClientState::Requesting(ref mut r_state) if r_state.retry >= REQUEST_RETRIES => { - net_debug!("DHCP request retries exceeded, restarting discovery"); - true - } - _ => false - }; - - 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 - // discovery packet in the next call to egress(). - return Ok(Some(Config { - address: Some(Ipv4Cidr::new(Ipv4Address::UNSPECIFIED, 0)), - router: None, - dns_servers: [None; 3], - })); - } - - // Prepare sending next packet - self.transaction_id += 1; - let mac = iface.ethernet_addr(); - - let mut dhcp_repr = DhcpRepr { - message_type: DhcpMessageType::Discover, - transaction_id: self.transaction_id, - client_hardware_address: mac, - client_ip: Ipv4Address::UNSPECIFIED, - your_ip: Ipv4Address::UNSPECIFIED, - server_ip: Ipv4Address::UNSPECIFIED, - router: None, - subnet_mask: None, - relay_agent_ip: Ipv4Address::UNSPECIFIED, - broadcast: true, - requested_ip: None, - client_identifier: Some(mac), - server_identifier: None, - parameter_request_list: Some(PARAMETER_REQUEST_LIST), - max_size: Some(raw_socket.payload_recv_capacity() as u16), - lease_duration: None, - dns_servers: None, - }; - let mut send_packet = |iface, endpoint, dhcp_repr| { - send_packet(iface, raw_socket, &endpoint, &dhcp_repr, checksum_caps) - .map(|()| None) - }; - - - match self.state { - ClientState::Discovering => { - self.next_egress = now + Duration::from_secs(DISCOVER_TIMEOUT); - let endpoint = IpEndpoint { - addr: Ipv4Address::BROADCAST.into(), - port: UDP_SERVER_PORT, - }; - net_trace!("DHCP send discover to {}: {:?}", endpoint, dhcp_repr); - send_packet(iface, endpoint, dhcp_repr) - } - ClientState::Requesting(ref mut r_state) => { - r_state.retry += 1; - self.next_egress = now + Duration::from_secs(REQUEST_TIMEOUT); - - let endpoint = IpEndpoint { - addr: Ipv4Address::BROADCAST.into(), - port: UDP_SERVER_PORT, - }; - dhcp_repr.message_type = DhcpMessageType::Request; - dhcp_repr.broadcast = false; - dhcp_repr.requested_ip = Some(r_state.requested_ip); - dhcp_repr.server_identifier = Some(r_state.server_identifier); - net_trace!("DHCP send request to {} = {:?}", endpoint, dhcp_repr); - send_packet(iface, endpoint, dhcp_repr) - } - ClientState::Renew(ref mut p_state) => { - self.next_egress = now + Duration::from_secs(DEFAULT_RENEW_INTERVAL.into()); - - let endpoint = IpEndpoint { - addr: p_state.endpoint_ip.into(), - port: UDP_SERVER_PORT, - }; - let client_ip = iface.ipv4_addr().unwrap_or(Ipv4Address::UNSPECIFIED); - dhcp_repr.message_type = DhcpMessageType::Request; - dhcp_repr.client_ip = client_ip; - dhcp_repr.broadcast = false; - net_trace!("DHCP send renew to {}: {:?}", endpoint, dhcp_repr); - send_packet(iface, endpoint, dhcp_repr) - } - } - } - - /// Reset state and restart discovery phase. - /// - /// Use this to speed up acquisition of an address in a new - /// network if a link was down and it is now back up. - /// - /// You *must* configure a `0.0.0.0` address on your interface - /// before the next call to `poll()`! - pub fn reset(&mut self, now: Instant) { - net_trace!("DHCP reset"); - self.state = ClientState::Discovering; - self.next_egress = now; - self.lease_expiration = None; - } -} - -fn send_packet Device<'d>>(iface: &mut Interface, raw_socket: &mut RawSocket, endpoint: &IpEndpoint, dhcp_repr: &DhcpRepr, checksum_caps: &ChecksumCapabilities) -> Result<()> { - - let udp_repr = UdpRepr { - src_port: UDP_CLIENT_PORT, - dst_port: endpoint.port, - }; - - let src_addr = iface.ipv4_addr().unwrap(); - let dst_addr = match endpoint.addr { - IpAddress::Ipv4(addr) => addr, - _ => return Err(Error::Illegal), - }; - let ipv4_repr = Ipv4Repr { - src_addr, - dst_addr, - protocol: IpProtocol::Udp, - payload_len: udp_repr.header_len() + dhcp_repr.buffer_len(), - hop_limit: 64, - }; - - let mut packet = raw_socket.send( - ipv4_repr.buffer_len() + udp_repr.header_len() + dhcp_repr.buffer_len() - )?; - { - let mut ipv4_packet = Ipv4Packet::new_unchecked(&mut packet); - ipv4_repr.emit(&mut ipv4_packet, &checksum_caps); - } - { - let mut udp_packet = UdpPacket::new_unchecked( - &mut packet[ipv4_repr.buffer_len()..] - ); - udp_repr.emit(&mut udp_packet, - &src_addr.into(), &dst_addr.into(), - dhcp_repr.buffer_len(), - |buf| dhcp_repr.emit(&mut Dhcpv4Packet::new_unchecked(buf)).unwrap(), - checksum_caps); - } - Ok(()) -} - -fn parse_udp<'a>(data: &'a [u8], checksum_caps: &ChecksumCapabilities) -> Result<(IpEndpoint, IpEndpoint, &'a [u8])> { - let ipv4_packet = Ipv4Packet::new_checked(data)?; - let ipv4_repr = Ipv4Repr::parse(&ipv4_packet, &checksum_caps)?; - let udp_packet = UdpPacket::new_checked(ipv4_packet.payload())?; - let udp_repr = UdpRepr::parse( - &udp_packet, - &ipv4_repr.src_addr.into(), &ipv4_repr.dst_addr.into(), - checksum_caps - )?; - let src = IpEndpoint { - addr: ipv4_repr.src_addr.into(), - port: udp_repr.src_port, - }; - let dst = IpEndpoint { - addr: ipv4_repr.dst_addr.into(), - port: udp_repr.dst_port, - }; - let data = udp_packet.payload(); - Ok((src, dst, data)) -} diff --git a/src/dhcp/mod.rs b/src/dhcp/mod.rs deleted file mode 100644 index 2c1b47a..0000000 --- a/src/dhcp/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub const UDP_SERVER_PORT: u16 = 67; -pub const UDP_CLIENT_PORT: u16 = 68; - -mod clientv4; -pub use self::clientv4::{Client as Dhcpv4Client, Config as Dhcpv4Config}; diff --git a/src/iface/interface.rs b/src/iface/interface.rs index d569067..e1acc58 100644 --- a/src/iface/interface.rs +++ b/src/iface/interface.rs @@ -265,7 +265,9 @@ pub(crate) enum IpPacket<'a> { #[cfg(feature = "socket-udp")] Udp((IpRepr, UdpRepr, &'a [u8])), #[cfg(feature = "socket-tcp")] - Tcp((IpRepr, TcpRepr<'a>)) + Tcp((IpRepr, TcpRepr<'a>)), + #[cfg(feature = "socket-dhcpv4")] + Dhcpv4((Ipv4Repr, UdpRepr, DhcpRepr<'a>)), } impl<'a> IpPacket<'a> { @@ -283,6 +285,8 @@ impl<'a> IpPacket<'a> { IpPacket::Udp((ip_repr, _, _)) => ip_repr.clone(), #[cfg(feature = "socket-tcp")] IpPacket::Tcp((ip_repr, _)) => ip_repr.clone(), + #[cfg(feature = "socket-dhcpv4")] + IpPacket::Dhcpv4((ipv4_repr, _, _)) => IpRepr::Ipv4(*ipv4_repr), } } @@ -331,6 +335,13 @@ impl<'a> IpPacket<'a> { &_ip_repr.src_addr(), &_ip_repr.dst_addr(), &caps.checksum); } + #[cfg(feature = "socket-dhcpv4")] + IpPacket::Dhcpv4((_, udp_repr, dhcp_repr)) => + udp_repr.emit(&mut UdpPacket::new_unchecked(payload), + &_ip_repr.src_addr(), &_ip_repr.dst_addr(), + dhcp_repr.buffer_len(), + |buf| dhcp_repr.emit(&mut DhcpPacket::new_unchecked(buf)).unwrap(), + &caps.checksum), } } } @@ -662,6 +673,14 @@ impl<'a, DeviceT> Interface<'a, DeviceT> }) } + let _ip_mtu = match _caps.medium { + #[cfg(feature = "medium-ethernet")] + Medium::Ethernet => _caps.max_transmission_unit - EthernetFrame::<&[u8]>::header_len(), + #[cfg(feature = "medium-ip")] + Medium::Ip => _caps.max_transmission_unit, + }; + + let socket_result = match *socket { #[cfg(feature = "socket-raw")] @@ -687,15 +706,14 @@ impl<'a, DeviceT> Interface<'a, DeviceT> respond!(IpPacket::Udp(response))), #[cfg(feature = "socket-tcp")] Socket::Tcp(ref mut socket) => { - let ip_mtu = match _caps.medium { - #[cfg(feature = "medium-ethernet")] - Medium::Ethernet => _caps.max_transmission_unit - EthernetFrame::<&[u8]>::header_len(), - #[cfg(feature = "medium-ip")] - Medium::Ip => _caps.max_transmission_unit, - }; - socket.dispatch(timestamp, ip_mtu, |response| + socket.dispatch(timestamp, _ip_mtu, |response| respond!(IpPacket::Tcp(response))) } + #[cfg(feature = "socket-dhcpv4")] + Socket::Dhcpv4(ref mut socket) => + // todo don't unwrap + socket.dispatch(timestamp, inner.ethernet_addr.unwrap(), _ip_mtu, |response| + respond!(IpPacket::Dhcpv4(response))), }; match (device_result, socket_result) { @@ -1063,6 +1081,34 @@ impl<'a> InterfaceInner<'a> { #[cfg(not(feature = "socket-raw"))] let handled_by_raw_socket = false; + + #[cfg(feature = "socket-dhcpv4")] + { + if ipv4_repr.protocol == IpProtocol::Udp && self.ethernet_addr.is_some() { + // First check for source and dest ports, then do `UdpRepr::parse` if they match. + // This way we avoid validating the UDP checksum twice for all non-DHCP UDP packets (one here, one in `process_udp`) + let udp_packet = UdpPacket::new_checked(ip_payload)?; + if udp_packet.src_port() == DHCP_SERVER_PORT && udp_packet.dst_port() == DHCP_CLIENT_PORT { + if let Some(mut dhcp_socket) = sockets.iter_mut().filter_map(Dhcpv4Socket::downcast).next() { + let (src_addr, dst_addr) = (ip_repr.src_addr(), ip_repr.dst_addr()); + let checksum_caps = self.device_capabilities.checksum.clone(); + let udp_repr = UdpRepr::parse(&udp_packet, &src_addr, &dst_addr, &checksum_caps)?; + let udp_payload = udp_packet.payload(); + + // NOTE(unwrap): we checked for is_some above. + let ethernet_addr = self.ethernet_addr.unwrap(); + + match dhcp_socket.process(timestamp, ethernet_addr, &ipv4_repr, &udp_repr, udp_payload) { + // The packet is valid and handled by socket. + Ok(()) => return Ok(None), + // The packet is malformed, or the socket buffer is full. + Err(e) => return Err(e) + } + } + } + } + } + if !self.has_ip_addr(ipv4_repr.dst_addr) && !self.has_multicast_group(ipv4_repr.dst_addr) && !self.is_broadcast_v4(ipv4_repr.dst_addr) { diff --git a/src/lib.rs b/src/lib.rs index 0fda13b..028edf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -121,8 +121,6 @@ pub mod iface; #[cfg(feature = "socket")] pub mod socket; pub mod time; -#[cfg(feature = "proto-dhcpv4")] -pub mod dhcp; /// The error type for the networking stack. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/src/socket/dhcpv4.rs b/src/socket/dhcpv4.rs new file mode 100644 index 0000000..8137ff6 --- /dev/null +++ b/src/socket/dhcpv4.rs @@ -0,0 +1,422 @@ +use crate::{Error, Result}; +use crate::wire::{EthernetAddress, IpProtocol, IpAddress, + Ipv4Cidr, Ipv4Address, Ipv4Repr, + UdpRepr, UDP_HEADER_LEN, + DhcpPacket, DhcpRepr, DhcpMessageType, DHCP_CLIENT_PORT, DHCP_SERVER_PORT}; +use crate::wire::dhcpv4::{field as dhcpv4_field}; +use crate::socket::SocketMeta; +use crate::time::{Instant, Duration}; + +use super::{PollAt, Socket}; + +const DISCOVER_TIMEOUT: Duration = Duration::from_secs(10); + +const REQUEST_TIMEOUT: Duration = Duration::from_secs(1); +const REQUEST_RETRIES: u16 = 15; + +const MIN_RENEW_TIMEOUT: Duration = Duration::from_secs(60); + +const DEFAULT_LEASE_DURATION: u32 = 120; + +const PARAMETER_REQUEST_LIST: &[u8] = &[ + dhcpv4_field::OPT_SUBNET_MASK, + dhcpv4_field::OPT_ROUTER, + dhcpv4_field::OPT_DOMAIN_NAME_SERVER, +]; + +/// IPv4 configuration data provided by the DHCP server. +#[derive(Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Config { + /// IP address + pub address: Ipv4Cidr, + /// Router address, also known as default gateway. Does not necessarily + /// match the DHCP server's address. + pub router: Option, + /// DNS servers + pub dns_servers: [Option; 3], +} + +/// Information on how to reach a DHCP server. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct ServerInfo { + /// IP address to use as destination in outgoing packets + address: Ipv4Address, + /// Server identifier to use in outgoing packets. Usually equal to server_address, + /// but may differ in some situations (eg DHCP relays) + identifier: Ipv4Address, +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct DiscoverState { + /// When to send next request + retry_at: Instant, +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct RequestState { + /// When to send next request + retry_at: Instant, + /// How many retries have been done + retry: u16, + /// Server we're trying to request from + server: ServerInfo, + /// IP address that we're trying to request. + requested_ip: Ipv4Address, +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct RenewState { + /// Server that gave us the lease + server: ServerInfo, + /// Active networkc config + config: Config, + + /// Renew timer. When reached, we will start attempting + /// to renew this lease with the DHCP server. + /// Must be less or equal than `expires_at`. + renew_at: Instant, + /// Expiration timer. When reached, this lease is no longer valid, so it must be + /// thrown away and the ethernet interface deconfigured. + expires_at: Instant, +} + +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +enum ClientState { + /// Discovering the DHCP server + Discovering(DiscoverState), + /// Requesting an address + Requesting(RequestState), + /// Having an address, refresh it periodically. + Renewing(RenewState), +} + +/// Return value for the `Dhcpv4Socket::poll` function +pub enum Event<'a> { + /// No change has occured to the configuration. + NoChange, + /// Configuration has been lost (for example, the lease has expired) + Deconfigured, + /// Configuration has been newly acquired, or modified. + Configured(&'a Config), +} + +#[derive(Debug)] +pub struct Dhcpv4Socket { + pub(crate) meta: SocketMeta, + /// State of the DHCP client. + state: ClientState, + /// Set to true on config/state change, cleared back to false by the `config` function. + config_changed: bool, + /// xid of the last sent message. + transaction_id: u32, +} + +/// DHCP client socket. +/// +/// The socket acquires an IP address configuration through DHCP autonomously. +/// You must query the configuration with `.poll()` after every call to `Interface::poll()`, +/// and apply the configuration to the `Interface`. +impl Dhcpv4Socket { + /// Create a DHCPv4 socket + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Dhcpv4Socket { + meta: SocketMeta::default(), + state: ClientState::Discovering(DiscoverState{ + retry_at: Instant::from_millis(0), + }), + config_changed: true, + transaction_id: 1, + } + } + + pub(crate) fn poll_at(&self) -> PollAt { + let t = match &self.state { + ClientState::Discovering(state) => state.retry_at, + ClientState::Requesting(state) => state.retry_at, + ClientState::Renewing(state) => state.renew_at.min(state.expires_at), + }; + PollAt::Time(t) + } + + pub(crate) fn process(&mut self, now: Instant, ethernet_addr: EthernetAddress, ip_repr: &Ipv4Repr, repr: &UdpRepr, payload: &[u8]) -> Result<()> { + let src_ip = ip_repr.src_addr; + + if repr.src_port != DHCP_SERVER_PORT || repr.dst_port != DHCP_CLIENT_PORT { + return Ok(()) + } + + let dhcp_packet = match DhcpPacket::new_checked(payload) { + Ok(dhcp_packet) => dhcp_packet, + Err(e) => { + net_debug!("DHCP invalid pkt from {}: {:?}", src_ip, e); + return Ok(()); + } + }; + let dhcp_repr = match DhcpRepr::parse(&dhcp_packet) { + Ok(dhcp_repr) => dhcp_repr, + Err(e) => { + net_debug!("DHCP error parsing pkt from {}: {:?}", src_ip, e); + return Ok(()); + } + }; + if dhcp_repr.client_hardware_address != ethernet_addr { return Ok(()) } + if dhcp_repr.transaction_id != self.transaction_id { return Ok(()) } + let server_identifier = match dhcp_repr.server_identifier { + Some(server_identifier) => server_identifier, + None => { + net_debug!("DHCP ignoring {:?} because missing server_identifier", dhcp_repr.message_type); + return Ok(()); + } + }; + + net_debug!("DHCP recv {:?} from {} ({})", dhcp_repr.message_type, src_ip, server_identifier); + + match (&mut self.state, dhcp_repr.message_type){ + (ClientState::Discovering(_state), DhcpMessageType::Offer) => { + if !dhcp_repr.your_ip.is_unicast() { + net_debug!("DHCP ignoring OFFER because your_ip is not unicast"); + return Ok(()) + } + + self.state = ClientState::Requesting(RequestState { + retry_at: now, + retry: 0, + server: ServerInfo { + address: src_ip, + identifier: server_identifier, + }, + requested_ip: dhcp_repr.your_ip // use the offered ip + }); + } + (ClientState::Requesting(state), DhcpMessageType::Ack) => { + if let Some((config, renew_at, expires_at)) = Self::parse_ack(now, ip_repr, &dhcp_repr) { + self.config_changed = true; + self.state = ClientState::Renewing(RenewState{ + server: state.server, + config, + renew_at, + expires_at, + }); + } + } + (ClientState::Renewing(state), DhcpMessageType::Ack) => { + if let Some((config, renew_at, expires_at)) = Self::parse_ack(now, ip_repr, &dhcp_repr) { + state.renew_at = renew_at; + state.expires_at = expires_at; + if state.config != config { + self.config_changed = true; + state.config = config; + } + } + } + _ => { + net_debug!("DHCP ignoring {:?}: unexpected in current state", dhcp_repr.message_type); + } + } + + Ok(()) + } + + fn parse_ack(now: Instant, _ip_repr: &Ipv4Repr, dhcp_repr: &DhcpRepr) -> Option<(Config, Instant, Instant)> { + let subnet_mask = match dhcp_repr.subnet_mask { + Some(subnet_mask) => subnet_mask, + None => { + net_debug!("DHCP ignoring ACK because missing subnet_mask"); + return None + } + }; + + let prefix_len = match IpAddress::Ipv4(subnet_mask).to_prefix_len() { + Some(prefix_len) => prefix_len, + None => { + net_debug!("DHCP ignoring ACK because subnet_mask is not a valid mask"); + return None + } + }; + + if !dhcp_repr.your_ip.is_unicast() { + net_debug!("DHCP ignoring ACK because your_ip is not unicast"); + return None + } + + let lease_duration = dhcp_repr.lease_duration.unwrap_or(DEFAULT_LEASE_DURATION); + + let config = Config{ + address: Ipv4Cidr::new(dhcp_repr.your_ip, prefix_len), + router: dhcp_repr.router, + dns_servers: dhcp_repr.dns_servers.unwrap_or([None; 3]), + }; + + // RFC 2131 indicates clients should renew a lease halfway through its expiration. + let renew_at = now + Duration::from_secs((lease_duration / 2).into()); + let expires_at = now + Duration::from_secs(lease_duration.into()); + + Some((config, renew_at, expires_at)) + } + + pub(crate) fn dispatch(&mut self, now: Instant, ethernet_addr: EthernetAddress, ip_mtu: usize, emit: F) -> Result<()> + where F: FnOnce((Ipv4Repr, UdpRepr, DhcpRepr)) -> Result<()> { + + // Worst case biggest IPv4 header length. + // 0x0f * 4 = 60 bytes. + const MAX_IPV4_HEADER_LEN: usize = 60; + + // We don't directly increment transaction_id because sending the packet + // may fail. We only want to update state after succesfully sending. + let next_transaction_id = self.transaction_id + 1; + + let mut dhcp_repr = DhcpRepr { + message_type: DhcpMessageType::Discover, + transaction_id: next_transaction_id, + client_hardware_address: ethernet_addr, + client_ip: Ipv4Address::UNSPECIFIED, + your_ip: Ipv4Address::UNSPECIFIED, + server_ip: Ipv4Address::UNSPECIFIED, + router: None, + subnet_mask: None, + relay_agent_ip: Ipv4Address::UNSPECIFIED, + broadcast: true, + requested_ip: None, + client_identifier: Some(ethernet_addr), + server_identifier: None, + parameter_request_list: Some(PARAMETER_REQUEST_LIST), + max_size: Some((ip_mtu - MAX_IPV4_HEADER_LEN - UDP_HEADER_LEN) as u16), + lease_duration: None, + dns_servers: None, + }; + + let udp_repr = UdpRepr { + src_port: DHCP_CLIENT_PORT, + dst_port: DHCP_SERVER_PORT, + }; + + let mut ipv4_repr = Ipv4Repr { + src_addr: Ipv4Address::UNSPECIFIED, + dst_addr: Ipv4Address::BROADCAST, + protocol: IpProtocol::Udp, + payload_len: 0, // filled right before emit + hop_limit: 64, + }; + + match &mut self.state { + ClientState::Discovering(state) => { + if now < state.retry_at { + return Err(Error::Exhausted) + } + + // send packet + net_debug!("DHCP send DISCOVER to {}: {:?}", ipv4_repr.dst_addr, dhcp_repr); + ipv4_repr.payload_len = udp_repr.header_len() + dhcp_repr.buffer_len(); + emit((ipv4_repr, udp_repr, dhcp_repr))?; + + // Update state AFTER the packet has been successfully sent. + state.retry_at = now + DISCOVER_TIMEOUT; + self.transaction_id = next_transaction_id; + Ok(()) + } + ClientState::Requesting(state) => { + if now < state.retry_at { + return Err(Error::Exhausted) + } + + if state.retry >= REQUEST_RETRIES { + net_debug!("DHCP request retries exceeded, restarting discovery"); + self.reset(); + // return Ok so we get polled again + return Ok(()) + } + + dhcp_repr.message_type = DhcpMessageType::Request; + dhcp_repr.broadcast = false; + dhcp_repr.requested_ip = Some(state.requested_ip); + dhcp_repr.server_identifier = Some(state.server.identifier); + + net_debug!("DHCP send request to {}: {:?}", ipv4_repr.dst_addr, dhcp_repr); + ipv4_repr.payload_len = udp_repr.header_len() + dhcp_repr.buffer_len(); + emit((ipv4_repr, udp_repr, dhcp_repr))?; + + // Exponential backoff + state.retry_at = now + REQUEST_TIMEOUT; + state.retry += 1; + + self.transaction_id = next_transaction_id; + Ok(()) + } + ClientState::Renewing(state) => { + if state.expires_at <= now { + net_debug!("DHCP lease expired"); + self.reset(); + // return Ok so we get polled again + return Ok(()) + } + + if now < state.renew_at { + return Err(Error::Exhausted) + } + + ipv4_repr.src_addr = state.config.address.address(); + ipv4_repr.dst_addr = state.server.address; + dhcp_repr.message_type = DhcpMessageType::Request; + dhcp_repr.client_ip = state.config.address.address(); + dhcp_repr.broadcast = false; + + net_debug!("DHCP send renew to {}: {:?}", ipv4_repr.dst_addr, dhcp_repr); + ipv4_repr.payload_len = udp_repr.header_len() + dhcp_repr.buffer_len(); + emit((ipv4_repr, udp_repr, dhcp_repr))?; + + // In both RENEWING and REBINDING states, if the client receives no + // response to its DHCPREQUEST message, the client SHOULD wait one-half + // of the remaining time until T2 (in RENEWING state) and one-half of + // the remaining lease time (in REBINDING state), down to a minimum of + // 60 seconds, before retransmitting the DHCPREQUEST message. + state.renew_at = now + MIN_RENEW_TIMEOUT.max((state.expires_at - now) / 2); + + self.transaction_id = next_transaction_id; + Ok(()) + } + } + } + + /// Reset state and restart discovery phase. + /// + /// Use this to speed up acquisition of an address in a new + /// network if a link was down and it is now back up. + pub fn reset(&mut self) { + net_trace!("DHCP reset"); + if let ClientState::Renewing(_) = &self.state { + self.config_changed = true; + } + self.state = ClientState::Discovering(DiscoverState{ + retry_at: Instant::from_millis(0), + }); + } + + /// Query the socket for configuration changes. + /// + /// The socket has an internal "configuration changed" flag. If + /// set, this function returns the configuration and resets the flag. + pub fn poll(&mut self) -> Event<'_> { + if !self.config_changed { + Event::NoChange + } else if let ClientState::Renewing(state) = &self.state { + self.config_changed = false; + Event::Configured(&state.config) + } else { + self.config_changed = false; + Event::Deconfigured + } + } +} + +impl<'a> Into> for Dhcpv4Socket { + fn into(self) -> Socket<'a> { + Socket::Dhcpv4(self) + } +} diff --git a/src/socket/mod.rs b/src/socket/mod.rs index 76fe30f..ebd0c42 100644 --- a/src/socket/mod.rs +++ b/src/socket/mod.rs @@ -22,6 +22,8 @@ mod icmp; mod udp; #[cfg(feature = "socket-tcp")] mod tcp; +#[cfg(feature = "socket-dhcpv4")] +mod dhcpv4; mod set; mod ref_; @@ -53,6 +55,9 @@ pub use self::tcp::{SocketBuffer as TcpSocketBuffer, State as TcpState, TcpSocket}; +#[cfg(feature = "socket-dhcpv4")] +pub use self::dhcpv4::{Dhcpv4Socket, Config as Dhcpv4Config, Event as Dhcpv4Event}; + pub use self::set::{Set as SocketSet, Item as SocketSetItem, Handle as SocketHandle}; pub use self::set::{Iter as SocketSetIter, IterMut as SocketSetIterMut}; @@ -91,6 +96,8 @@ pub enum Socket<'a> { Udp(UdpSocket<'a>), #[cfg(feature = "socket-tcp")] Tcp(TcpSocket<'a>), + #[cfg(feature = "socket-dhcpv4")] + Dhcpv4(Dhcpv4Socket), } macro_rules! dispatch_socket { @@ -110,6 +117,8 @@ macro_rules! dispatch_socket { &$( $mut_ )* Socket::Udp(ref $( $mut_ )* $socket) => $code, #[cfg(feature = "socket-tcp")] &$( $mut_ )* Socket::Tcp(ref $( $mut_ )* $socket) => $code, + #[cfg(feature = "socket-dhcpv4")] + &$( $mut_ )* Socket::Dhcpv4(ref $( $mut_ )* $socket) => $code, } }; } @@ -169,3 +178,5 @@ from_socket!(IcmpSocket<'a>, Icmp); from_socket!(UdpSocket<'a>, Udp); #[cfg(feature = "socket-tcp")] from_socket!(TcpSocket<'a>, Tcp); +#[cfg(feature = "socket-dhcpv4")] +from_socket!(Dhcpv4Socket, Dhcpv4); diff --git a/src/socket/ref_.rs b/src/socket/ref_.rs index 9e030ff..3a52a77 100644 --- a/src/socket/ref_.rs +++ b/src/socket/ref_.rs @@ -1,13 +1,5 @@ use core::ops::{Deref, DerefMut}; -#[cfg(feature = "socket-raw")] -use crate::socket::RawSocket; -#[cfg(all(feature = "socket-icmp", any(feature = "proto-ipv4", feature = "proto-ipv6")))] -use crate::socket::IcmpSocket; -#[cfg(feature = "socket-udp")] -use crate::socket::UdpSocket; -#[cfg(feature = "socket-tcp")] -use crate::socket::TcpSocket; /// A trait for tracking a socket usage session. /// @@ -20,13 +12,15 @@ pub trait Session { } #[cfg(feature = "socket-raw")] -impl<'a> Session for RawSocket<'a> {} +impl<'a> Session for crate::socket::RawSocket<'a> {} #[cfg(all(feature = "socket-icmp", any(feature = "proto-ipv4", feature = "proto-ipv6")))] -impl<'a> Session for IcmpSocket<'a> {} +impl<'a> Session for crate::socket::IcmpSocket<'a> {} #[cfg(feature = "socket-udp")] -impl<'a> Session for UdpSocket<'a> {} +impl<'a> Session for crate::socket::UdpSocket<'a> {} #[cfg(feature = "socket-tcp")] -impl<'a> Session for TcpSocket<'a> {} +impl<'a> Session for crate::socket::TcpSocket<'a> {} +#[cfg(feature = "socket-dhcpv4")] +impl Session for crate::socket::Dhcpv4Socket {} /// A smart pointer to a socket. /// diff --git a/src/socket/set.rs b/src/socket/set.rs index 6866656..74af317 100644 --- a/src/socket/set.rs +++ b/src/socket/set.rs @@ -156,6 +156,9 @@ impl<'a> Set<'a> { } else { socket.close() }, + #[cfg(feature = "socket-dhcpv4")] + Socket::Dhcpv4(_) => + may_remove = true, } } if may_remove { diff --git a/src/wire/dhcpv4.rs b/src/wire/dhcpv4.rs index a938943..0e3bfdd 100644 --- a/src/wire/dhcpv4.rs +++ b/src/wire/dhcpv4.rs @@ -6,6 +6,9 @@ use crate::{Error, Result}; use crate::wire::{EthernetAddress, Ipv4Address}; use crate::wire::arp::Hardware; +pub const SERVER_PORT: u16 = 67; +pub const CLIENT_PORT: u16 = 68; + const DHCP_MAGIC_NUMBER: u32 = 0x63825363; enum_with_unknown! { diff --git a/src/wire/mod.rs b/src/wire/mod.rs index 4fee202..ca1f244 100644 --- a/src/wire/mod.rs +++ b/src/wire/mod.rs @@ -211,7 +211,8 @@ pub use self::mld::{AddressRecord as MldAddressRecord, Repr as MldRepr}; pub use self::udp::{Packet as UdpPacket, - Repr as UdpRepr}; + Repr as UdpRepr, + HEADER_LEN as UDP_HEADER_LEN}; pub use self::tcp::{SeqNumber as TcpSeqNumber, Packet as TcpPacket, @@ -222,4 +223,6 @@ pub use self::tcp::{SeqNumber as TcpSeqNumber, #[cfg(feature = "proto-dhcpv4")] pub use self::dhcpv4::{Packet as DhcpPacket, Repr as DhcpRepr, - MessageType as DhcpMessageType}; + MessageType as DhcpMessageType, + CLIENT_PORT as DHCP_CLIENT_PORT, + SERVER_PORT as DHCP_SERVER_PORT}; diff --git a/src/wire/udp.rs b/src/wire/udp.rs index ef60ed2..ad1c1a2 100644 --- a/src/wire/udp.rs +++ b/src/wire/udp.rs @@ -28,6 +28,8 @@ mod field { } } +pub const HEADER_LEN: usize = field::CHECKSUM.end; + #[allow(clippy::len_without_is_empty)] impl> Packet { /// Imbue a raw octet buffer with UDP packet structure.