diff --git a/src/IPMeow.Dhcp/IPMeow.Dhcp.csproj b/src/IPMeow.Dhcp/IPMeow.Dhcp.csproj index 5957e8a..39a53d2 100644 --- a/src/IPMeow.Dhcp/IPMeow.Dhcp.csproj +++ b/src/IPMeow.Dhcp/IPMeow.Dhcp.csproj @@ -1,10 +1,11 @@ - - + + + Exe net7.0 enable enable diff --git a/src/IPMeow.Dhcp/Program.cs b/src/IPMeow.Dhcp/Program.cs new file mode 100644 index 0000000..804dae1 --- /dev/null +++ b/src/IPMeow.Dhcp/Program.cs @@ -0,0 +1,16 @@ +using IPMeow.Dhcp.Server; +using IPMeow.Dhcp.Server.Dhcp4; + +namespace IPMeow.Dhcp +{ + public static class Program + { + public static async Task Main(string[] args) + { + Console.WriteLine("Listening on DHCP..."); + var dhcp = new DhcpServer(); + + await dhcp.Run(); + } + } +} diff --git a/src/IPMeow.Dhcp/Server/AddressAssociation.cs b/src/IPMeow.Dhcp/Server/AddressAssociation.cs new file mode 100644 index 0000000..c02cedd --- /dev/null +++ b/src/IPMeow.Dhcp/Server/AddressAssociation.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server; + +public readonly struct AddressAssociation +{ + public IPAddress Address { get; } + public int PreferredLifetime { get; } + public int ValidLifetime { get; } + +} diff --git a/src/IPMeow.Dhcp/Server/DhcpFields.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpFields.cs similarity index 86% rename from src/IPMeow.Dhcp/Server/DhcpFields.cs rename to src/IPMeow.Dhcp/Server/Dhcp4/DhcpFields.cs index 579f3a5..b5ad696 100644 --- a/src/IPMeow.Dhcp/Server/DhcpFields.cs +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpFields.cs @@ -1,4 +1,4 @@ -namespace IPMeow.Dhcp.Server; +namespace IPMeow.Dhcp.Server.Dhcp4; public enum DhcpMessageType { diff --git a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs new file mode 100644 index 0000000..bc784e9 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Text; +using System.Threading.Tasks; +using IPMeow.Lib; + +namespace IPMeow.Dhcp.Server.Dhcp4; + +public class DhcpPacket +{ + public required DhcpOpType OpType { get; init; } + public required byte HType { get; init; } + public required byte HLen { get; init; } + public required byte Hops { get; init; } + public required uint Xid { get; init; } + public required short Seconds { get; init; } + public required short Flags { get; init; } + public required IPAddress ClientAddress { get; init; } + public required IPAddress YourAddress { get; init; } + public required IPAddress NextServerAddress { get; init; } + public required IPAddress RelayAgentAddress { get; init; } + public required byte[] ClientHWAddress { get; init; } + public required string ServerHostName { get; init; } + public required string BootFileName { get; init; } + public bool? IsValidated { get; set; } + + public byte[] GetClientHwAddress() + { + int padLen = 16 - ClientHWAddress.Length; + + if (padLen == 16) + { + return ClientHWAddress; + } + + var buf = new byte[16]; + ClientHWAddress.CopyTo(buf, 0); + return buf; + } + + public byte[] GetSnameBytes() + { + var buf = new byte[64]; + if (!string.IsNullOrWhiteSpace(ServerHostName)) + { + Encoding.UTF8.GetBytes(ServerHostName).CopyTo(buf, 0); + } + + return buf; + } + + public byte[] GetBootFileBytes() + { + var buf = new byte[128]; + if (!string.IsNullOrWhiteSpace(BootFileName)) + { + Encoding.UTF8.GetBytes(BootFileName).CopyTo(buf, 0); + } + + return buf; + } + + public static DhcpPacket Deserialize(byte[] data) + { + BitEnumerator enu = new(data); + + if (data.Length < 236 || data.Length > 576) + { + throw new ArgumentException("Invalid datagram."); + } + + var OpType = (DhcpOpType)enu.TakeByte(); + + var HType = enu.TakeByte(); + var HLen = enu.TakeByte(); + var Hops = enu.TakeByte(); + var Xid = enu.TakeUInt32(); + var Seconds = enu.TakeInt16(); + var Flags = enu.TakeInt16(); + var ClientAddress = new IPAddress(enu.TakeBytes(4)); + var YourAddress = new IPAddress(enu.TakeBytes(4)); + var NextServerAddress = new IPAddress(enu.TakeBytes(4)); + var RelayAgentAddress = new IPAddress(enu.TakeBytes(4)); + byte[] chaddr = enu.TakeBytes(16); + byte[] sname = enu.TakeBytes(64); + byte[] file = enu.TakeBytes(128); + + var BootFileName = Encoding.UTF8.GetString(file.TakeWhile(x => x != 0).ToArray()); + var ServerHostName = Encoding.UTF8.GetString(sname.TakeWhile(x => x != 0).ToArray()); + + return new() + { + OpType = OpType, + HType = HType, + HLen = HLen, + Hops = Hops, + Xid = Xid, + Seconds = Seconds, + Flags = Flags, + ClientAddress = ClientAddress, + YourAddress = YourAddress, + NextServerAddress = NextServerAddress, + RelayAgentAddress = RelayAgentAddress, + ClientHWAddress = chaddr, + ServerHostName = ServerHostName, + BootFileName = BootFileName + }; + } + + public byte[] Serialize() + { + using MemoryStream buffer = new(); + BinaryWriter writer = new(buffer); + + writer.Write((byte)OpType); + writer.Write(HType); + writer.Write(HLen); + writer.Write(Hops); + writer.Write(Xid); + writer.Write(Seconds); + writer.Write(Flags); + writer.Write(ClientAddress.GetAddressBytes()); + writer.Write(YourAddress.GetAddressBytes()); + writer.Write(NextServerAddress.GetAddressBytes()); + writer.Write(RelayAgentAddress.GetAddressBytes()); + writer.Write(GetClientHwAddress()); + writer.Write(GetSnameBytes()); + writer.Write(GetBootFileBytes()); + + return buffer.GetBuffer(); + } +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs new file mode 100644 index 0000000..7108bfa --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp4; + +public class DhcpServer +{ + public const int clientPort = 68, serverPort = 67; + + public DhcpServer() + { + _udp = new UdpClient(serverPort, AddressFamily.InterNetwork); + } + + public async Task Run() + { + while (true) + { + await Receive(); + } + } + + protected async Task Receive() + { + var datagram = await _udp.ReceiveAsync(); + + if (datagram.Buffer.Length < 236 || datagram.Buffer.Length > 576) + { + return; + } + + var packet = DhcpPacket.Deserialize(datagram.Buffer); + packet.IsValidated = ValidateIncoming(packet); + + if (!packet.IsValidated.Value) + { + return; + } + + + + Console.WriteLine("Solicitation: "); + Console.WriteLine(packet.ToString()); + } + + public bool ValidateIncoming(DhcpPacket packet) + { + if (packet.OpType != DhcpOpType.BOOTREQUEST) + { + return false; + } + + //Discard unsupported + bool supported = packet.HLen == 6 && packet.HType == 1; + + if (!supported) + { + return false; + } + + if (packet.NextServerAddress != IPAddress.Any) + { + return false; + } + + + return true; + } + + private readonly UdpClient _udp; +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpFields.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpFields.cs new file mode 100644 index 0000000..e54bce1 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpFields.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public enum DhcpMessageType : byte +{ + Invalid = 0, + Solicit = 1, + Advertise = 2, + Request = 3, + Confirm = 4, + Renew = 5, + Rebind = 6, + Reply = 7, + Release = 8, + Decline = 9, + Reconfigure = 10, + InfoRequest = 11, + RelayForw = 12, + RelayRepl = 13 +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs new file mode 100644 index 0000000..7186bd6 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public enum DhcpOptionCode : short +{ + ClientId = 1, + ServerId = 2, + IANonTempAssoc = 3, + IATempAssoc = 4, + IAAddr = 5, + OptionRequest = 6, + Preference = 7, + ElapsedTime = 8, + ServerUnicast = 12, + StatusCode = 13, + + +} + +public interface IDhcpOption +{ + public DhcpOptionCode Code { get; } + public byte[] Data { get; set; } + public Type DataType { get; } + public abstract object GetData(); +} + +public class ClientIdentifierOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.ClientId; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(Duid); + + public object GetData() + { + return new Duid(Data); + } +} + +public class ServerIdentifierOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.ServerId; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(Duid); + + public object GetData() + { + return new Duid(Data); + } +} + +public class NonTempIdentityAssociationOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.IANonTempAssoc; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(IdentityAssociation); + + public object GetData() + { + throw new NotImplementedException(); + } +} + +public class TempIdentityAssociationOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.IATempAssoc; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(IdentityAssociation); + + public object GetData() + { + throw new NotImplementedException(); + } +} + +public class IAAddressOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.IAAddr; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(AddressAssociation); + + public object GetData() + { + throw new NotImplementedException(); + } +} + +public class OptionRequestOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.OptionRequest; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(ICollection); + + public object GetData() + { + throw new NotImplementedException(); + } +} + +public class ElapsedTimeOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.ElapsedTime; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(TimeSpan); + + public object GetData() + { + return TimeSpan.FromSeconds(BitConverter.ToInt16(Data, 0)); + } +} + +public class ServerUnicastOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.ServerUnicast; + public required byte[] Data { get; set; } + + public Type DataType => typeof(IPAddress); + + public object GetData() + { + return new IPAddress(Data); + } +} + +public class StatusCodeOption : IDhcpOption +{ + public DhcpOptionCode Code => DhcpOptionCode.StatusCode; + + public required byte[] Data { get; set; } + + public Type DataType => typeof(DhcpStatus); + + public object GetData() + { + ushort code = BitConverter.ToUInt16(Data.AsSpan()[..2]); + + string? message = Data.Length > 2 ? Encoding.UTF8.GetString(Data.AsSpan()[2..]) : null; + + return new DhcpStatus(code, message); + } +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpPacket.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpPacket.cs new file mode 100644 index 0000000..c924610 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpPacket.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using IPMeow.Lib; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public class DhcpPacket +{ + public required DhcpMessageType MessageType { get; init; } + public required uint Xid { get; init; } + public ICollection Options { get; init; } = new List(); + + public static IDhcpOption GetOption(DhcpOptionCode type, byte[] data) + { + return type switch + { + DhcpOptionCode.ClientId => new ClientIdentifierOption() + { + Data = data + }, + DhcpOptionCode.ServerId => throw new NotImplementedException(), + DhcpOptionCode.IANonTempAssoc => throw new NotImplementedException(), + DhcpOptionCode.IATempAssoc => throw new NotImplementedException(), + DhcpOptionCode.IAAddr => throw new NotImplementedException(), + DhcpOptionCode.OptionRequest => throw new NotImplementedException(), + DhcpOptionCode.Preference => throw new NotImplementedException(), + DhcpOptionCode.ElapsedTime => throw new NotImplementedException(), + DhcpOptionCode.StatusCode => throw new NotImplementedException(), + _ => throw new InvalidOperationException($"Invalid option type {type}") + }; + } + + public static DhcpPacket Deserialize(byte[] data) + { + BitEnumerator enu = new(data); + + byte[] header = enu.TakeBytes(4); + + DhcpMessageType MessageType = (DhcpMessageType)header[0]; + + uint Xid = BitConverter.ToUInt32(header, 1); + + while (enu.Position < data.Length) + { + short optionCode = enu.TakeInt16(); + short optionLen = enu.TakeInt16(); + + if (enu.Position + optionLen > data.Length) + { + throw new DataMisalignedException("Invalid option length."); + } + + byte[] optionData = enu.TakeBytes(optionLen); + + + } + + return new() + { + MessageType = MessageType, + Xid = Xid + }; + } +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs new file mode 100644 index 0000000..6342343 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using IPMeow.Dhcp.Server.Dhcp4; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public class DhcpServer : IDhcpServer, IDisposable +{ + public const int clientPort = 546, serverPort = 547; + + public DhcpServer() + { + _listener = new(new IPEndPoint(IPAddress.Parse("ff02::1:2"), serverPort)); + _sender = new(clientPort, AddressFamily.InterNetworkV6); + } + private UdpClient _listener, _sender; + + public void Dispose() + { + GC.SuppressFinalize(this); + _listener.Dispose(); + _sender.Dispose(); + } + + public async Task Run() + { + while (true) + { + await Receive(); + } + } + + protected async Task Receive() + { + var datagram = await _listener.ReceiveAsync(); + + var packet = DhcpPacket.Deserialize(datagram.Buffer); + + if (packet.MessageType is DhcpMessageType.Invalid or DhcpMessageType.Solicit or DhcpMessageType.Confirm or DhcpMessageType.Rebind) + { + return; + } + + } +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpStatus.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpStatus.cs new file mode 100644 index 0000000..037bd90 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpStatus.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public struct DhcpStatus +{ + public DhcpStatus(ushort code, string? message) + { + Code = code; + Message = message; + } + + public static DhcpStatus? TryParse(byte[] data) + { + if (data.Length < 2) + { + return null; + } + + ushort code = BitConverter.ToUInt16(data.AsSpan()[..2]); + string? message = data.Length > 2 ? Encoding.UTF8.GetString(data.AsSpan()[2..]) : null; + + return new(code, message); + } + + public ushort Code { get; set; } + public string? Message { get; set; } + + public byte[] Serialize() + { + byte[] buf = new byte[2 + Message?.Length ?? 0]; + + buf[0] = (byte)((Code & 0xff00) >> 8); + buf[1] = (byte)(Code & 0x00ff); + + if (Message is not null) + { + Encoding.UTF8.GetBytes(Message).CopyTo(buf, 2); + } + + return buf; + } +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/Duid.cs b/src/IPMeow.Dhcp/Server/Dhcp6/Duid.cs new file mode 100644 index 0000000..a540cfd --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/Duid.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public enum DuidType : short +{ + LLAddrWithTime = 1, + VendorUID = 2, + LLAddr = 3, + Uuid = 4 +} + +public readonly struct Duid +{ + public Duid(byte[] data) + { + Type = (DuidType)BitConverter.ToInt16(data.AsSpan()[0..2]); + Data = data[2..]; + } + + public DuidType Type { get; } + public byte[] Data { get; } +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/IdentityAssociation.cs b/src/IPMeow.Dhcp/Server/Dhcp6/IdentityAssociation.cs new file mode 100644 index 0000000..1b9c58f --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/IdentityAssociation.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +public struct IdentityAssociation +{ + public int IAId { get; } + public int TimeToRefresh { get; } + public int TimeToNextAdvertise { get; } //T2 in RFC8415, 21.4 +} diff --git a/src/IPMeow.Dhcp/Server/DhcpPacket.cs b/src/IPMeow.Dhcp/Server/DhcpPacket.cs deleted file mode 100644 index 0032930..0000000 --- a/src/IPMeow.Dhcp/Server/DhcpPacket.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using IPMeow.Lib; - -namespace IPMeow.Dhcp.Server; - -public class DhcpPacket -{ - public DhcpPacket(byte[] data) - { - BitEnumerator enu = new(data); - - DhcpOpType opType = (DhcpOpType)enu.TakeByte(); - - byte hType = enu.TakeByte(); - byte hLen = enu.TakeByte(); - byte hOps = enu.TakeByte(); - - uint xid = enu.TakeUInt32(); - - short secs = enu.TakeInt16(); - - short flags = enu.TakeInt16(); - - IPAddress ciaddr = new(enu.TakeBytes(4)); - IPAddress yiaddr = new(enu.TakeBytes(4)); - IPAddress siaddr = new(enu.TakeBytes(4)); - IPAddress giaddr = new(enu.TakeBytes(4)); - - byte[] chaddr = enu.TakeBytes(16); - - byte[] sname = enu.TakeBytes(64); - byte[] file = enu.TakeBytes(128); - } -} diff --git a/src/IPMeow.Dhcp/Server/IDhcpServer.cs b/src/IPMeow.Dhcp/Server/IDhcpServer.cs new file mode 100644 index 0000000..84545ea --- /dev/null +++ b/src/IPMeow.Dhcp/Server/IDhcpServer.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server; + +public interface IDhcpServer +{ + public Task Run(); +}