From 5bb94e798a0d8d6e48438d36bcddafbd57043aa0 Mon Sep 17 00:00:00 2001 From: Brychan Dempsey Date: Tue, 5 Jan 2021 19:07:20 +1300 Subject: [PATCH] Add project files. --- Simple DHCP Server.sln | 25 + Simple DHCP Server/Program.cs | 605 +++++++++++++++++++ Simple DHCP Server/Simple DHCP Server.csproj | 9 + 3 files changed, 639 insertions(+) create mode 100644 Simple DHCP Server.sln create mode 100644 Simple DHCP Server/Program.cs create mode 100644 Simple DHCP Server/Simple DHCP Server.csproj diff --git a/Simple DHCP Server.sln b/Simple DHCP Server.sln new file mode 100644 index 0000000..0d8b31e --- /dev/null +++ b/Simple DHCP Server.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30611.23 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Simple DHCP Server", "Simple DHCP Server\Simple DHCP Server.csproj", "{B516FEAF-2AB5-494E-BAE7-2F1A85B9C228}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B516FEAF-2AB5-494E-BAE7-2F1A85B9C228}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B516FEAF-2AB5-494E-BAE7-2F1A85B9C228}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B516FEAF-2AB5-494E-BAE7-2F1A85B9C228}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B516FEAF-2AB5-494E-BAE7-2F1A85B9C228}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1C72CE9F-8D62-45EC-8661-3D4F23E0438A} + EndGlobalSection +EndGlobal diff --git a/Simple DHCP Server/Program.cs b/Simple DHCP Server/Program.cs new file mode 100644 index 0000000..03918ba --- /dev/null +++ b/Simple DHCP Server/Program.cs @@ -0,0 +1,605 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace Simple_DHCP_Server +{ + class Program + { + static byte[] MAGIC_COOKIE = { 0x63, 0x82, 0x53, 0x63 }; + const short STANDARD_MTU = 1480; + static uint DHCPLeaseTime = 900; // 15 minutes + + static uint[] DNSServers = new uint[] { 16_843_009, }; // 1.1.1.1 + + // NEW + // Local network IP address + static byte localAddress1 = 192; + static byte localAddress2 = 168; + static byte localAddress3 = 250; + static byte deviceIP = 1; + + static byte leaseStart = 2; + + // Local Subnet Address + static byte localSubnet1 = 255; + static byte localSubnet2 = 255; + static byte localSubnet3 = 255; + static byte localSubnet4 = 0; + + // This option is used on Init() to prereserve lease space (in RAM) for this many devices + // Take note that each lease uses 25 bytes, minimum, so 128 leases = 3.2 kB RAM + // As this server is intended to run only IPv4, we can crop the MAC address to 6 bytes + 10*zero, + // giving 15/bytes entry (total of 1.92 kB initially) + // Storing hashes of the MAC address should result in faster lookups, though at the cost of 2n space. + // (so 1.92 kB is actually 2.176 kB) + static byte maxLeases = 32; + + static DHCPEntry[] DHCPEntries = new DHCPEntry[maxLeases]; + static int[] DHCPHashes = new int[maxLeases]; + + static void Main(string[] args) + { + Console.WriteLine("Creating DHCP Server..."); + for (int i = 0; i < maxLeases; i++) + { + DHCPEntries[i] = new DHCPEntry(); + } + UdpClient udpClient = new UdpClient(); + //udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, 67)); + udpClient.Client.Bind(new IPEndPoint(IPAddress.Parse("192.168.250.1"), 67)); + // Keep track of the remote IP, if it exists + IPEndPoint remote = new IPEndPoint(0, 0); + while (true) + { + // Possibility to multithread this, though for the microprocessor, we will remain single threaded + byte[] buffer = udpClient.Receive(ref remote); + if (remote.Address.GetAddressBytes()[2] != 2) + { + try + { + Tuple result = ProcessDHCP(buffer, udpClient); + // Return data to the remote endpoint, if it has a valid IP, else we just broadcast the packet + if (remote.Address.Equals(IPAddress.Parse("0.0.0.0"))) + { + udpClient.Send(result.Item2, result.Item2.Length, result.Item1, 68); + } + else + { + udpClient.Send(result.Item2, result.Item2.Length, remote); + } + + } + catch (NullReferenceException) + { + Console.WriteLine("Data invalid"); + } + } + // Clean the DHCP table as necessary + ProcessDHCPTable(); + } + } + + public static Tuple ProcessDHCP(byte[] rxBuffer, UdpClient client) + { + // Reserve the standard MTU as DHCP packets should not be broken - might get resized as options sizes are calculated + byte[] txBuffer = new byte[STANDARD_MTU]; + string destAddress = "255.255.255.255"; // Broadcast + if (rxBuffer[236] == MAGIC_COOKIE[0] && rxBuffer[237] == MAGIC_COOKIE[1] && rxBuffer[238] == MAGIC_COOKIE[2] && rxBuffer[239] == MAGIC_COOKIE[3]) + { + // Jump to after the magic packet + uint position = 240; + // To avoid excessive memory usage (and as DHCP messages require >~700 bytes of data), we create the output + // buffer and write directly to it as data is processed. + // This helps keep memory free, and allows quicker processing of the data. + // However, we must first identify the type of DHCP operation expected to be performed + + + // Begin by identifying the options requested, + // Assume DHCP Option (53) is the first element + List RXOptions = new List(); + // Iterate through the remaining buffer while the current position is not too far & the value at this position is not 255 + while (rxBuffer[position] != 0xFF && position < STANDARD_MTU) + { + byte option = rxBuffer[position++]; + // Read the data length from the option + byte dataLength = rxBuffer[position++]; + DHCPOption newOption = new DHCPOption() + { + option = option, + dataLength = dataLength + }; + while (dataLength > 0) + { + newOption.DHCPData.Add(rxBuffer[position++]); + dataLength--; + } + RXOptions.Add(newOption); + } + + // Next, check that we start with a DHCP option + if (RXOptions[0].option == 53) + { + // Grab the MAC address object, so we can compare with the existing items + MACAddress clientMAC = new MACAddress(rxBuffer[28..34]); // take the range of values from 28 (inclusive) to 34 (exclusive) i.e. 6 bytes of MAC address + // Find if the item already exists, and/or determine its IP now + byte leaseIndex = FindPosByMac(clientMAC, DHCPEntries, maxLeases); + byte clientIP = 0; + if (leaseIndex < 255) + { + // If found, use the client's assigned IP address + clientIP =(byte)(leaseIndex + leaseStart); + } + else + { + // Not found; search for the next empty MAC and assign + leaseIndex = FindPosByMac(MACAddress.Empty, DHCPEntries, maxLeases); + if (leaseIndex == 255) + { + return null; + } + clientIP = (byte)(leaseIndex + leaseStart); + } + // The same basic packet structure occurs to all responses: + // In context, we're always responding + txBuffer[0] = 2; + // The following duplicates the HTYPE, LEN, HOPS, XID, SECS, FLAGS fields, as they are not expected to change + for (byte i = 1; i < 12; i++) + { + txBuffer[i] = rxBuffer[i]; + } + // The mac address [28..34] is 16 bytes long, 6 bytes followed by 10 zeros + // We can just copy this from the client's message + for (byte i = 28; i < 44; i++) + { + txBuffer[i] = rxBuffer[i]; + } + // Copy magic packet + for (int i = 236; i < 240; i++) + { + txBuffer[i] = rxBuffer[i]; + } + // Now, all responses include our address (20-24) and your address (16-20) + txBuffer[16] = localAddress1; + txBuffer[17] = localAddress2; + txBuffer[18] = localAddress3; + txBuffer[19] = clientIP; + txBuffer[20] = localAddress1; + txBuffer[21] = localAddress2; + txBuffer[22] = localAddress3; + txBuffer[23] = deviceIP; + + + position = 240; + // Now, figure the DHCP option requested + if (RXOptions[0].DHCPData[0] == 1) + { + Console.Write("D"); + byte[] prl = new byte[0]; + // Sort through DHCP options + foreach (DHCPOption option in RXOptions) + { + // Ignore 53 as we've processed and its reply is done later + if (option.option == 50) + { + // Device has requested a specific IP; check if it is in use + byte requestSuffix = option.DHCPData[option.dataLength - 1]; + if (DHCPEntries[requestSuffix-leaseStart].MAC.Equals(MACAddress.Empty)) + { + // No device under this lease; change our ip and assign this instead + clientIP = requestSuffix; + txBuffer[19] = clientIP; + } + } + else if(option.option == 55) + { + // Parameter request list + prl = new byte[option.dataLength]; + for (int i = 0; i < option.dataLength; i++) + { + prl[i] = option.DHCPData[i]; + } + } + } + // We now have all the required data to formulate an Offer + // We make this offer for 30 seconds; if another Discover request is made, update the old time + // Begin by adding the Offer to the table + //DHCPEntries[clientIP - leaseStart].IpSuffix = clientIP; + DHCPEntries[clientIP - leaseStart].MAC = clientMAC; + DHCPEntries[clientIP - leaseStart].requestedItems = prl; + DHCPEntries[clientIP - leaseStart].expiry = (millis() / 1000) + 30; // Set the expiry 30 seconds from now + // Next formulate the options in the offer + // DHCP Option + txBuffer[position++] = 53; + txBuffer[position++] = 1; + txBuffer[position++] = 2; + // Must have lease time & DHCP Server + List r = Generate_DHCP_Option(54, clientIP); + foreach (byte b in r) + { + txBuffer[position++] = b; + } + r = Generate_DHCP_Option(51, clientIP); + foreach (byte b in r) + { + txBuffer[position++] = b; + } + // Requested Options + foreach (byte item in prl) + { + r = Generate_DHCP_Option(item, clientIP); + if (r != null) + { + for (int i = 0; i < r.Count; i++) + { + txBuffer[position++] = r[i]; + } + } + // We don't know how to handle this option, so ignore it + } + // Log our offer + Console.Write("O"); + } + else if (RXOptions[0].DHCPData[0] == 3) + { + Console.Write("R"); + bool optionsMatch = true; + foreach (DHCPOption option in RXOptions) + { + // Ignore 53 as we've processed and its reply is done later + // If 50 does not match our provided IP, reject the request (NACK) + if (option.option == 50) + { + // Should the entry not be initialised, just accept the client, despite the incorrect joining (maybe the server lost power?) + if (clientIP != option.DHCPData[option.dataLength - 1] && !DHCPEntries[clientIP-2].MAC.Equals(MACAddress.Empty)) + { + optionsMatch = false; + break; + } + } + else if (option.option == 54) + { + // DHCP Server. Check that this matches our IP + if (deviceIP != option.DHCPData[option.dataLength - 1]) + { + optionsMatch = false; + break; + } + } + else if (option.option == 55) + { + // Parameter request list + DHCPEntries[clientIP-2].requestedItems = new byte[option.dataLength]; + for (int i = 0; i < option.dataLength; i++) + { + DHCPEntries[clientIP - 2].requestedItems[i] = option.DHCPData[i]; + } + } + } + if (optionsMatch) + { + // ACK + // Expiry is our current system time in seconds, (millis / 1000) + the lease time + DHCPEntries[clientIP - leaseStart].expiry = (millis() / 1000) + DHCPLeaseTime; + // Next formulate the options in the offer + // DHCP Option + txBuffer[position++] = 53; + txBuffer[position++] = 1; + txBuffer[position++] = 5; // Acknowledge + // Must have lease time & DHCP Server + List r = Generate_DHCP_Option(54, clientIP); + foreach (byte b in r) + { + txBuffer[position++] = b; + } + // Time must use the default value here; 255 isn't a valid client and the option generator uses + r = Generate_DHCP_Option(51, clientIP); + foreach (byte b in r) + { + txBuffer[position++] = b; + } + // Requested Options + foreach (byte item in DHCPEntries[clientIP - leaseStart].requestedItems) + { + r = Generate_DHCP_Option(item, clientIP); + if (r != null) + { + for (int i = 0; i < r.Count; i++) + { + txBuffer[position++] = r[i]; + } + } + } + // Log our offer + Console.WriteLine("A"); + } + else + { + // NACK + txBuffer[position++] = 53; + txBuffer[position++] = 1; + txBuffer[position++] = 6; // Negative Acknowledge + // Must have lease time & DHCP Server + List r = Generate_DHCP_Option(54, clientIP); + foreach (byte b in r) + { + txBuffer[position++] = b; + } + // Time must use the default value here; 255 isn't a valid client and the option generator uses + r = Generate_DHCP_Option(51, clientIP); + foreach (byte b in r) + { + txBuffer[position++] = b; + } + // Requested Options + foreach (byte item in DHCPEntries[clientIP - leaseStart].requestedItems) + { + r = Generate_DHCP_Option(item, clientIP); + if (r != null) + { + for (int i = 0; i < r.Count; i++) + { + txBuffer[position++] = r[i]; + } + } + } + // Log our offer + Console.WriteLine("N"); + } + } + else + { + Console.WriteLine(" " + RXOptions[0].DHCPData[0] + " "); + } + } + // Set the final byte high + txBuffer[position++] = 255; + byte[] returnBuffer = new byte[position]; + Array.Copy(txBuffer, returnBuffer, position); + return new Tuple(destAddress, returnBuffer); + } + else + { + Console.WriteLine("Magic Cookie not found"); + return null; + } + } + /// + /// returns a similar expected value as to millis() in Arduino (milliseconds since system start; rolls over) + /// This in + /// + /// + public static uint millis() + { + TimeSpan elapsed = DateTime.Now.Subtract(Process.GetCurrentProcess().StartTime); + return (uint)(GetTickCount64() % uint.MaxValue); + } + + [DllImport("kernel32")] + extern static UInt64 GetTickCount64(); + + static uint GetIP(byte IpSuffix) + { + return (uint)((localAddress1 << 24) | (localAddress2 << 16) | (localAddress3 << 8) | IpSuffix); + } + + static uint GetIP() + { + return GetIP(deviceIP); + } + /// + /// returns 255 if not found. + /// + /// + /// + /// + /// + static byte FindPosByMac(MACAddress MAC, DHCPEntry[] array, byte arrayLimit) + { + for (byte i = 0; i < arrayLimit; i++) + { + if (array[i].MAC.Bytes == null) return 255; + if (array[i].MAC.Equals(MAC)) + { + return i; + } + } + return 255; + } + + /// + /// Searches the DHCP table for expired entries and removes them as appropriate + /// (Also restructures the table) + /// + static void ProcessDHCPTable() + { + uint currentTime = millis() / 1000; + for (int i = 0; i < maxLeases; i++) + { + if (DHCPEntries[i].expiry <= currentTime && !DHCPEntries[i].MAC.Equals(MACAddress.Empty)) + { + DHCPEntries[i].MAC = MACAddress.Empty; + // clear requested items + DHCPEntries[i].requestedItems = new byte[5]; + } + } + } + + /// + /// Generates a DHCP option, by the provided code + /// + /// + /// + static List Generate_DHCP_Option(byte option, byte clientIP) + { + List s = new List(); + s.Add(option); + s.Add(0); + byte length = 0; + // Subnet code + if (option == 1) + { + s.Add(localSubnet1); + s.Add(localSubnet2); + s.Add(localSubnet3); + s.Add(localSubnet4); + length = 4; + } + // Router + else if (option == 3) + { + s.Add(localAddress1); + s.Add(localAddress2); + s.Add(localAddress3); + s.Add(deviceIP); + length = 4; + } + // DNS Servers + else if (option == 6) + { + if (DNSServers.Length > 0) + { + for (int i = 0; i < DNSServers.Length; i++) + { + if (DNSServers[i] != 0) + { + s.Add((byte)(DNSServers[i] >> 24)); + s.Add((byte)(DNSServers[i] >> 16)); + s.Add((byte)(DNSServers[i] >> 8)); + s.Add((byte)DNSServers[i]); + length += 4; + + } + } + } + else return null; + } + // Lease Time + else if (option == 51) + { + long time = 0; + if (clientIP == 255) + { + time = DHCPLeaseTime; + } + else + { + // As the time stored in DHCPEntries is in seconds, subtracting the current millis() in seconds + time = DHCPEntries[clientIP - 2].expiry - (millis()/1000); + } + s.Add((byte)(time >> 24)); + s.Add((byte)(time >> 16)); + s.Add((byte)(time >> 8)); + s.Add((byte)time); + length = 4; + } + else if (option == 54) + { + s.Add(localAddress1); + s.Add(localAddress2); + s.Add(localAddress3); + s.Add(deviceIP); + length = 4; + } + else return null; + + s[1] = length; + return s; + } + } + class DHCPOption + { + public byte option=0; + public List DHCPData = new List(); + public byte dataLength=0; + } + /// + /// Initial size is 111 bytes / entry (theoretically) + /// Can grow as necessary + /// + class DHCPEntry + { + //public byte IpSuffix = 0; // implied by position + // Don't init the MAC, that must be done once the table is ready + public MACAddress MAC = MACAddress.Empty; + public uint expiry = 0; // 4 + public byte[] requestedItems = new byte[0]; + } + + /// + /// NAT table entries keep track of the client endpoint for maintaining consistent communication via one gateway. + /// (6 bytes/entry) + /// + class NATEntry + { + public byte sourceDevice = 0; // The device sends data through the socket : + public ushort sourcePort = 0; + // This device rewrites the source port with a new one (below), and replaces the ip. + public ushort destPort = 0; + // Keep track of the connection state, setting this to closed if necessary + public bool closed = false; + } + + /// + /// Represents a MAC address as a single object + /// This should be approximately ported to c++, including the Equals() method, + /// so that easy comparison of the MAC addresses can be performed + /// + struct MACAddress + { + byte _MACSize; + byte[] MACbytes; + public readonly static MACAddress Empty = new MACAddress(0, 0, 0, 0, 0, 0); + + public MACAddress(byte b1, byte b2, byte b3, byte b4, byte b5, byte b6) + { + MACbytes = new byte[] { b1, b2, b3, b4, b5, b6 }; + _MACSize = 6; + } + + public MACAddress(byte[] bytes) + { + if (bytes.Length > byte.MaxValue) throw new IndexOutOfRangeException("Size of source byte array cannot exceed byte.MaxValue"); + MACbytes = new byte[bytes.Length]; + for (int i = 0; i < bytes.Length; i++) + { + MACbytes[i] = bytes[i]; + } + _MACSize = (byte)bytes.Length; + } + + public byte[] Bytes => MACbytes; + public byte MACSize { get { return _MACSize; } } + + public override bool Equals(object obj) + { + try + { + if (MACSize != ((MACAddress)obj).MACSize) + { + return false; + } + for (int i = 0; i < MACSize; i++) + { + if (MACbytes[i] != ((MACAddress)obj).MACbytes[i]) + { + return false; + } + } + } + catch (InvalidCastException) + { + return false; + } + return true; + } + + public override int GetHashCode() + { + return base.GetHashCode(); + } + } +} diff --git a/Simple DHCP Server/Simple DHCP Server.csproj b/Simple DHCP Server/Simple DHCP Server.csproj new file mode 100644 index 0000000..75d3f33 --- /dev/null +++ b/Simple DHCP Server/Simple DHCP Server.csproj @@ -0,0 +1,9 @@ + + + + Exe + netcoreapp3.1 + Simple_DHCP_Server + + +