Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A Small DHCP Server Using UDP With Asynchronous Callbacks and Events

4.61/5 (19 votes)
11 Sep 2007CPOL4 min read 1   10.7K  
A UDP server.

Screenshot - frmimg.jpg

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:

C#
//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:

C#
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:

C#
//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:

C#
//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 above
  • len is the length of the message in bytes
  • Message is the message that is passed over, whose length is determined by len
  • 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:

C#
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:

C#
//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.

C#
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:

C#
//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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)