diff --git a/.travis.yml b/.travis.yml index 2febeaa..07398ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,8 @@ matrix: env: FEATURES='std proto-ipv4 proto-igmp socket-raw' MODE='test' - rust: nightly env: FEATURES='std proto-ipv4 socket-udp socket-tcp' MODE='test' + - rust: nightly + env: FEATURES='std proto-ipv4 proto-dhcpv4 socket-udp' MODE='test' - rust: nightly env: FEATURES='std proto-ipv6 socket-udp' MODE='test' - rust: nightly @@ -35,7 +37,7 @@ matrix: env: FEATURES='proto-ipv4 proto-ipv6 socket-raw socket-udp socket-tcp socket-icmp alloc' MODE='test' - rust: nightly - env: FEATURES='proto-ipv4 proto-ipv6 proto-igmp socket-raw socket-udp socket-tcp socket-icmp' + env: FEATURES='proto-ipv4 proto-ipv6 proto-igmp proto-dhcpv4 socket-raw socket-udp socket-tcp socket-icmp' MODE='build' - rust: nightly env: MODE='fuzz run' ARGS='packet_parser -- -max_len=1536 -max_total_time=30' diff --git a/Cargo.toml b/Cargo.toml index ef9580e..779dde4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,12 +35,12 @@ verbose = [] "phy-tap_interface" = ["std", "libc"] "proto-ipv4" = [] "proto-igmp" = ["proto-ipv4"] +"proto-dhcpv4" = ["proto-ipv4", "socket-raw"] "proto-ipv6" = [] "socket-raw" = [] "socket-udp" = [] "socket-tcp" = [] "socket-icmp" = [] -"proto-dhcpv4" = ["proto-ipv4"] default = [ "std", "log", # needed for `cargo test --no-default-features --features default` :/ "phy-raw_socket", "phy-tap_interface", @@ -88,5 +88,9 @@ required-features = ["std", "phy-tap_interface", "proto-ipv4", "proto-igmp", "so name = "benchmark" required-features = ["std", "phy-tap_interface", "proto-ipv4", "socket-raw", "socket-udp"] +[[example]] +name = "dhcp_client" +required-features = ["std", "phy-tap_interface", "proto-ipv4", "proto-dhcpv4", "socket-raw"] + [profile.release] debug = 2 diff --git a/examples/dhcp_client.rs b/examples/dhcp_client.rs new file mode 100644 index 0000000..0695c40 --- /dev/null +++ b/examples/dhcp_client.rs @@ -0,0 +1,106 @@ +#[macro_use] +extern crate log; +extern crate env_logger; +extern crate getopts; +extern crate smoltcp; + +mod utils; + +use std::collections::BTreeMap; +use std::os::unix::io::AsRawFd; +use smoltcp::phy::wait as phy_wait; +use smoltcp::wire::{EthernetAddress, Ipv4Address, IpCidr, Ipv4Cidr}; +use smoltcp::iface::{NeighborCache, EthernetInterfaceBuilder, Routes}; +use smoltcp::socket::{SocketSet, RawSocketBuffer, RawPacketMetadata}; +use smoltcp::time::Instant; +use smoltcp::dhcp::Dhcpv4Client; + +fn main() { + #[cfg(feature = "log")] + utils::setup_logging(""); + + let (mut opts, mut free) = utils::create_options(); + utils::add_tap_options(&mut opts, &mut free); + utils::add_middleware_options(&mut opts, &mut free); + + let mut matches = utils::parse_options(&opts, free); + let device = utils::parse_tap_options(&mut matches); + let fd = device.as_raw_fd(); + let device = utils::parse_middleware_options(&mut matches, device, /*loopback=*/false); + + let neighbor_cache = NeighborCache::new(BTreeMap::new()); + let ethernet_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]); + let ip_addrs = [IpCidr::new(Ipv4Address::UNSPECIFIED.into(), 0)]; + let mut routes_storage = [None; 1]; + let routes = Routes::new(&mut routes_storage[..]); + let mut iface = EthernetInterfaceBuilder::new(device) + .ethernet_addr(ethernet_addr) + .neighbor_cache(neighbor_cache) + .ip_addrs(ip_addrs) + .routes(routes) + .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); + 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); + match config.address { + Some(cidr) => if cidr != prev_cidr { + iface.update_ip_addrs(|addrs| { + addrs.iter_mut().nth(0) + .map(|addr| { + *addr = IpCidr::Ipv4(cidr); + }); + }); + prev_cidr = cidr; + println!("Assigned a new IPv4 address: {}", cidr); + } + _ => {} + } + + config.router.map(|router| iface.routes_mut() + .add_default_ipv4_route(router.into()) + .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); + } + } + }); + + 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));; + } +} diff --git a/src/dhcp/clientv4.rs b/src/dhcp/clientv4.rs new file mode 100644 index 0000000..361e899 --- /dev/null +++ b/src/dhcp/clientv4.rs @@ -0,0 +1,435 @@ +use {Result, Error}; +use wire::{IpVersion, IpProtocol, IpEndpoint, IpAddress, + Ipv4Cidr, Ipv4Address, Ipv4Packet, Ipv4Repr, + UdpPacket, UdpRepr, + DhcpPacket, DhcpRepr, DhcpMessageType}; +use wire::dhcpv4::field as dhcpv4_field; +use socket::{SocketSet, SocketHandle, RawSocket, RawSocketBuffer}; +use phy::{Device, ChecksumCapabilities}; +use iface::EthernetInterface as Interface; +use 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 RENEW_INTERVAL: u64 = 60; +const RENEW_RETRIES: u16 = 3; +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)] +pub struct Config { + pub address: Option, + pub router: Option, + pub dns_servers: [Option; 3], +} + +#[derive(Debug)] +struct RequestState { + retry: u16, + endpoint_ip: Ipv4Address, + server_identifier: Ipv4Address, +} + +#[derive(Debug)] +struct RenewState { + retry: u16, + endpoint_ip: Ipv4Address, + server_identifier: Ipv4Address, +} + +#[derive(Debug)] +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, + 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, 'b, 'c>(sockets: &mut SocketSet<'a, 'b, 'c>, rx_buffer: RawSocketBuffer<'b, 'c>, tx_buffer: RawSocketBuffer<'b, 'c>, now: Instant) -> Self + where 'b: 'c, + { + 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, + } + } + + /// 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. You must configure the new IPv4 address from the + /// returned `Config`. Otherwise, DHCP will not work. + /// + /// 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 = ChecksumCapabilities::default(); // ? + 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); + + let config = if (dhcp_repr.message_type == DhcpMessageType::Offer || + dhcp_repr.message_type == DhcpMessageType::Ack) && + dhcp_repr.your_ip != Ipv4Address::UNSPECIFIED { + 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, + }; + Some(ClientState::Requesting(r_state)) + } + ClientState::Requesting(ref r_state) + 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); + + 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 + } + 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 { + 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: 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, + }; + let requested_ip = match iface.ipv4_addr() { + Some(addr) if !addr.is_unspecified() => + Some(addr), + _ => + None, + }; + dhcp_repr.message_type = DhcpMessageType::Request; + dhcp_repr.broadcast = false; + dhcp_repr.requested_ip = requested_ip; + dhcp_repr.server_identifier = Some(r_state.server_identifier); + dhcp_repr.parameter_request_list = Some(PARAMETER_REQUEST_LIST); + net_trace!("DHCP send request to {} = {:?}", endpoint, dhcp_repr); + 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); + + 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; + } +} + +fn send_packet Device<'d>>(iface: &mut Interface, raw_socket: &mut RawSocket, endpoint: &IpEndpoint, dhcp_repr: &DhcpRepr, checksum_caps: &ChecksumCapabilities) -> Result<()> { + let mut dhcp_payload_buf = [0; 320]; + assert!(dhcp_repr.buffer_len() <= dhcp_payload_buf.len()); + let dhcp_payload = &mut dhcp_payload_buf[0..dhcp_repr.buffer_len()]; + { + let mut dhcp_packet = DhcpPacket::new_checked(&mut dhcp_payload[..])?; + dhcp_repr.emit(&mut dhcp_packet)?; + } + + let udp_repr = UdpRepr { + src_port: UDP_CLIENT_PORT, + dst_port: endpoint.port, + payload: dhcp_payload, + }; + + 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.buffer_len(), + hop_limit: 64, + }; + + let mut packet = raw_socket.send( + ipv4_repr.buffer_len() + udp_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(), + 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_repr.payload; + Ok((src, dst, data)) +} diff --git a/src/dhcp/mod.rs b/src/dhcp/mod.rs new file mode 100644 index 0000000..f259b20 --- /dev/null +++ b/src/dhcp/mod.rs @@ -0,0 +1,5 @@ +pub const UDP_SERVER_PORT: u16 = 67; +pub const UDP_CLIENT_PORT: u16 = 68; + +mod clientv4; +pub use self::clientv4::Client as Dhcpv4Client; diff --git a/src/iface/ethernet.rs b/src/iface/ethernet.rs index 2b98d79..e461687 100644 --- a/src/iface/ethernet.rs +++ b/src/iface/ethernet.rs @@ -433,6 +433,16 @@ impl<'b, 'c, 'e, DeviceT> Interface<'b, 'c, 'e, DeviceT> self.inner.ip_addrs.as_ref() } + /// Get the first IPv4 address if present. + #[cfg(feature = "proto-ipv4")] + pub fn ipv4_addr(&self) -> Option { + self.ip_addrs().iter() + .filter_map(|cidr| match cidr.address() { + IpAddress::Ipv4(addr) => Some(addr), + _ => None, + }).next() + } + /// Update the IP addresses of the interface. /// /// # Panics @@ -698,7 +708,7 @@ impl<'b, 'c, 'e> InterfaceInner<'b, 'c, 'e> { fn check_ip_addrs(addrs: &[IpCidr]) { for cidr in addrs { - if !cidr.address().is_unicast() { + if !cidr.address().is_unicast() && !cidr.address().is_unspecified() { panic!("IP address {} is not unicast", cidr.address()) } } diff --git a/src/lib.rs b/src/lib.rs index 93d90a1..a336469 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -119,6 +119,8 @@ pub mod wire; pub mod iface; 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/wire/dhcpv4.rs b/src/wire/dhcpv4.rs index 81173a5..fe85682 100644 --- a/src/wire/dhcpv4.rs +++ b/src/wire/dhcpv4.rs @@ -765,7 +765,8 @@ impl<'a> Repr<'a> { }) } - /// Emit a high-level representation into a Transmission Control Protocol packet. + /// Emit a high-level representation into a Dynamic Host + /// Configuration Protocol packet. pub fn emit(&self, packet: &mut Packet<&mut T>) -> Result<()> where T: AsRef<[u8]> + AsMut<[u8]> + ?Sized { packet.set_sname_and_boot_file_to_zero();