A Small DHCP Server Using UDP With Asynchronous Callbacks and Events






4.61/5 (18 votes)
A UDP server.
Introduction
About a while ago, I was working on a project that needed to identify devices on a network that were being powered up, and allocate a separate known IP address to each.
To solve this problem, I decided to use the DHCP service, as each separate MAC address shall get a separate IP.
There are compiled programs on the net, or DLLs or objects that you can purchase to include with your project; however, none do present the code.
Sometimes, we need the code of the DHCP service to include in the project as well as to put in added functionality that is not offered by normal DHCP servers. For example, on a local network, there is a main DHCP server, which allocates IPs to computers, and there is also the small DHCP server that allocates a known IP address to a particular device only. Since the DHCP protocol communicates using UPD, the data is broadcasted (not point to point) and can be picked up very easily. In order to do that, we shall use a simple filtering algorithm that shall filter out requests and shall allow only permitted MAC addresses to be assigned IPs.
Definition of DHCP
DHCP means Dynamic Host Configuration Protocol, and is made up of two components which include a DHCP client (the network device requesting IP settings), and a DHCP Server (an Internet Host that shall return configuration parameters to the client requesting it).
A brief description of how the small DHCP server works
A DHCP server is generally installed on a local network, and is used to centrally allocate TCP-IP configurations to network devices or computers without automatically setting it manually. DHCP means Dynamic Host Configuration Protocol. It saves you plenty of time to set up and manage TCP/IP networks, especially if you have a big network.
The DHCP server listens on UDP port 67, and sends data to the clients using UDP on port 67 as well. The UDP service uses an asynchronous method by using callbacks:
//this function shall start the listening service for the UDP client
//s is a class or structure that shall contain a refernce to the UDP client
//class of .NET, as well as an endpoint refernce.
private void IniListnerCallBack()
{
try
{
// start teh recieve call back method
s.u.BeginReceive(new AsyncCallback(OnDataRecieved), s);
}
catch (Exception ex)
{
if (IsListening == true)
Console.WriteLine(ex.Message);
}
}
// This is the call back function that shall be triggered
// when data has been recieved.
//var asyn shall contain an instance of the UPD structure (UDPstate)
// returned by the callback
public void OnDataRecieved(IAsyncResult asyn)
{
Byte[] receiveBytes;
UdpClient u;
IPEndPoint e;
try
{
//get the udp client
u = (UdpClient)((UdpState)(asyn.AsyncState)).u;
//get the endpoint (shall contain refernce about the client)
e = (IPEndPoint)((UdpState)(asyn.AsyncState)).e;
//stop the callback and get the number of bytes recieved
receiveBytes = u.EndReceive(asyn, ref e);
//raise the event with the data recieved to the DHCP class
DataRcvd(receiveBytes, e);
}
catch (Exception ex)
{
if (IsListening == true)
Console.WriteLine(ex.Message);
}
finally
{
u = null;
e = null;
receiveBytes = null;
// recall the call back, ie go and listen for more data
IniListnerCallBack();
}
}
The UDP client shall be set up and will listen on the network for incoming requests.
Each message shall be identified as being unique with the MAC address and a Transaction ID (D_xid
), which is a random number chosen by the client. On data exchange, the data is sent as a byte stream, and the format will follow the RFC defined structure as follows:
public struct DHCPstruct
{
public byte D_op; //Op code: 1 = bootRequest, 2 = BootReply
public byte D_htype; //Hardware Address Type: 1 = 10MB ethernet
public byte D_hlen; //hardware address length: length of MACID
public byte D_hops; //Hw options
public byte[] D_xid; //transaction id (5),
public byte[] D_secs; //elapsed time from trying to boot (3)
public byte[] D_flags; //flags (3)
public byte[] D_ciaddr; // client IP (5)
public byte[] D_yiaddr; // your client IP (5)
public byte[] D_siaddr; // Server IP (5)
public byte[] D_giaddr; // relay agent IP (5)
public byte[] D_chaddr; // Client HW address (16)
public byte[] D_sname; // Optional server host name (64)
public byte[] D_file; // Boot file name (128)
public byte[] M_Cookie; // Magic cookie (4)
public byte[] D_options; //options (rest)
}
Therefore, data is sent to the DHCP class via an event as a byte stream. And using the BinaryReader
class as defined by .NET, we shall put the bytes in their relevant place. OPTION_OFFSET
is a predefined constant which will state where the Option Structure starts:
//pass over a byte as convert it
//using the predefined stream reader function
//Data is an array containing the udp data sent.
public cDHCPStruct(byte[] Data)
{
System.IO.BinaryReader rdr;
System.IO.MemoryStream stm =
new System.IO.MemoryStream(Data, 0, Data.Length);
try
{ //read data
dStruct.D_op = rdr.ReadByte();
dStruct.D_htype = rdr.ReadByte();
dStruct.D_hlen = rdr.ReadByte();
dStruct.D_hops = rdr.ReadByte();
dStruct.D_xid = rdr.ReadBytes(4);
dStruct.D_secs = rdr.ReadBytes(2);
dStruct.D_flags = rdr.ReadBytes(2);
dStruct.D_ciaddr = rdr.ReadBytes(4);
dStruct.D_yiaddr = rdr.ReadBytes(4);
dStruct.D_siaddr = rdr.ReadBytes(4);
dStruct.D_giaddr = rdr.ReadBytes(4);
dStruct.D_chaddr = rdr.ReadBytes(16);
dStruct.D_sname = rdr.ReadBytes(64);
dStruct.D_file = rdr.ReadBytes(128);
dStruct.M_Cookie = rdr.ReadBytes(4);
//read the rest of the data, which shall determine the dhcp
//options
dStruct.D_options = rdr.ReadBytes(Data.Length - OPTION_OFFSET);
}
catch(Exception ex)
{
Console.WriteLine (ex.Message);
}
}
A client requesting an IP address shall also pass over a list of options in the option list that need to filled out by the DHCP server, to pass back. The options that can be passed over as defined by the RFC, can include:
//end option is the mark that shall signify the end of the message
public enum DHCPOptionEnum
{
SubnetMask = 1,
TimeOffset = 2,
Router = 3,
TimeServer = 4,
NameServer = 5,
DomainNameServer = 6,
LogServer = 7,
CookieServer = 8,
LPRServer = 9,
ImpressServer = 10,
ResourceLocServer = 11,
HostName = 12,
BootFileSize = 13,
MeritDump = 14,
DomainName = 15,
SwapServer = 16,
RootPath = 17,
ExtensionsPath = 18,
IpForwarding = 19,
NonLocalSourceRouting = 20,
PolicyFilter = 21,
MaximumDatagramReAssemblySize = 22,
DefaultIPTimeToLive = 23,
PathMTUAgingTimeout = 24,
PathMTUPlateauTable = 25,
InterfaceMTU = 26,
AllSubnetsAreLocal = 27,
BroadcastAddress = 28,
PerformMaskDiscovery = 29,
MaskSupplier = 30,
PerformRouterDiscovery = 31,
RouterSolicitationAddress = 32,
StaticRoute = 33,
TrailerEncapsulation = 34,
ARPCacheTimeout = 35,
EthernetEncapsulation = 36,
TCPDefaultTTL = 37,
TCPKeepaliveInterval = 38,
TCPKeepaliveGarbage = 39,
NetworkInformationServiceDomain = 40,
NetworkInformationServers = 41,
NetworkTimeProtocolServers = 42,
VendorSpecificInformation = 43,
NetBIOSoverTCPIPNameServer = 44,
NetBIOSoverTCPIPDatagramDistributionServer = 45,
NetBIOSoverTCPIPNodeType = 46,
NetBIOSoverTCPIPScope = 47,
XWindowSystemFontServer = 48,
XWindowSystemDisplayManager = 49,
RequestedIPAddress = 50,
IPAddressLeaseTime = 51,
OptionOverload = 52,
DHCPMessageTYPE = 53,
ServerIdentifier = 54,
ParameterRequestList = 55,
Message = 56,
MaximumDHCPMessageSize = 57,
RenewalTimeValue_T1 = 58,
RebindingTimeValue_T2 = 59,
Vendorclassidentifier = 60,
ClientIdentifier = 61,
NetworkInformationServicePlusDomain = 64,
NetworkInformationServicePlusServers = 65,
TFTPServerName = 66,
BootfileName = 67,
MobileIPHomeAgent = 68,
SMTPServer = 69,
POP3Server = 70,
NNTPServer = 71,
DefaultWWWServer = 72,
DefaultFingerServer = 73,
DefaultIRCServer = 74,
StreetTalkServer = 75,
STDAServer = 76,
END_Option = 255
}
The code shall then have to breakdown the option byte array, which shall follow the format of:
-------------------------------------------------
|a|len|Message|a|len|Message|........|END_OPTION|
-------------------------------------------------
where:
a
signifies the option message code as defined abovelen
is the length of the message in bytesMessage
is the message that is passed over, whose length is determined bylen
END_OPTION
shall signify the end of the option message
The message type
The message type is an identifier (53) in the OPTIONS
message. It shall determine the state of negotiation, and shall include:
public enum DHCPMsgType //Message types as defined by the RFC
{
DHCPDISCOVER = 1, //a client broadcasts to locate servers
DHCPOFFER = 2, //a server offers an IP address to the device
DHCPREQUEST = 3, //client accepts offers from DHCP server
DHCPDECLINE = 4, //client declines the offer from this DHCP server
DHCPACK = 5, //server to client + committed IP address
DHCPNAK = 6, //server to client to state net address incorrect
DHCPRELEASE = 7, //graceful shutdown from client to Server
DHCPINFORM = 8 //client to server asking for local info
}
In the code, these shall be raised as events from the DHCP class to the main form:
//an event has to call a delegate (function pointer)
#region "event Delegates"
public delegate void AnnouncedEventHandler(cDHCPStruct d_DHCP,string MacId);
public delegate void ReleasedEventHandler();//(cDHCPStruct d_DHCP);
public delegate void RequestEventHandler(cDHCPStruct d_DHCP, string MacId);
public delegate void AssignedEventHandler(string IPAdd,string MacID );
#endregion
public event AnnouncedEventHandler Announced;
public event RequestEventHandler Request;
The main form, before assigning an IP address, will use the Ping
class of .NET and see if the IP is already in use.
public static bool CheckAlive(string IpAdd)
{
Ping pingSender = new Ping();
IPAddress address;
PingReply reply;
try
{
address = IPAddress.Parse(IpAdd);//IPAddress.Loopback;
reply = pingSender.Send(address,100);
if (reply.Status == IPStatus.Success)
{
Console.WriteLine("Address: {0}",
reply.Address.ToString());
Console.WriteLine("RoundTrip time: {0}",
reply.RoundtripTime);
Console.WriteLine("Time to live: {0}",
reply.Options.Ttl);
Console.WriteLine("Don't fragment: {0}",
reply.Options.DontFragment);
Console.WriteLine("Buffer size: {0}",
reply.Buffer.Length);
return true;
}
else
{
Console.WriteLine(reply.Status);
return false;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
finally
{
if (pingSender != null) pingSender.Dispose();
pingSender = null;
address = null;
reply = null;
}
}
The main program shall, then, take in the user parameters, which include the leas time, subnet, the server host name etc.. and send the data over by converting from the structure to a byte
stream, using the Array
class as defined in .NET:
//function to build the data structure to a byte array
private byte[] BuildDataStructure(cDHCPStruct.DHCPstruct ddHcpS)
{
byte[] mArray;
try
{
mArray = new byte[0];
AddOptionElement(new byte[] { ddHcpS.D_op }, ref mArray);
AddOptionElement(new byte[] { ddHcpS.D_htype }, ref mArray);
AddOptionElement(new byte[] { ddHcpS.D_hlen }, ref mArray);
AddOptionElement(new byte[] { ddHcpS.D_hops }, ref mArray);
AddOptionElement(ddHcpS.D_xid, ref mArray);
AddOptionElement(ddHcpS.D_secs, ref mArray);
AddOptionElement(ddHcpS.D_flags, ref mArray);
AddOptionElement(ddHcpS.D_ciaddr, ref mArray);
AddOptionElement(ddHcpS.D_yiaddr, ref mArray);
AddOptionElement(ddHcpS.D_siaddr, ref mArray);
AddOptionElement(ddHcpS.D_giaddr, ref mArray);
AddOptionElement(ddHcpS.D_chaddr, ref mArray);
AddOptionElement(ddHcpS.D_sname, ref mArray);
AddOptionElement(ddHcpS.D_file, ref mArray);
AddOptionElement(ddHcpS.M_Cookie, ref mArray);
AddOptionElement(ddHcpS.D_options, ref mArray);
return mArray;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
return false;
}
finally
{
marray = null;
}
}
//function to grow an array, we shall pass
//the array back by using references
private void AddOptionElement(byte[] FromValue, ref byte[] TargetArray)
{
try
{
//resize the array accoringly
if (TargetArray != null)
Array.Resize(ref TargetArray,
TargetArray.Length + FromValue.Length );
else
Array.Resize(ref TargetArray, FromValue.Length );
//copy the data over
Array.Copy(FromValue, 0, TargetArray,
TargetArray.Length - FromValue.Length,
FromValue.Length);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
The code provided
The code provided is split into three main classes, an asynchronous UDP service, a DHCP structure converter, and the main form. Each communicate to the other by using events; however, callbacks can be used as well. It is important to note that when calling controls from an event, the events shall be multi-cast, and Invoke
will need to be used.
Conclusion
This is a very basic implementation of a DHCP server class, and various improvements can be made by adding more options according to individual needs.