/*
Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.Dhcp.Options;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using TechnitiumLibrary.IO;
namespace DnsServerCore.Dhcp
{
enum DhcpMessageOpCode : byte
{
BootRequest = 1,
BootReply = 2
}
enum DhcpMessageHardwareAddressType : byte
{
Ethernet = 1
}
enum DhcpMessageFlags : ushort
{
None = 0,
Broadcast = 0x8000
}
class DhcpMessage
{
#region variables
const uint MAGIC_COOKIE = 0x63538263; //in reverse format
readonly DhcpMessageOpCode _op;
readonly DhcpMessageHardwareAddressType _htype;
readonly byte _hlen;
readonly byte _hops;
readonly byte[] _xid;
readonly byte[] _secs;
readonly DhcpMessageFlags _flags;
readonly IPAddress _ciaddr;
readonly IPAddress _yiaddr;
readonly IPAddress _siaddr;
readonly IPAddress _giaddr;
readonly byte[] _chaddr;
readonly byte[] _sname;
readonly byte[] _file;
readonly IReadOnlyCollection _options;
readonly byte[] _clientHardwareAddress;
readonly string _serverHostName;
readonly string _bootFileName;
OptionOverloadOption _optionOverload;
DhcpMessageTypeOption _dhcpMessageType;
VendorClassIdentifierOption _vendorClassIdentifier;
ClientIdentifierOption _clientIdentifier;
ClientIdentifierOption _clientHardwareIdentifier;
HostNameOption _hostName;
ClientFullyQualifiedDomainNameOption _clientFullyQualifiedDomainName;
ParameterRequestListOption _parameterRequestList;
MaximumDhcpMessageSizeOption _maximumDhcpMessageSize;
ServerIdentifierOption _serverIdentifier;
RequestedIpAddressOption _requestedIpAddress;
#endregion
#region constructor
public DhcpMessage(DhcpMessageOpCode op, DhcpMessageHardwareAddressType hardwareAddressType, byte[] xid, byte[] secs, DhcpMessageFlags flags, IPAddress ciaddr, IPAddress yiaddr, IPAddress siaddr, IPAddress giaddr, byte[] clientHardwareAddress, string sname, string file, IReadOnlyCollection options)
{
if (ciaddr.AddressFamily != AddressFamily.InterNetwork)
throw new ArgumentException("Address family not supported.", nameof(ciaddr));
if (yiaddr.AddressFamily != AddressFamily.InterNetwork)
throw new ArgumentException("Address family not supported.", nameof(yiaddr));
if (siaddr.AddressFamily != AddressFamily.InterNetwork)
throw new ArgumentException("Address family not supported.", nameof(siaddr));
if (giaddr.AddressFamily != AddressFamily.InterNetwork)
throw new ArgumentException("Address family not supported.", nameof(giaddr));
ArgumentNullException.ThrowIfNull(clientHardwareAddress);
if (clientHardwareAddress.Length > 16)
throw new ArgumentException("Client hardware address cannot exceed 16 bytes.", nameof(clientHardwareAddress));
if (xid.Length != 4)
throw new ArgumentException("Transaction ID must be 4 bytes.", nameof(xid));
if (secs.Length != 2)
throw new ArgumentException("Seconds elapsed must be 2 bytes.", nameof(secs));
_op = op;
_htype = hardwareAddressType;
_hlen = Convert.ToByte(clientHardwareAddress.Length);
_hops = 0;
_xid = xid;
_secs = secs;
_flags = flags;
_ciaddr = ciaddr;
_yiaddr = yiaddr;
_siaddr = siaddr;
_giaddr = giaddr;
_clientHardwareAddress = clientHardwareAddress;
_chaddr = new byte[16];
Buffer.BlockCopy(_clientHardwareAddress, 0, _chaddr, 0, _clientHardwareAddress.Length);
_sname = new byte[64];
if (sname != null)
{
_serverHostName = sname;
byte[] buffer = Encoding.ASCII.GetBytes(sname);
if (buffer.Length >= 64)
throw new ArgumentException("Server host name cannot exceed 63 bytes.", nameof(sname));
Buffer.BlockCopy(buffer, 0, _sname, 0, buffer.Length);
}
_file = new byte[128];
if (file != null)
{
_bootFileName = file;
byte[] buffer = Encoding.ASCII.GetBytes(file);
if (buffer.Length >= 128)
throw new ArgumentException("Boot file name cannot exceed 127 bytes.", nameof(file));
Buffer.BlockCopy(buffer, 0, _file, 0, buffer.Length);
}
_options = options;
foreach (DhcpOption option in _options)
{
if (option.Code == DhcpOptionCode.ServerIdentifier)
{
_serverIdentifier = option as ServerIdentifierOption;
break;
}
}
}
public DhcpMessage(Stream s)
{
Span buffer = stackalloc byte[4];
s.ReadExactly(buffer);
_op = (DhcpMessageOpCode)buffer[0];
_htype = (DhcpMessageHardwareAddressType)buffer[1];
_hlen = buffer[2];
_hops = buffer[3];
_xid = s.ReadExactly(4);
s.ReadExactly(buffer);
_secs = new byte[2];
buffer.Slice(0, 2).CopyTo(_secs);
buffer.Reverse();
_flags = (DhcpMessageFlags)BitConverter.ToUInt16(buffer);
s.ReadExactly(buffer);
_ciaddr = new IPAddress(buffer);
s.ReadExactly(buffer);
_yiaddr = new IPAddress(buffer);
s.ReadExactly(buffer);
_siaddr = new IPAddress(buffer);
s.ReadExactly(buffer);
_giaddr = new IPAddress(buffer);
_chaddr = s.ReadExactly(16);
_clientHardwareAddress = new byte[_hlen];
Buffer.BlockCopy(_chaddr, 0, _clientHardwareAddress, 0, _hlen);
_sname = s.ReadExactly(64);
_file = s.ReadExactly(128);
//read options
List options = new List();
_options = options;
s.ReadExactly(buffer);
uint magicCookie = BitConverter.ToUInt32(buffer);
if (magicCookie == MAGIC_COOKIE)
{
ParseOptions(s, options);
if ((_optionOverload != null) && _optionOverload.Value.HasFlag(OptionOverloadValue.FileFieldUsed))
{
using (MemoryStream mS = new MemoryStream(_file))
{
ParseOptions(mS, options);
}
}
else
{
for (int i = 0; i < _file.Length; i++)
{
if (_file[i] == 0)
{
if (i == 0)
break;
_bootFileName = Encoding.ASCII.GetString(_file, 0, i);
break;
}
}
}
if ((_optionOverload != null) && _optionOverload.Value.HasFlag(OptionOverloadValue.SnameFieldUsed))
{
using (MemoryStream mS = new MemoryStream(_sname))
{
ParseOptions(mS, options);
}
}
else
{
for (int i = 0; i < _sname.Length; i++)
{
if (_sname[i] == 0)
{
if (i == 0)
break;
_serverHostName = Encoding.ASCII.GetString(_sname, 0, i);
break;
}
}
}
//parse all option values
foreach (DhcpOption option in options)
option.ParseOptionValue();
}
if (_maximumDhcpMessageSize != null)
_maximumDhcpMessageSize = new MaximumDhcpMessageSizeOption(576);
}
#endregion
#region static
public static DhcpMessage CreateReply(DhcpMessage request, IPAddress yiaddr, IPAddress siaddr, string sname, string file, IReadOnlyCollection options)
{
return new DhcpMessage(DhcpMessageOpCode.BootReply, request.HardwareAddressType, request.TransactionId, request.SecondsElapsed, request.Flags, request.ClientIpAddress, yiaddr, siaddr, request.RelayAgentIpAddress, request.ClientHardwareAddress, sname, file, options);
}
#endregion
#region private
private void ParseOptions(Stream s, List options)
{
while (true)
{
DhcpOption option = DhcpOption.Parse(s);
if (option.Code == DhcpOptionCode.End)
break;
if (option.Code == DhcpOptionCode.Pad)
continue;
bool optionExists = false;
foreach (DhcpOption existingOption in options)
{
if (existingOption.Code == option.Code)
{
//option already exists so append current option value into existing option
existingOption.AppendOptionValue(option);
optionExists = true;
break;
}
}
if (optionExists)
continue;
//add option to list
options.Add(option);
switch (option.Code)
{
case DhcpOptionCode.DhcpMessageType:
_dhcpMessageType = option as DhcpMessageTypeOption;
break;
case DhcpOptionCode.VendorClassIdentifier:
_vendorClassIdentifier = option as VendorClassIdentifierOption;
break;
case DhcpOptionCode.ClientIdentifier:
_clientIdentifier = option as ClientIdentifierOption;
break;
case DhcpOptionCode.HostName:
_hostName = option as HostNameOption;
break;
case DhcpOptionCode.ClientFullyQualifiedDomainName:
_clientFullyQualifiedDomainName = option as ClientFullyQualifiedDomainNameOption;
break;
case DhcpOptionCode.ParameterRequestList:
_parameterRequestList = option as ParameterRequestListOption;
break;
case DhcpOptionCode.MaximumDhcpMessageSize:
_maximumDhcpMessageSize = option as MaximumDhcpMessageSizeOption;
break;
case DhcpOptionCode.ServerIdentifier:
_serverIdentifier = option as ServerIdentifierOption;
break;
case DhcpOptionCode.RequestedIpAddress:
_requestedIpAddress = option as RequestedIpAddressOption;
break;
case DhcpOptionCode.OptionOverload:
_optionOverload = option as OptionOverloadOption;
break;
}
}
}
#endregion
#region public
public void WriteTo(Stream s)
{
s.WriteByte((byte)_op);
s.WriteByte((byte)_htype);
s.WriteByte(_hlen);
s.WriteByte(_hops);
s.Write(_xid);
s.Write(_secs);
byte[] buffer = BitConverter.GetBytes((ushort)_flags);
Array.Reverse(buffer);
s.Write(buffer);
s.Write(_ciaddr.GetAddressBytes());
s.Write(_yiaddr.GetAddressBytes());
s.Write(_siaddr.GetAddressBytes());
s.Write(_giaddr.GetAddressBytes());
s.Write(_chaddr);
s.Write(_sname);
s.Write(_file);
//write options
s.Write(BitConverter.GetBytes(MAGIC_COOKIE));
foreach (DhcpOption option in _options)
option.WriteTo(s);
}
public ClientIdentifierOption GetClientIdentifier(bool ignoreClientIdentifierOption)
{
if (ignoreClientIdentifierOption || (_clientIdentifier is null))
{
if (_clientHardwareIdentifier is null)
_clientHardwareIdentifier = new ClientIdentifierOption((byte)_htype, _clientHardwareAddress);
return _clientHardwareIdentifier;
}
return _clientIdentifier;
}
public string GetClientFullIdentifier()
{
string hardwareAddress = BitConverter.ToString(_clientHardwareAddress);
if (_clientFullyQualifiedDomainName != null)
return _clientFullyQualifiedDomainName.DomainName + " [" + hardwareAddress + "]";
if (_hostName != null)
return _hostName.HostName + " [" + hardwareAddress + "]";
return "[" + hardwareAddress + "]";
}
#endregion
#region properties
public DhcpMessageOpCode OpCode
{ get { return _op; } }
public DhcpMessageHardwareAddressType HardwareAddressType
{ get { return _htype; } }
public byte HardwareAddressLength
{ get { return _hlen; } }
public byte Hops
{ get { return _hops; } }
public byte[] TransactionId
{ get { return _xid; } }
public byte[] SecondsElapsed
{ get { return _secs; } }
public DhcpMessageFlags Flags
{ get { return _flags; } }
public IPAddress ClientIpAddress
{ get { return _ciaddr; } }
public IPAddress YourClientIpAddress
{ get { return _yiaddr; } }
public IPAddress NextServerIpAddress
{ get { return _siaddr; } }
public IPAddress RelayAgentIpAddress
{ get { return _giaddr; } }
public byte[] ClientHardwareAddress
{ get { return _clientHardwareAddress; } }
public string ServerHostName
{ get { return _serverHostName; } }
public string BootFileName
{ get { return _bootFileName; } }
public IReadOnlyCollection Options
{ get { return _options; } }
public DhcpMessageTypeOption DhcpMessageType
{ get { return _dhcpMessageType; } }
public VendorClassIdentifierOption VendorClassIdentifier
{ get { return _vendorClassIdentifier; } }
public HostNameOption HostName
{ get { return _hostName; } }
public ClientFullyQualifiedDomainNameOption ClientFullyQualifiedDomainName
{ get { return _clientFullyQualifiedDomainName; } }
public ParameterRequestListOption ParameterRequestList
{ get { return _parameterRequestList; } }
public MaximumDhcpMessageSizeOption MaximumDhcpMessageSize
{ get { return _maximumDhcpMessageSize; } }
public ServerIdentifierOption ServerIdentifier
{ get { return _serverIdentifier; } }
public RequestedIpAddressOption RequestedIpAddress
{ get { return _requestedIpAddress; } }
#endregion
}
}