commit ff62df183eb4fbe71b13e972fb2bf8cc161b80fa Author: pj1234678 <13920460+pj1234678@users.noreply.github.com> Date: Fri Oct 27 19:36:06 2023 -0700 Initial Upload of Files diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..df4b6a8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,370 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const_fn" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" + +[[package]] +name = "dhcp4r" +version = "0.2.3" +dependencies = [ + "time", +] + +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "serde_json" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "standback" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e113fb6f3de07a243d434a56ec6f186dfd51cb08448239fe7bcae73f87ff28ff" +dependencies = [ + "version_check", +] + +[[package]] +name = "stdweb" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2", + "quote", + "serde", + "serde_derive", + "serde_json", + "sha1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" +dependencies = [ + "const_fn", + "libc", + "standback", + "stdweb", + "time-macros", + "version_check", + "winapi", +] + +[[package]] +name = "time-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" +dependencies = [ + "proc-macro-hack", + "time-macros-impl", +] + +[[package]] +name = "time-macros-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" +dependencies = [ + "proc-macro-hack", + "proc-macro2", + "quote", + "standback", + "syn 1.0.109", +] + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.29", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e7f72b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "dhcp4r" +version = "0.2.3" +authors = ["Richard Warburton "] +description = "IPv4 DHCP library with working server example." +edition = "2018" + +[profile.dev] +opt-level = 0 + +[profile.release] +opt-level = 3 + + +# These URLs point to more information about the repository. +#documentation = "..." +#homepage = "..." + +# This points to a file in the repository (relative to this `Cargo.toml`). The +# contents of this file are stored and indexed in the registry. +# readme = "..." + +[dependencies] + + +[dev-dependencies] +time = "0.2" diff --git a/examples/monitor.rs b/examples/monitor.rs new file mode 100644 index 0000000..36a4915 --- /dev/null +++ b/examples/monitor.rs @@ -0,0 +1,43 @@ +extern crate dhcp4r; +extern crate time; + +use dhcp4r::{options, packet, server}; +use std::net::{Ipv4Addr, UdpSocket}; + +fn main() { + server::Server::serve( + UdpSocket::bind("0.0.0.0:67").unwrap(), + Ipv4Addr::new(0, 0, 0, 0), + MyServer {}, + ); +} + +struct MyServer {} + +impl server::Handler for MyServer { + fn handle_request(&mut self, _: &server::Server, in_packet: packet::Packet) { + match in_packet.message_type() { + Ok(options::MessageType::Request) => { + let req_ip = match in_packet.option(options::REQUESTED_IP_ADDRESS) { + Some(options::DhcpOption::RequestedIpAddress(x)) => x.clone(), + _ => in_packet.ciaddr, + }; + println!( + "{}\t{}\t{}\tOnline", + time::OffsetDateTime::now_local().format("%Y-%m-%dT%H:%M:%S"), + chaddr(&in_packet.chaddr), + Ipv4Addr::from(req_ip) + ); + } + _ => {} + } + } +} + +/// Formats byte array machine address into hex pairs separated by colons. +/// Array must be at least one byte long. +fn chaddr(a: &[u8]) -> String { + a[1..].iter().fold(format!("{:02x}", a[0]), |acc, &b| { + format!("{}:{:02x}", acc, &b) + }) +} diff --git a/examples/server.rs b/examples/server.rs new file mode 100644 index 0000000..f09cd41 --- /dev/null +++ b/examples/server.rs @@ -0,0 +1,202 @@ +#[macro_use(u32_bytes, bytes_u32)] +extern crate dhcp4r; + +use std::collections::HashMap; +use std::net::{Ipv4Addr, UdpSocket}; +use std::ops::Add; +use std::time::{Duration, Instant}; +use std::fs::File; +use std::io::{BufRead, BufReader}; + +use dhcp4r::{options, packet, server}; + +// Server configuration +const SERVER_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 1); +const IP_START: [u8; 4] = [192, 168, 2, 2]; +const SUBNET_MASK: Ipv4Addr = Ipv4Addr::new(255, 255, 255, 0); +const DNS_IPS: [Ipv4Addr; 1] = [ + // Google DNS servers + Ipv4Addr::new(8, 8, 8, 8), +]; +const ROUTER_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 1); +const BROADCAST_IP: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 255); +const LEASE_DURATION_SECS: u32 = 86400; +const LEASE_NUM: u32 = 252; + +// Derived constants +const IP_START_NUM: u32 = bytes_u32!(IP_START); +const INFINITE_LEASE: Option = None; // Special value for infinite lease + + +fn main() { + let socket = UdpSocket::bind("0.0.0.0:67").unwrap(); + socket.set_broadcast(true).unwrap(); + + let mut leases: HashMap)> = HashMap::new(); + // Read and populate leases from the file +if let Ok(file) = File::open("leases") { + let reader = BufReader::new(file); + for line in reader.lines() { + if let Ok(line) = line { + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() == 2 { + let mac_parts: Vec = parts[0] + .split(':') + .filter_map(|part| u8::from_str_radix(part, 16).ok()) + .collect(); + + if mac_parts.len() == 6 { + let mut mac = [0u8; 6]; + mac.copy_from_slice(&mac_parts); + + let ip = parts[1].trim().parse::().unwrap(); + leases.insert(ip, (mac, INFINITE_LEASE)); + } + } + } + } +} else { + eprintln!("Failed to open leases file. Continuing..."); + //return; +} + + let ms = MyServer { + leases, + last_lease: 0, + lease_duration: Duration::new(LEASE_DURATION_SECS as u64, 0), + }; + + server::Server::serve(socket, SERVER_IP, BROADCAST_IP, ms); +} + +struct MyServer { + leases: HashMap)>, // Ipv4Addr -> (MAC address, lease duration) mapping + last_lease: u32, + lease_duration: Duration, +} + +impl server::Handler for MyServer { + fn handle_request(&mut self, server: &server::Server, in_packet: packet::Packet) { + match in_packet.message_type() { + Ok(options::MessageType::Discover) => { + + // Otherwise prefer existing (including expired if available) + if let Some(ip) = self.current_lease(&in_packet.chaddr) { + println!("Sending Reply to discover"); + reply(server, options::MessageType::Offer, in_packet, &ip); + return; + } + // Otherwise choose a free ip if available + for _ in 0..LEASE_NUM { + self.last_lease = (self.last_lease + 1) % LEASE_NUM; + if self.available( + &in_packet.chaddr, + &((IP_START_NUM + &self.last_lease).into()), + ) { + println!("Sending Reply to discover"); + reply( + server, + options::MessageType::Offer, + in_packet, + &((IP_START_NUM + &self.last_lease).into()), + ); + break; + } + } + } + + Ok(options::MessageType::Request) => { + // Ignore requests to alternative DHCP server + if !server.for_this_server(&in_packet) { + //println!("Not for this server"); + // return; + } + + let req_ip = match in_packet.option(options::REQUESTED_IP_ADDRESS) { + Some(options::DhcpOption::RequestedIpAddress(x)) => *x, + _ => in_packet.ciaddr, + }; + for (ip, (mac, _)) in &self.leases { + println!("IP: {:?}, MAC: {:?}", ip, mac); + } + if let Some(ip) = self.current_lease(&in_packet.chaddr) { + println!("Found Current Lease"); + reply(server, options::MessageType::Ack, in_packet, &ip); + return; + } + if !&self.available(&in_packet.chaddr, &req_ip) { + println!("Sending Reply to Request"); + nak(server, in_packet, "Requested IP not available"); + return; + } + self.leases.insert(req_ip, (in_packet.chaddr, Some(Instant::now().add(self.lease_duration)))); + println!("Sending Reply to Request"); + reply(server, options::MessageType::Ack, in_packet, &req_ip); + } + + Ok(options::MessageType::Release) | Ok(options::MessageType::Decline) => { + // Ignore requests to alternative DHCP server + if !server.for_this_server(&in_packet) { + return; + } + if let Some(ip) = self.current_lease(&in_packet.chaddr) { + self.leases.remove(&ip); + } + } + + // TODO - not necessary but support for dhcp4r::INFORM might be nice + _ => {} + } + } +} + +impl MyServer { + fn available(&self, chaddr: &[u8; 6], addr: &Ipv4Addr) -> bool { + let pos: u32 = (*addr).into(); + pos >= IP_START_NUM + && pos < IP_START_NUM + LEASE_NUM + && match self.leases.get(addr) { + Some((mac, expiry)) => { + *mac == *chaddr || expiry.map_or(true, |exp| Instant::now().gt(&exp)) + } + None => true, + } + } + fn current_lease(&self, chaddr: &[u8; 6]) -> Option { + + for (i, v) in &self.leases { + if v.0 == *chaddr { + return Some(*i); + } + } + None + } +} + +fn reply( + s: &server::Server, + msg_type: options::MessageType, + req_packet: packet::Packet, + offer_ip: &Ipv4Addr, +) { + let _ = s.reply( + msg_type, + vec![ + options::DhcpOption::IpAddressLeaseTime(LEASE_DURATION_SECS), + options::DhcpOption::SubnetMask(SUBNET_MASK), + options::DhcpOption::Router(vec![ROUTER_IP]), + options::DhcpOption::DomainNameServer(DNS_IPS.to_vec()), + ], + *offer_ip, + req_packet, + ); +} + +fn nak(s: &server::Server, req_packet: packet::Packet, message: &str) { + let _ = s.reply( + options::MessageType::Nak, + vec![options::DhcpOption::Message(message.to_string())], + Ipv4Addr::new(0, 0, 0, 0), + req_packet, + ); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f13167b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,35 @@ + + +pub mod options; +pub mod packet; +pub mod server; + +/// Converts a u32 to 4 bytes (Big endian) +#[macro_export] +macro_rules! u32_bytes { + ( $x:expr ) => { + [ + ($x >> 24) as u8, + ($x >> 16) as u8, + ($x >> 8) as u8, + $x as u8, + ] + }; +} + +/// Converts 4 bytes to a u32 (Big endian) +#[macro_export] +macro_rules! bytes_u32 { + ( $x:expr ) => { + ($x[0] as u32) * (1 << 24) + + ($x[1] as u32) * (1 << 16) + + ($x[2] as u32) * (1 << 8) + + ($x[3] as u32) + }; +} + +#[cfg(test)] +mod tests { + #[test] + fn it_works() {} +} diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 0000000..e2508e5 --- /dev/null +++ b/src/options.rs @@ -0,0 +1,367 @@ +///use num_traits::FromPrimitive; +use std::net::Ipv4Addr; + +#[derive(PartialEq, Clone, Debug)] +pub struct RawDhcpOption { + pub code: u8, + pub data: Vec, +} + +#[derive(PartialEq, Debug)] +pub enum DhcpOption { + DhcpMessageType(MessageType), + ServerIdentifier(Ipv4Addr), + ParameterRequestList(Vec), + RequestedIpAddress(Ipv4Addr), + HostName(String), + Router(Vec), + DomainNameServer(Vec), + IpAddressLeaseTime(u32), + SubnetMask(Ipv4Addr), + Message(String), + Unrecognized(RawDhcpOption), +} + +impl DhcpOption { + pub fn to_raw(&self) -> RawDhcpOption { + match self { + Self::DhcpMessageType(mtype) => RawDhcpOption { + code: DHCP_MESSAGE_TYPE, + data: vec![*mtype as u8], + }, + Self::ServerIdentifier(addr) => RawDhcpOption { + code: SERVER_IDENTIFIER, + data: addr.octets().to_vec(), + }, + Self::ParameterRequestList(prl) => RawDhcpOption { + code: PARAMETER_REQUEST_LIST, + data: prl.clone(), + }, + Self::RequestedIpAddress(addr) => RawDhcpOption { + code: REQUESTED_IP_ADDRESS, + data: addr.octets().to_vec(), + }, + Self::HostName(name) => RawDhcpOption { + code: HOST_NAME, + data: name.as_bytes().to_vec(), + }, + Self::Router(addrs) => RawDhcpOption { + code: ROUTER, + data: { + let mut v = vec![]; + for a in addrs { + v.extend(a.octets().iter()); + } + v + }, + }, + Self::DomainNameServer(addrs) => RawDhcpOption { + code: DOMAIN_NAME_SERVER, + data: { + let mut v = vec![]; + for a in addrs { + v.extend(a.octets().iter()); + } + v + }, + }, + Self::IpAddressLeaseTime(secs) => RawDhcpOption { + code: IP_ADDRESS_LEASE_TIME, + data: secs.to_be_bytes().to_vec(), + }, + Self::SubnetMask(mask) => RawDhcpOption { + code: SUBNET_MASK, + data: mask.octets().to_vec(), + }, + Self::Message(msg) => RawDhcpOption { + code: MESSAGE, + data: msg.as_bytes().to_vec(), + }, + Self::Unrecognized(raw) => raw.clone(), + } + } + + pub fn code(&self) -> u8 { + match self { + Self::DhcpMessageType(_) => DHCP_MESSAGE_TYPE, + Self::ServerIdentifier(_) => SERVER_IDENTIFIER, + Self::ParameterRequestList(_) => PARAMETER_REQUEST_LIST, + Self::RequestedIpAddress(_) => REQUESTED_IP_ADDRESS, + Self::HostName(_) => HOST_NAME, + Self::Router(_) => ROUTER, + Self::DomainNameServer(_) => DOMAIN_NAME_SERVER, + Self::IpAddressLeaseTime(_) => IP_ADDRESS_LEASE_TIME, + Self::SubnetMask(_) => SUBNET_MASK, + Self::Message(_) => MESSAGE, + Self::Unrecognized(x) => x.code, + } + } +} + +// DHCP Options; +pub const SUBNET_MASK: u8 = 1; +pub const TIME_OFFSET: u8 = 2; +pub const ROUTER: u8 = 3; +pub const TIME_SERVER: u8 = 4; +pub const NAME_SERVER: u8 = 5; +pub const DOMAIN_NAME_SERVER: u8 = 6; +pub const LOG_SERVER: u8 = 7; +pub const COOKIE_SERVER: u8 = 8; +pub const LPR_SERVER: u8 = 9; +pub const IMPRESS_SERVER: u8 = 10; +pub const RESOURCE_LOCATION_SERVER: u8 = 11; +pub const HOST_NAME: u8 = 12; +pub const BOOT_FILE_SIZE: u8 = 13; +pub const MERIT_DUMP_FILE: u8 = 14; +pub const DOMAIN_NAME: u8 = 15; +pub const SWAP_SERVER: u8 = 16; +pub const ROOT_PATH: u8 = 17; +pub const EXTENSIONS_PATH: u8 = 18; + +// IP LAYER PARAMETERS PER HOST; +pub const IP_FORWARDING_ENABLE_DISABLE: u8 = 19; +pub const NON_LOCAL_SOURCE_ROUTING_ENABLE_DISABLE: u8 = 20; +pub const POLICY_FILTER: u8 = 21; +pub const MAXIMUM_DATAGRAM_REASSEMBLY_SIZE: u8 = 22; +pub const DEFAULT_IP_TIME_TO_LIVE: u8 = 23; +pub const PATH_MTU_AGING_TIMEOUT: u8 = 24; +pub const PATH_MTU_PLATEAU_TABLE: u8 = 25; + +// IP LAYER PARAMETERS PER INTERFACE; +pub const INTERFACE_MTU: u8 = 26; +pub const ALL_SUBNETS_ARE_LOCAL: u8 = 27; +pub const BROADCAST_ADDRESS: u8 = 28; +pub const PERFORM_MASK_DISCOVERY: u8 = 29; +pub const MASK_SUPPLIER: u8 = 30; +pub const PERFORM_ROUTER_DISCOVERY: u8 = 31; +pub const ROUTER_SOLICITATION_ADDRESS: u8 = 32; +pub const STATIC_ROUTE: u8 = 33; + +// LINK LAYER PARAMETERS PER INTERFACE; +pub const TRAILER_ENCAPSULATION: u8 = 34; +pub const ARP_CACHE_TIMEOUT: u8 = 35; +pub const ETHERNET_ENCAPSULATION: u8 = 36; + +// TCP PARAMETERS; +pub const TCP_DEFAULT_TTL: u8 = 37; +pub const TCP_KEEPALIVE_INTERVAL: u8 = 38; +pub const TCP_KEEPALIVE_GARBAGE: u8 = 39; + +// APPLICATION AND SERVICE PARAMETERS; +pub const NETWORK_INFORMATION_SERVICE_DOMAIN: u8 = 40; +pub const NETWORK_INFORMATION_SERVERS: u8 = 41; +pub const NETWORK_TIME_PROTOCOL_SERVERS: u8 = 42; +pub const VENDOR_SPECIFIC_INFORMATION: u8 = 43; +pub const NETBIOS_OVER_TCPIP_NAME_SERVER: u8 = 44; +pub const NETBIOS_OVER_TCPIP_DATAGRAM_DISTRIBUTION_SERVER: u8 = 45; +pub const NETBIOS_OVER_TCPIP_NODE_TYPE: u8 = 46; +pub const NETBIOS_OVER_TCPIP_SCOPE: u8 = 47; +pub const XWINDOW_SYSTEM_FONT_SERVER: u8 = 48; +pub const XWINDOW_SYSTEM_DISPLAY_MANAGER: u8 = 49; +pub const NETWORK_INFORMATION_SERVICEPLUS_DOMAIN: u8 = 64; +pub const NETWORK_INFORMATION_SERVICEPLUS_SERVERS: u8 = 65; +pub const MOBILE_IP_HOME_AGENT: u8 = 68; +pub const SIMPLE_MAIL_TRANSPORT_PROTOCOL: u8 = 69; +pub const POST_OFFICE_PROTOCOL_SERVER: u8 = 70; +pub const NETWORK_NEWS_TRANSPORT_PROTOCOL: u8 = 71; +pub const DEFAULT_WORLD_WIDE_WEB_SERVER: u8 = 72; +pub const DEFAULT_FINGER_SERVER: u8 = 73; +pub const DEFAULT_INTERNET_RELAY_CHAT_SERVER: u8 = 74; +pub const STREETTALK_SERVER: u8 = 75; +pub const STREETTALK_DIRECTORY_ASSISTANCE: u8 = 76; + +pub const RELAY_AGENT_INFORMATION: u8 = 82; + +// DHCP EXTENSIONS +pub const REQUESTED_IP_ADDRESS: u8 = 50; +pub const IP_ADDRESS_LEASE_TIME: u8 = 51; +pub const OVERLOAD: u8 = 52; +pub const DHCP_MESSAGE_TYPE: u8 = 53; +pub const SERVER_IDENTIFIER: u8 = 54; +pub const PARAMETER_REQUEST_LIST: u8 = 55; +pub const MESSAGE: u8 = 56; +pub const MAXIMUM_DHCP_MESSAGE_SIZE: u8 = 57; +pub const RENEWAL_TIME_VALUE: u8 = 58; +pub const REBINDING_TIME_VALUE: u8 = 59; +pub const VENDOR_CLASS_IDENTIFIER: u8 = 60; +pub const CLIENT_IDENTIFIER: u8 = 61; + +pub const TFTP_SERVER_NAME: u8 = 66; +pub const BOOTFILE_NAME: u8 = 67; + +pub const USER_CLASS: u8 = 77; + +pub const CLIENT_ARCHITECTURE: u8 = 93; + +pub const TZ_POSIX_STRING: u8 = 100; +pub const TZ_DATABASE_STRING: u8 = 101; + +pub const CLASSLESS_ROUTE_FORMAT: u8 = 121; + +/// Returns title of DHCP Option code, if known. +pub fn title(code: u8) -> Option<&'static str> { + Some(match code { + SUBNET_MASK => "Subnet Mask", + + TIME_OFFSET => "Time Offset", + ROUTER => "Router", + TIME_SERVER => "Time Server", + NAME_SERVER => "Name Server", + DOMAIN_NAME_SERVER => "Domain Name Server", + LOG_SERVER => "Log Server", + COOKIE_SERVER => "Cookie Server", + LPR_SERVER => "LPR Server", + IMPRESS_SERVER => "Impress Server", + RESOURCE_LOCATION_SERVER => "Resource Location Server", + HOST_NAME => "Host Name", + BOOT_FILE_SIZE => "Boot File Size", + MERIT_DUMP_FILE => "Merit Dump File", + DOMAIN_NAME => "Domain Name", + SWAP_SERVER => "Swap Server", + ROOT_PATH => "Root Path", + EXTENSIONS_PATH => "Extensions Path", + + // IP LAYER PARAMETERS PER HOST", + IP_FORWARDING_ENABLE_DISABLE => "IP Forwarding Enable/Disable", + NON_LOCAL_SOURCE_ROUTING_ENABLE_DISABLE => "Non-Local Source Routing Enable/Disable", + POLICY_FILTER => "Policy Filter", + MAXIMUM_DATAGRAM_REASSEMBLY_SIZE => "Maximum Datagram Reassembly Size", + DEFAULT_IP_TIME_TO_LIVE => "Default IP Time-to-live", + PATH_MTU_AGING_TIMEOUT => "Path MTU Aging Timeout", + PATH_MTU_PLATEAU_TABLE => "Path MTU Plateau Table", + + // IP LAYER PARAMETERS PER INTERFACE", + INTERFACE_MTU => "Interface MTU", + ALL_SUBNETS_ARE_LOCAL => "All Subnets are Local", + BROADCAST_ADDRESS => "Broadcast Address", + PERFORM_MASK_DISCOVERY => "Perform Mask Discovery", + MASK_SUPPLIER => "Mask Supplier", + PERFORM_ROUTER_DISCOVERY => "Perform Router Discovery", + ROUTER_SOLICITATION_ADDRESS => "Router Solicitation Address", + STATIC_ROUTE => "Static Route", + + // LINK LAYER PARAMETERS PER INTERFACE", + TRAILER_ENCAPSULATION => "Trailer Encapsulation", + ARP_CACHE_TIMEOUT => "ARP Cache Timeout", + ETHERNET_ENCAPSULATION => "Ethernet Encapsulation", + + // TCP PARAMETERS", + TCP_DEFAULT_TTL => "TCP Default TTL", + TCP_KEEPALIVE_INTERVAL => "TCP Keepalive Interval", + TCP_KEEPALIVE_GARBAGE => "TCP Keepalive Garbage", + + // APPLICATION AND SERVICE PARAMETERS", + NETWORK_INFORMATION_SERVICE_DOMAIN => "Network Information Service Domain", + NETWORK_INFORMATION_SERVERS => "Network Information Servers", + NETWORK_TIME_PROTOCOL_SERVERS => "Network Time Protocol Servers", + VENDOR_SPECIFIC_INFORMATION => "Vendor Specific Information", + NETBIOS_OVER_TCPIP_NAME_SERVER => "NetBIOS over TCP/IP Name Server", + NETBIOS_OVER_TCPIP_DATAGRAM_DISTRIBUTION_SERVER => { + "NetBIOS over TCP/IP Datagram Distribution Server" + } + NETBIOS_OVER_TCPIP_NODE_TYPE => "NetBIOS over TCP/IP Node Type", + NETBIOS_OVER_TCPIP_SCOPE => "NetBIOS over TCP/IP Scope", + XWINDOW_SYSTEM_FONT_SERVER => "X Window System Font Server", + XWINDOW_SYSTEM_DISPLAY_MANAGER => "X Window System Display Manager", + NETWORK_INFORMATION_SERVICEPLUS_DOMAIN => "Network Information Service+ Domain", + NETWORK_INFORMATION_SERVICEPLUS_SERVERS => "Network Information Service+ Servers", + MOBILE_IP_HOME_AGENT => "Mobile IP Home Agent", + SIMPLE_MAIL_TRANSPORT_PROTOCOL => "Simple Mail Transport Protocol (SMTP) Server", + POST_OFFICE_PROTOCOL_SERVER => "Post Office Protocol (POP3) Server", + NETWORK_NEWS_TRANSPORT_PROTOCOL => "Network News Transport Protocol (NNTP) Server", + DEFAULT_WORLD_WIDE_WEB_SERVER => "Default World Wide Web (WWW) Server", + DEFAULT_FINGER_SERVER => "Default Finger Server", + DEFAULT_INTERNET_RELAY_CHAT_SERVER => "Default Internet Relay Chat (IRC) Server", + STREETTALK_SERVER => "StreetTalk Server", + STREETTALK_DIRECTORY_ASSISTANCE => "StreetTalk Directory Assistance (STDA) Server", + + RELAY_AGENT_INFORMATION => "Relay Agent Information", + + // DHCP EXTENSIONS + REQUESTED_IP_ADDRESS => "Requested IP Address", + IP_ADDRESS_LEASE_TIME => "IP Address Lease Time", + OVERLOAD => "Overload", + DHCP_MESSAGE_TYPE => "DHCP Message Type", + SERVER_IDENTIFIER => "Server Identifier", + PARAMETER_REQUEST_LIST => "Parameter Request List", + MESSAGE => "Message", + MAXIMUM_DHCP_MESSAGE_SIZE => "Maximum DHCP Message Size", + RENEWAL_TIME_VALUE => "Renewal (T1) Time Value", + REBINDING_TIME_VALUE => "Rebinding (T2) Time Value", + VENDOR_CLASS_IDENTIFIER => "Vendor class identifier", + CLIENT_IDENTIFIER => "Client-identifier", + + // Find below + TFTP_SERVER_NAME => "TFTP server name", + BOOTFILE_NAME => "Bootfile name", + + USER_CLASS => "User Class", + + CLIENT_ARCHITECTURE => "Client Architecture", + + TZ_POSIX_STRING => "TZ-POSIX String", + TZ_DATABASE_STRING => "TZ-Database String", + CLASSLESS_ROUTE_FORMAT => "Classless Route Format", + + _ => return None, + }) +} + +/// +/// DHCP Message Type. +/// +/// # Standards +/// +/// The semantics of the various DHCP message types are described in RFC 2131 (see Table 2). +/// Their numeric values are described in Section 9.6 of RFC 2132, which begins: +/// +/// > This option is used to convey the type of the DHCP message. The code for this option is 53, +/// > and its length is 1. +/// +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum MessageType { + /// Client broadcast to locate available servers. + Discover = 1, + + /// Server to client in response to DHCPDISCOVER with offer of configuration parameters. + Offer = 2, + + /// Client message to servers either (a) requesting offered parameters from one server and + /// implicitly declining offers from all others, (b) confirming correctness of previously + /// allocated address after, e.g., system reboot, or (c) extending the lease on a particular + /// network address. + Request = 3, + + /// Client to server indicating network address is already in use. + Decline = 4, + + /// Server to client with configuration parameters, including committed network address. + Ack = 5, + + /// Server to client indicating client's notion of network address is incorrect (e.g., client + /// has moved to new subnet) or client's lease as expired. + Nak = 6, + + /// Client to server relinquishing network address and cancelling remaining lease. + Release = 7, + + /// Client to server, asking only for local configuration parameters; client already has + /// externally configured network address. + Inform = 8, +} + +impl MessageType { + pub fn from(val: u8) -> Result { + match val { + 1 => Ok(MessageType::Discover), + 2 => Ok(MessageType::Offer), + 3 => Ok(MessageType::Request), + 4 => Ok(MessageType::Decline), + 5 => Ok(MessageType::Ack), + 6 => Ok(MessageType::Nak), + 7 => Ok(MessageType::Release), + 8 => Ok(MessageType::Inform), + _ => Err(format!("Invalid DHCP Message Type: {:?}", val)), + } + } +} \ No newline at end of file diff --git a/src/packet.rs b/src/packet.rs new file mode 100644 index 0000000..be1d269 --- /dev/null +++ b/src/packet.rs @@ -0,0 +1,311 @@ +use crate::options::*; + +use std::net::Ipv4Addr; + +pub enum CustomErr { + NomError((I, ErrorKind)), + NonUtf8String, + UnrecognizedMessageType, + InvalidHlen, +} + +pub enum ErrorKind { + Tag, + MapRes, + ManyTill, + Eof, + Custom(u32), +} + +type IResult = Result<(I, O), CustomErr>; + + + +/// DHCP Packet Structure +#[derive(Debug)] +pub struct Packet { + pub reply: bool, // false = request, true = reply + pub hops: u8, + pub xid: u32, // Random identifier + pub secs: u16, + pub broadcast: bool, + pub ciaddr: Ipv4Addr, + pub yiaddr: Ipv4Addr, + pub siaddr: Ipv4Addr, + pub giaddr: Ipv4Addr, + pub chaddr: [u8; 6], + pub options: Vec, +} + +fn decode_reply(input: &[u8]) -> IResult<&[u8], bool> { + let (input, reply) = custom_take(1usize)(input)?; + Ok(( + input, + match reply[0] { + BOOT_REPLY => true, + BOOT_REQUEST => false, + _ => { + // @TODO: Throw an error + false + } + }, + )) +} + +fn decode_ipv4(p: &[u8]) -> IResult<&[u8], Ipv4Addr> { + let (input, addr) = custom_take(4usize)(p)?; + Ok((input, Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]))) +} +fn custom_many0(mut f: F) -> impl FnMut(I) -> IResult> +where + I: Clone + PartialEq, + F: FnMut(I) -> IResult, +{ + move |input: I| { + let mut acc = Vec::new(); + let mut remaining = input.clone(); + + loop { + match f(remaining.clone()) { + Ok((input, o)) => { + if input == remaining { + return Ok((input, acc)); + } + acc.push(o); + remaining = input; + } + Err(CustomErr::NomError(_)) => return Ok((remaining, acc)), + Err(e) => return Err(e), + } + } + } +} +pub fn decode_option(input: &[u8]) -> IResult<&[u8], DhcpOption> { + let (input, code) = custom_be_u8(input)?; + assert!(code != END); + + let (input, len) = custom_be_u8(input)?; + let (input, data) = custom_take(len.into())(input)?; + let option = match code { + DHCP_MESSAGE_TYPE => DhcpOption::DhcpMessageType(match MessageType::from(custom_be_u8(data)?.1) { + Ok(x) => x, + Err(_) => return Err(CustomErr::UnrecognizedMessageType), + }), + SERVER_IDENTIFIER => DhcpOption::ServerIdentifier(decode_ipv4(data)?.1), + PARAMETER_REQUEST_LIST => DhcpOption::ParameterRequestList(data.to_vec()), + REQUESTED_IP_ADDRESS => DhcpOption::RequestedIpAddress(decode_ipv4(data)?.1), + HOST_NAME => DhcpOption::HostName(match std::str::from_utf8(data) { + Ok(s) => s.to_string(), + Err(_) => return Err(CustomErr::NonUtf8String), + }), + ROUTER => DhcpOption::Router(custom_many0(decode_ipv4)(data)?.1), + DOMAIN_NAME_SERVER => DhcpOption::DomainNameServer(custom_many0(decode_ipv4)(data)?.1), + IP_ADDRESS_LEASE_TIME => DhcpOption::IpAddressLeaseTime(custom_be_u32(data)?.1), + SUBNET_MASK => DhcpOption::SubnetMask(decode_ipv4(data)?.1), + MESSAGE => DhcpOption::Message(match std::str::from_utf8(data) { + Ok(s) => s.to_string(), + Err(_) => return Err(CustomErr::NonUtf8String), + }), + _ => DhcpOption::Unrecognized(RawDhcpOption { + code, + data: data.to_vec(), + }), + }; + Ok((input, option)) +} +fn custom_take<'a>(n: usize) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], &'a [u8]> { + move |input: &'a [u8]| { + if input.len() >= n { + Ok((&input[n..], &input[0..n])) + } else { + Err(CustomErr::InvalidHlen) + } + } +} + +fn custom_tag<'a>(tag: &'static [u8]) -> impl Fn(&'a [u8]) -> IResult<&'a [u8], &'a [u8]> { + move |input: &'a [u8]| { + if input.starts_with(tag) { + Ok((&input[tag.len()..], &input[..tag.len()])) + } else { + Err(CustomErr::NomError((input, ErrorKind::Tag))) + } + } +} +fn custom_be_u8(input: &[u8]) -> IResult<&[u8], u8> { + if input.len() < 1 { + return Err(CustomErr::InvalidHlen); + } + + Ok((&input[1..], input[0])) +} + +fn custom_be_u16(input: &[u8]) -> IResult<&[u8], u16> { + if input.len() < 2 { + return Err(CustomErr::InvalidHlen); + } + + let value = u16::from_be_bytes([input[0], input[1]]); + Ok((&input[2..], value)) +} +fn custom_be_u32(input: &[u8]) -> IResult<&[u8], u32> { + if input.len() < 4 { + return Err(CustomErr::InvalidHlen); + } + + let value = u32::from_be_bytes([input[0], input[1], input[2], input[3]]); + Ok((&input[4..], value)) +} + +/// Parses Packet from byte array +fn decode(input: &[u8]) -> IResult<&[u8], Packet> { + let (options_input, input) = custom_take(236usize)(input)?; + + let (input, reply) = decode_reply(input)?; + let (input, _htype) = custom_take(1usize)(input)?; + let (input, hlen) = custom_be_u8(input)?; + let (input, hops) = custom_be_u8(input)?; + let (input, xid) = custom_be_u32(input)?; + let (input, secs) = custom_be_u16(input)?; + let (input, flags) = custom_be_u16(input)?; + let (input, ciaddr) = decode_ipv4(input)?; + let (input, yiaddr) = decode_ipv4(input)?; + let (input, siaddr) = decode_ipv4(input)?; + let (input, giaddr) = decode_ipv4(input)?; + + if hlen != 6 { + + return Err(CustomErr::InvalidHlen); + } + let (_, chaddr) = custom_take(6usize)(input)?; + + let input = options_input; + let (input, _) = custom_tag(&COOKIE)(input)?; + + + let mut options = Vec::new(); + let mut rest = input; + + loop { + match decode_option(rest) { + Ok((new_rest, option)) => { + rest = new_rest; + options.push(option); + if rest.starts_with(&[END]) { + break; + } + } + Err(_) => break, + } + } + + let input = rest.split_at(1).1; // Skip the END tag byte + + Ok(( + input, + Packet { + reply, + hops, + secs, + broadcast: flags & 128 == 128, + ciaddr, + yiaddr, + siaddr, + giaddr, + options, + chaddr: [ + chaddr[0], chaddr[1], chaddr[2], chaddr[3], chaddr[4], chaddr[5], + ], + xid, + }, + )) +} + +impl Packet { + pub fn from(input: &[u8]) -> Result> { + + Ok(decode(input)?.1) + } + + /// Extracts requested option payload from packet if available + pub fn option(&self, code: u8) -> Option<&DhcpOption> { + for option in &self.options { + if option.code() == code { + return Some(&option); + } + } + None + } + + /// Convenience function for extracting a packet's message type. + pub fn message_type(&self) -> Result { + match self.option(DHCP_MESSAGE_TYPE) { + Some(DhcpOption::DhcpMessageType(msgtype)) => Ok(*msgtype), + Some(option) => Err(format![ + "Got wrong enum code {} for DHCP_MESSAGE_TYPE", + option.code() + ]), + None => Err("Packet does not have MessageType option".to_string()), + } + } + pub fn encode<'a>(&'a self, p: &'a mut [u8]) -> &[u8] { + let broadcast_flag = if self.broadcast { 128 } else { 0 }; + let mut length = 240; + + p[..12].copy_from_slice(&[ + if self.reply { BOOT_REPLY } else { BOOT_REQUEST }, + 1, + 6, + self.hops, + ((self.xid >> 24) & 0xFF) as u8, + ((self.xid >> 16) & 0xFF) as u8, + ((self.xid >> 8) & 0xFF) as u8, + (self.xid & 0xFF) as u8, + (self.secs >> 8) as u8, + (self.secs & 255) as u8, + broadcast_flag, + 0, + ]); + + p[12..16].copy_from_slice(&self.ciaddr.octets()); + p[16..20].copy_from_slice(&self.yiaddr.octets()); + p[20..24].copy_from_slice(&self.siaddr.octets()); + p[24..28].copy_from_slice(&self.giaddr.octets()); + p[28..34].copy_from_slice(&self.chaddr); + p[34..236].fill(0); + p[236..240].copy_from_slice(&COOKIE); + + for option in &self.options { + let option = option.to_raw(); + let option_len = option.data.len(); + if length + 2 + option_len >= 272 { + break; + } + if let Some(dest) = p.get_mut(length..length + 2 + option_len) { + dest[0] = option.code; + dest[1] = option_len as u8; + dest[2..].copy_from_slice(&option.data); + } + length += 2 + option_len; + } + + if let Some(end_segment) = p.get_mut(length..length + 1) { + end_segment[0] = END; + } + length += 1; + + if let Some(pad_segment) = p.get_mut(length..272) { + pad_segment.fill(PAD); + } + + &p[..length] +} +} + +const COOKIE: [u8; 4] = [99, 130, 83, 99]; + +const BOOT_REQUEST: u8 = 1; // From Client; +const BOOT_REPLY: u8 = 2; // From Server; + +const END: u8 = 255; +const PAD: u8 = 0; diff --git a/src/server.rs b/src/server.rs new file mode 100644 index 0000000..480b315 --- /dev/null +++ b/src/server.rs @@ -0,0 +1,174 @@ +use std::cell::Cell; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; + +use crate::options; +use crate::options::{DhcpOption, MessageType}; +use crate::packet::*; + +///! This is a convenience module that simplifies the writing of a DHCP server service. + +pub struct Server { + out_buf: Cell<[u8; 1500]>, + socket: UdpSocket, + src: SocketAddr, + server_ip: Ipv4Addr, + broadcast_ip: Ipv4Addr, +} + +pub trait Handler { + fn handle_request(&mut self, server: &Server, in_packet: Packet); +} + +pub fn filter_options_by_req(opts: &mut Vec, req_params: &[u8]) { + let mut pos = 0; + let h = &[ + options::DHCP_MESSAGE_TYPE as u8, + options::SERVER_IDENTIFIER as u8, + options::SUBNET_MASK as u8, + options::IP_ADDRESS_LEASE_TIME as u8, + options::DOMAIN_NAME_SERVER as u8, + options::ROUTER as u8, + ] as &[u8]; + + // Process options from req_params + for r in req_params.iter() { + let mut found = false; + for (i, o) in opts[pos..].iter().enumerate() { + if o.code() == *r { + found = true; + if pos + i != pos { + opts.swap(pos + i, pos); + } + pos += 1; + break; + } + } + if !found { + // Option not found, continue searching + } + } + + // Process options from h + for r in h.iter() { + let mut found = false; + for (i, o) in opts[pos..].iter().enumerate() { + if o.code() == *r { + found = true; + if pos + i != pos { + opts.swap(pos + i, pos); + } + pos += 1; + break; + } + } + if !found { + // Option not found, continue searching + } + } + + // Truncate the options list if necessary + opts.truncate(pos); +} + +impl Server { + pub fn serve( + udp_soc: UdpSocket, + server_ip: Ipv4Addr, + broadcast_ip: Ipv4Addr, + mut handler: H, + ) -> std::io::Error { + let mut in_buf: [u8; 1500] = [0; 1500]; + let mut s = Server { + out_buf: Cell::new([0; 1500]), + socket: udp_soc, + server_ip, + broadcast_ip, + src: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), + + }; + loop { + match s.socket.recv_from(&mut in_buf) { + Err(e) => return e, + Ok((l, src)) => { + if let Ok(p) = Packet::from(&in_buf[..l]) { + s.src = src; + + handler.handle_request(&s, p); + } + } + } + } + } + + /// Constructs and sends a reply packet back to the client. + /// additional_options should not include DHCP_MESSAGE_TYPE nor SERVER_IDENTIFIER as these + /// are added automatically. + pub fn reply( + &self, + msg_type: MessageType, + additional_options: Vec, + offer_ip: Ipv4Addr, + req_packet: Packet, + ) -> std::io::Result { + let ciaddr = match msg_type { + MessageType::Nak => Ipv4Addr::new(0, 0, 0, 0), + _ => req_packet.ciaddr, + }; + + //let mt = &[msg_type as u8]; + + let mut opts: Vec = Vec::with_capacity(additional_options.len() + 2); + opts.push(DhcpOption::DhcpMessageType(msg_type)); + opts.push(DhcpOption::ServerIdentifier(self.server_ip)); + /*opts.push(DhcpOption { + code: options::DHCP_MESSAGE_TYPE, + data: mt, + }); + opts.push(DhcpOption { + code: options::SERVER_IDENTIFIER, + data: &self.server_ip, + });*/ + opts.extend(additional_options); + + if let Some(DhcpOption::ParameterRequestList(prl)) = + req_packet.option(options::PARAMETER_REQUEST_LIST) + { + filter_options_by_req(&mut opts, &prl); + } + + self.send(Packet { + reply: true, + hops: 0, + xid: req_packet.xid, + secs: 0, + broadcast: req_packet.broadcast, + ciaddr, + yiaddr: offer_ip, + siaddr: Ipv4Addr::new(0, 0, 0, 0), + giaddr: req_packet.giaddr, + chaddr: req_packet.chaddr, + options: opts, + }) + } + + /// Checks the packet see if it was intended for this DHCP server (as opposed to some other also on the network). + pub fn for_this_server(&self, packet: &Packet) -> bool { + match packet.option(options::SERVER_IDENTIFIER) { + Some(DhcpOption::ServerIdentifier(x)) => { + x == &self.server_ip + }, + _ => false, + } + } + +/// Encodes and sends a DHCP packet back to the client. +pub fn send(&self, p: Packet) -> std::io::Result { + let mut addr = self.src; + if p.broadcast || addr.ip() == IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) { + addr.set_ip(std::net::IpAddr::V4(self.broadcast_ip)); + } + println!("Sending Response to: {:?}", addr); // Print the address + + self.socket.send_to(p.encode(&mut self.out_buf.get()), addr) +} +}