| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| #![deny(warnings)] |
| #![allow(unused)] // TODO(atait): Remove once there are non-test clients |
| |
| use std::net::Ipv4Addr; |
| |
| const MAC_ADDR_LEN: usize = 6; |
| |
| const OP_IDX: usize = 0; |
| const HTYPE_IDX: usize = 1; |
| const HLEN_IDX: usize = 2; |
| const HOPS_IDX: usize = 3; |
| const XID_IDX: usize = 4; |
| const SECS_IDX: usize = 8; |
| const FLAGS_IDX: usize = 10; |
| const CIADDR_IDX: usize = 12; |
| const YIADDR_IDX: usize = 16; |
| const SIADDR_IDX: usize = 20; |
| const GIADDR_IDX: usize = 24; |
| const CHADDR_IDX: usize = 28; |
| const SNAME_IDX: usize = 44; |
| const FILE_IDX: usize = 108; |
| const OPTIONS_START_IDX: usize = 236; |
| |
| const ETHERNET_HTYPE: u8 = 1; |
| const ETHERNET_HLEN: u8 = 6; |
| const HOPS_DEFAULT: u8 = 0; |
| |
| const UNUSED_CHADDR_BYTES: usize = 10; |
| |
| const SNAME_LEN: usize = 64; |
| const FILE_LEN: usize = 128; |
| |
| const ONE_BYTE_LEN: usize = 8; |
| const TWO_BYTE_LEN: usize = 16; |
| const THREE_BYTE_LEN: usize = 24; |
| |
| /// A DHCP protocol message as defined in RFC 2131. |
| /// |
| /// All fields in `Message` follow the naming conventions outlined in the RFC. |
| /// Note that `Message` does not expose `htype`, `hlen`, or `hops` fields, as |
| /// these fields are effectively constants. |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct Message { |
| pub op: OpCode, |
| pub xid: u32, |
| pub secs: u16, |
| pub bdcast_flag: bool, |
| /// `ciaddr` should be stored in Big-Endian order, e.g `[192, 168, 1, 1]`. |
| pub ciaddr: Ipv4Addr, |
| /// `yiaddr` should be stored in Big-Endian order, e.g `[192, 168, 1, 1]`. |
| pub yiaddr: Ipv4Addr, |
| /// `siaddr` should be stored in Big-Endian order, e.g `[192, 168, 1, 1]`. |
| pub siaddr: Ipv4Addr, |
| /// `giaddr` should be stored in Big-Endian order, e.g `[192, 168, 1, 1]`. |
| pub giaddr: Ipv4Addr, |
| /// `chaddr` should be stored in Big-Endian order, |
| /// e.g `[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]`. |
| pub chaddr: MacAddr, |
| /// `sname` should not exceed 64 characters. |
| pub sname: String, |
| /// `file` should not exceed 128 characters. |
| pub file: String, |
| pub options: Vec<ConfigOption>, |
| } |
| |
| impl Message { |
| /// Instantiates a new `Message` with default field values. |
| pub fn new() -> Self { |
| let msg = Message { |
| op: OpCode::BOOTREQUEST, |
| xid: 0, |
| secs: 0, |
| bdcast_flag: false, |
| ciaddr: Ipv4Addr::new(0, 0, 0, 0), |
| yiaddr: Ipv4Addr::new(0, 0, 0, 0), |
| siaddr: Ipv4Addr::new(0, 0, 0, 0), |
| giaddr: Ipv4Addr::new(0, 0, 0, 0), |
| chaddr: [0; MAC_ADDR_LEN], |
| sname: String::new(), |
| file: String::new(), |
| options: Vec::new(), |
| }; |
| msg |
| } |
| |
| /// Instantiates a new `Message` from a byte buffer conforming to the DHCP |
| /// protocol as defined RFC 2131. Returns `None` if the buffer is malformed. |
| /// Any malformed configuration options will be skipped over, leaving only |
| /// well formed `ConfigOption`s in the final `Message`. |
| pub fn from_buffer(buf: &[u8]) -> Option<Self> { |
| use byteorder::{BigEndian, ByteOrder}; |
| |
| if buf.len() < OPTIONS_START_IDX { |
| return None; |
| } |
| |
| let mut msg = Message::new(); |
| let op = buf.get(OP_IDX)?; |
| msg.op = OpCode::from(*op)?; |
| msg.xid = BigEndian::read_u32(&buf[XID_IDX..SECS_IDX]); |
| msg.secs = BigEndian::read_u16(&buf[SECS_IDX..FLAGS_IDX]); |
| msg.bdcast_flag = buf[FLAGS_IDX] > 0; |
| // The conditional earlier in this function ensures that buf is long enough |
| // for the following 4 calls to always succeed. |
| msg.ciaddr = ip_addr_from_buf_at(buf, CIADDR_IDX).expect("out of range indexing on buf"); |
| msg.yiaddr = ip_addr_from_buf_at(buf, YIADDR_IDX).expect("out of range indexing on buf"); |
| msg.siaddr = ip_addr_from_buf_at(buf, SIADDR_IDX).expect("out of range indexing on buf"); |
| msg.giaddr = ip_addr_from_buf_at(buf, GIADDR_IDX).expect("out of range indexing on buf"); |
| copy_buf_into_mac_addr(&buf[CHADDR_IDX..CHADDR_IDX + 6], &mut msg.chaddr); |
| msg.sname = buf_to_msg_string(&buf[SNAME_IDX..FILE_IDX])?; |
| msg.file = buf_to_msg_string(&buf[FILE_IDX..OPTIONS_START_IDX])?; |
| buf_into_options(&buf[OPTIONS_START_IDX..], &mut msg.options); |
| |
| Some(msg) |
| } |
| |
| /// Consumes the calling `Message` to serialize it into a buffer of bytes. |
| pub fn serialize(&self) -> Vec<u8> { |
| let mut buffer = Vec::with_capacity(OPTIONS_START_IDX); |
| buffer.push(self.op as u8); |
| buffer.push(ETHERNET_HTYPE); |
| buffer.push(ETHERNET_HLEN); |
| buffer.push(HOPS_DEFAULT); |
| buffer.push((self.xid >> THREE_BYTE_LEN) as u8); |
| buffer.push((self.xid >> TWO_BYTE_LEN) as u8); |
| buffer.push((self.xid >> ONE_BYTE_LEN) as u8); |
| buffer.push(self.xid as u8); |
| buffer.push((self.secs >> ONE_BYTE_LEN) as u8); |
| buffer.push(self.secs as u8); |
| if self.bdcast_flag { |
| // Set most significant bit. |
| buffer.push(128u8); |
| } else { |
| buffer.push(0u8); |
| } |
| buffer.push(0u8); |
| buffer.extend_from_slice(&self.ciaddr.octets()); |
| buffer.extend_from_slice(&self.yiaddr.octets()); |
| buffer.extend_from_slice(&self.siaddr.octets()); |
| buffer.extend_from_slice(&self.giaddr.octets()); |
| buffer.extend_from_slice(&self.chaddr); |
| buffer.extend_from_slice(&[0u8; UNUSED_CHADDR_BYTES]); |
| trunc_string_to_n_and_push(&self.sname, SNAME_LEN, &mut buffer); |
| trunc_string_to_n_and_push(&self.file, FILE_LEN, &mut buffer); |
| buffer.extend_from_slice(&self.serialize_options()); |
| buffer |
| } |
| |
| fn serialize_options(&self) -> Vec<u8> { |
| let mut bytes = Vec::new(); |
| for option in &self.options { |
| option.serialize_to(&mut bytes); |
| } |
| bytes.push(OptionCode::End as u8); |
| bytes |
| } |
| |
| /// Returns a reference to the `Message`'s `ConfigOption` with `code`, or `None` |
| /// if `Message` does not have the specified `ConfigOption`. |
| pub fn get_config_option(&self, code: OptionCode) -> Option<&ConfigOption> { |
| // There should generally be few (~0 - 10) options attached to a message |
| // so the linear search should not be unreasonably costly. |
| for opt in &self.options { |
| if opt.code == code { |
| return Some(opt); |
| } |
| } |
| None |
| } |
| } |
| |
| /// A DHCP protocol op-code as defined in RFC 2131. |
| /// |
| /// Note that this type corresponds to the first field of a DHCP message, |
| /// opcode, and is distinct from the OptionCode type. In this case, "Op" |
| /// is an abbreviation for Operator, not Option. |
| /// |
| /// `OpCode::BOOTREQUEST` should only appear in protocol messages from the |
| /// client, and conversely `OpCode::BOOTREPLY` should only appear in messages |
| /// from the server. |
| #[derive(Copy, Clone, Debug, PartialEq)] |
| pub enum OpCode { |
| BOOTREQUEST = 1, |
| BOOTREPLY = 2, |
| } |
| |
| impl OpCode { |
| /// Instantiates an `OpCode` from its `u8` raw value. Returns None for an |
| /// invalid `OpCode` raw value. |
| pub fn from(val: u8) -> Option<Self> { |
| match val { |
| 1 => Some(OpCode::BOOTREQUEST), |
| 2 => Some(OpCode::BOOTREPLY), |
| _ => None, |
| } |
| } |
| } |
| |
| /// A 48-bit network hardware MAC address. |
| pub type MacAddr = [u8; MAC_ADDR_LEN]; |
| |
| /// A vendor extension/configuration option for DHCP protocol messages. |
| /// |
| /// `ConfigOption`s can be fixed or variable length per RFC 1533. When |
| /// `value` is left empty, the `ConfigOption` will be treated as a fixed |
| /// length field. |
| #[derive(Clone, Debug, PartialEq)] |
| pub struct ConfigOption { |
| pub code: OptionCode, |
| pub value: Vec<u8>, |
| } |
| |
| impl ConfigOption { |
| fn from_buffer(buf: &[u8]) -> Option<ConfigOption> { |
| if buf.len() <= 0 { |
| return None; |
| } |
| let raw_code = buf[0]; |
| let code = OptionCode::option_code_from_u8(raw_code)?; |
| let len: usize = match buf.get(1) { |
| Some(l) => *l as usize, |
| None => 0, |
| }; |
| let mut value = Vec::new(); |
| let mut i: usize = 2; |
| while i < len + 2 { |
| let v = match buf.get(i) { |
| Some(val) => *val, |
| None => return None, |
| }; |
| value.push(v); |
| i += 1; |
| } |
| if len != value.len() { |
| return None; |
| } |
| let opt = ConfigOption { code, value }; |
| Some(opt) |
| } |
| |
| fn serialize_to(&self, output: &mut Vec<u8>) { |
| output.push(self.code as u8); |
| let len = self.value.len() as u8; |
| if len > 0 { |
| output.push(len); |
| } |
| output.extend(&self.value); |
| } |
| } |
| |
| /// A DHCP option code. |
| /// |
| /// This enum corresponds to the codes for DHCP options as defined in |
| /// RFC 1533. Note that not all options defined in the RFC are represented |
| /// here; options which are not in this type are not currently supported. Supported |
| /// options appear in this type in the order in which they are defined in the RFC. |
| #[derive(Copy, Clone, Debug, PartialEq)] |
| pub enum OptionCode { |
| Pad = 0, |
| End = 255, |
| SubnetMask = 1, |
| RequestedIpAddr = 50, |
| IpAddrLeaseTime = 51, |
| DhcpMessageType = 53, |
| ServerId = 54, |
| } |
| |
| impl OptionCode { |
| /// Returns an OptionCode value when given a valid and supported code value, else None. |
| pub fn option_code_from_u8(n: u8) -> Option<OptionCode> { |
| match n { |
| 0 => Some(OptionCode::Pad), |
| 255 => Some(OptionCode::End), |
| 1 => Some(OptionCode::SubnetMask), |
| 50 => Some(OptionCode::RequestedIpAddr), |
| 51 => Some(OptionCode::IpAddrLeaseTime), |
| 53 => Some(OptionCode::DhcpMessageType), |
| 54 => Some(OptionCode::ServerId), |
| _ => None, |
| } |
| } |
| } |
| |
| /// A DHCP Message Type. |
| /// |
| /// This enum corresponds to the DHCP Message Type option values |
| /// defined in section 9.4 of RFC 1533. |
| #[derive(Copy, Clone, Debug, PartialEq)] |
| pub enum MessageType { |
| DHCPDISCOVER = 1, |
| DHCPOFFER = 2, |
| DHCPREQUEST = 3, |
| DHCPDECLINE = 4, |
| DHCPACK = 5, |
| DHCPNAK = 6, |
| DHCPRELEASE = 7, |
| } |
| |
| // Returns an Ipv4Addr when given a byte buffer in network order whose len >= start + 4. |
| pub fn ip_addr_from_buf_at(buf: &[u8], start: usize) -> Option<Ipv4Addr> { |
| if buf.len() < start + 4 { |
| return None; |
| } |
| Some(Ipv4Addr::new( |
| buf[start], |
| buf[start + 1], |
| buf[start + 2], |
| buf[start + 3], |
| )) |
| } |
| |
| fn copy_buf_into_mac_addr(buf: &[u8], addr: &mut MacAddr) { |
| addr.copy_from_slice(buf); |
| } |
| |
| fn buf_to_msg_string(buf: &[u8]) -> Option<String> { |
| use std::str; |
| let maybe_string = str::from_utf8(buf); |
| match maybe_string.ok() { |
| Some(string) => Some(string.trim_right_matches('\x00').to_string()), |
| None => None, |
| } |
| } |
| |
| fn buf_into_options(buf: &[u8], options: &mut Vec<ConfigOption>) { |
| let mut opt_idx = 0; |
| while opt_idx < buf.len() |
| && OptionCode::option_code_from_u8(buf[opt_idx]) != Some(OptionCode::End) |
| { |
| let opt_code = buf[opt_idx]; |
| let opt_len: usize = match OptionCode::option_code_from_u8(opt_code) { |
| None => continue, // invalid option |
| Some(OptionCode::Pad) | Some(OptionCode::End) => 1, // fixed length option |
| _ => match buf.get(opt_idx + 1) { |
| // variable length option |
| Some(len) => (len + 2) as usize, |
| None => 1 as usize, |
| }, |
| }; |
| let maybe_opt = ConfigOption::from_buffer(&buf[opt_idx..opt_idx + opt_len]); |
| opt_idx += opt_len; |
| let opt = match maybe_opt { |
| Some(opt) => opt, |
| None => continue, |
| }; |
| options.push(opt); |
| } |
| } |
| |
| fn trunc_string_to_n_and_push(s: &str, n: usize, buffer: &mut Vec<u8>) { |
| if s.len() > n { |
| let truncated = s.split_at(n); |
| buffer.extend(truncated.0.as_bytes()); |
| return; |
| } |
| buffer.extend(s.as_bytes()); |
| let unused_bytes = n - s.len(); |
| let old_len = buffer.len(); |
| buffer.resize(old_len + unused_bytes, 0); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| |
| use super::*; |
| use std::net::Ipv4Addr; |
| |
| #[test] |
| fn test_serialize_returns_correct_bytes() { |
| let mut msg = Message::new(); |
| msg.xid = 42; |
| msg.secs = 1024; |
| msg.yiaddr = Ipv4Addr::new(192, 168, 1, 1); |
| msg.sname = String::from("relay.example.com"); |
| msg.file = String::from("boot.img"); |
| msg.options.push(ConfigOption { |
| code: OptionCode::SubnetMask, |
| value: vec![255, 255, 255, 0], |
| }); |
| |
| let bytes = msg.serialize(); |
| |
| assert_eq!(bytes.len(), 243); |
| assert_eq!(bytes[0], 1u8); |
| assert_eq!(bytes[1], 1u8); |
| assert_eq!(bytes[2], 6u8); |
| assert_eq!(bytes[3], 0u8); |
| assert_eq!(bytes[7], 42u8); |
| assert_eq!(bytes[8], 4u8); |
| assert_eq!(bytes[16], 192u8); |
| assert_eq!(bytes[17], 168u8); |
| assert_eq!(bytes[18], 1u8); |
| assert_eq!(bytes[19], 1u8); |
| assert_eq!(bytes[44], 'r' as u8); |
| assert_eq!(bytes[60], 'm' as u8); |
| assert_eq!(bytes[61], 0u8); |
| assert_eq!(bytes[108], 'b' as u8); |
| assert_eq!(bytes[115], 'g' as u8); |
| assert_eq!(bytes[116], 0u8); |
| assert_eq!(bytes[bytes.len() - 1], 255u8); |
| } |
| |
| #[test] |
| fn test_message_from_buffer_returns_correct_message() { |
| use std::string::ToString; |
| |
| let mut buf = Vec::new(); |
| buf.push(1u8); |
| buf.push(1u8); |
| buf.push(6u8); |
| buf.push(0u8); |
| buf.extend_from_slice(b"\x00\x00\x00\x2A"); |
| buf.extend_from_slice(b"\x04\x00"); |
| buf.extend_from_slice(b"\x00\x00"); |
| buf.extend_from_slice(b"\x00\x00\x00\x00"); |
| buf.extend_from_slice(b"\xC0\xA8\x01\x01"); |
| buf.extend_from_slice(b"\x00\x00\x00\x00"); |
| buf.extend_from_slice(b"\x00\x00\x00\x00"); |
| buf.extend_from_slice(b"\x00\x00\x00\x00\x00\x00"); |
| buf.extend_from_slice(b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); |
| buf.extend_from_slice(b"relay.example.com"); |
| let mut old_len = buf.len(); |
| let mut unused_bytes = SNAME_LEN - b"relay.example.com".len(); |
| buf.resize(old_len + unused_bytes, 0u8); |
| buf.extend_from_slice(b"boot.img"); |
| old_len = buf.len(); |
| unused_bytes = FILE_LEN - b"boot.img".len(); |
| buf.resize(old_len + unused_bytes, 0u8); |
| buf.extend_from_slice(b"\x01\x04\xFF\xFF\xFF\x00"); |
| buf.extend_from_slice(b"\x00"); |
| buf.extend_from_slice(b"\x00"); |
| buf.extend_from_slice(b"\x36\x04\xAA\xBB\xCC\xDD"); |
| buf.extend_from_slice(b"\xFF"); |
| |
| let got = Message::from_buffer(&buf).unwrap(); |
| |
| let opt_want1 = ConfigOption { |
| code: OptionCode::SubnetMask, |
| value: vec![255, 255, 255, 0], |
| }; |
| let opt_want2 = ConfigOption { |
| code: OptionCode::Pad, |
| value: vec![], |
| }; |
| let opt_want3 = ConfigOption { |
| code: OptionCode::Pad, |
| value: vec![], |
| }; |
| let opt_want4 = ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![0xAA, 0xBB, 0xCC, 0xDD], |
| }; |
| let want = Message { |
| op: OpCode::BOOTREQUEST, |
| xid: 42, |
| secs: 1024, |
| bdcast_flag: false, |
| ciaddr: Ipv4Addr::new(0, 0, 0, 0), |
| yiaddr: Ipv4Addr::new(192, 168, 1, 1), |
| siaddr: Ipv4Addr::new(0, 0, 0, 0), |
| giaddr: Ipv4Addr::new(0, 0, 0, 0), |
| chaddr: [0, 0, 0, 0, 0, 0], |
| sname: "relay.example.com".to_string(), |
| file: "boot.img".to_string(), |
| options: vec![opt_want1, opt_want2, opt_want3, opt_want4], |
| }; |
| |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_message_from_too_short_buffer_returns_none() { |
| let buf = vec![0u8, 0u8, 0u8]; |
| |
| let got = Message::from_buffer(&buf); |
| |
| assert_eq!(got, None); |
| } |
| |
| #[test] |
| fn test_serialize_with_valid_option_returns_correct_bytes() { |
| let opt = ConfigOption { |
| code: OptionCode::SubnetMask, |
| value: vec![255, 255, 255, 0], |
| }; |
| let mut bytes = Vec::new(); |
| opt.serialize_to(&mut bytes); |
| assert_eq!(bytes.len(), 6); |
| assert_eq!(bytes[0], 1); |
| assert_eq!(bytes[1], 4); |
| assert_eq!(bytes[2], 255); |
| assert_eq!(bytes[3], 255); |
| assert_eq!(bytes[4], 255); |
| assert_eq!(bytes[5], 0); |
| } |
| |
| #[test] |
| fn test_serialize_with_fixed_len_option_returns_correct_bytes() { |
| let opt = ConfigOption { |
| code: OptionCode::End, |
| value: vec![], |
| }; |
| let mut bytes = Vec::new(); |
| opt.serialize_to(&mut bytes); |
| assert_eq!(bytes.len(), 1); |
| assert_eq!(bytes[0], 255); |
| } |
| |
| #[test] |
| fn test_option_from_valid_buffer_has_correct_values() { |
| let buf = vec![1, 4, 255, 255, 255, 0]; |
| let result = ConfigOption::from_buffer(&buf); |
| match result { |
| Some(opt) => { |
| assert_eq!(opt.code as u8, 1); |
| assert_eq!(opt.value, vec![255, 255, 255, 0]); |
| } |
| None => assert!(false), // test failure |
| } |
| } |
| |
| #[test] |
| fn test_option_from_valid_buffer_with_fixed_length_has_correct_values() { |
| let buf = vec![255]; |
| let result = ConfigOption::from_buffer(&buf); |
| match result { |
| Some(opt) => { |
| assert_eq!(opt.code as u8, 255); |
| assert!(opt.value.is_empty()); |
| } |
| None => assert!(false), // test failure |
| } |
| } |
| |
| #[test] |
| fn test_option_from_buffer_with_invalid_code_returns_none() { |
| let buf = vec![72, 2, 1, 2]; |
| let result = ConfigOption::from_buffer(&buf); |
| match result { |
| Some(opt) => assert!(false), // test failure |
| None => assert!(true), // test success |
| } |
| } |
| |
| #[test] |
| fn test_option_from_buffer_with_invalid_length_returns_none() { |
| let buf = vec![1, 6, 255, 255, 255, 0]; |
| let result = ConfigOption::from_buffer(&buf); |
| match result { |
| Some(opt) => assert!(false), // test failure |
| None => assert!(true), // test success |
| } |
| } |
| } |