diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..66a1071 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/IPMeow.Server/bin/Debug/net7.0/IPMeow.Server.dll", + "args": [], + "cwd": "${workspaceFolder}/src/IPMeow.Server", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..f507ec6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/IPMeow.Server/IPMeow.Server.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/src/IPMeow.Server/IPMeow.Server.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/src/IPMeow.Server/IPMeow.Server.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9caa133..c005b6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,4 +71,3 @@ EXPOSE 80 EXPOSE 443 ENTRYPOINT [ "dotnet", "IPMeow.Viewer.dll" ] - diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5d73ed --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# IPMeow + +NYANET IP address management suite (DHCP, DNS & IPAM). + +## Features + +//TODO + +## Building + +With docker: + +```bash + docker build -t nyanet/ipmeow-server . + #TODO +``` + +## Deploying + +The most efficient method is with docker compose: + +```yaml +version: '3.3' +services: + # Main server + ipam: + ports: + - '80:80' + - '443:443' + - '67:67/udp' # if using integrated DHCP server + - '547:547/udp' # then include these two ports + volumes: + - './data:/etc/ipmeow' + - './certs:/etc/tls/certs:ro' + - './log:/var/log/nyangate' + restart: always + image: nyanet/ipmeow:ipam + + # If you want a standalone DHCP server container, include this service + dhcp: + ports: + - '67:67/udp' + - '547:547/udp' + volumes: + - './log:/var/log/nyangate' + restart: always + image: nyanet/ipmeow:dhcp + + # UI + viewer: + ports: + - '80:80' + - '443:443' + volumes: + - './certs:/etc/tls/certs:ro' + - './log:/var/log/nyangate' + restart: always + image: nyanet/ipmeow:viewer + + # Database + viewer: + volumes: + - './db:/var/data/postgres' + restart: always + image: postgres:latest +``` diff --git a/src/IPMeow.Dhcp/Server/Dhcp4/Bootp.cs b/src/IPMeow.Dhcp/Server/Dhcp4/Bootp.cs new file mode 100644 index 0000000..d500d85 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp4/Bootp.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using IPMeow.Lib; + +namespace IPMeow.Dhcp.Server.Dhcp4; + +public enum BootpOpType : byte +{ + BOOTREQUEST = 0x1, + BOOTREPLY = 0x2 +} + +public class BootpHeader +{ + public BootpOpType OpType { get; set; } + public byte HType { get; set; } + public byte HLen { get; set; } + 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 override string ToString() + { + return $"{Hops}\nXid: {Xid}, Seconds: {Seconds}, Flags: {Flags:x}\nci: {ClientAddress}\nyi: {YourAddress}, ns: {NextServerAddress}, ra: {RelayAgentAddress}\nhw: {MacAddress.Parse(GetMacAddress())}\nhost: {ServerHostName}\nboot: {BootFileName}"; + } + + 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[] GetMacAddress() => ClientHWAddress[0..6]; + + public static BootpHeader? Deserialize(byte[] data) + { + BitEnumerator enu = new(data); + + if (data.Length < 240 || data.Length > 576) + { + return null; + } + + var OpType = (BootpOpType)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()); + + //Check if hardware address is 48 bits + if (HLen != 6) + { + return null; + } + + //Validate presence of magic cookie + byte[] magic = enu.TakeBytes(4); + if (magic[0] != 0x63 || magic[1] != 0x82 || magic[2] != 0x53 || magic[3] != 0x63) + { + return null; + } + + return new() + { + Hops = Hops, + Xid = Xid, + Seconds = Seconds, + Flags = Flags, + ClientAddress = ClientAddress, + YourAddress = YourAddress, + NextServerAddress = NextServerAddress, + RelayAgentAddress = RelayAgentAddress, + ClientHWAddress = chaddr, + }; + } + + public byte[] Serialize() + { + using MemoryStream buffer = new(); + BinaryWriter writer = new(buffer); + + writer.Write((byte)); + writer.Write((byte)1); + writer.Write((byte)6); + 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/DhcpFields.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpFields.cs index b5ad696..182ce2d 100644 --- a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpFields.cs +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpFields.cs @@ -11,9 +11,3 @@ public enum DhcpMessageType DhcpRelease, DhcpInform, } - -public enum DhcpOpType : byte -{ - BOOTREQUEST = 0x1, - BOOTREPLY = 0x2 -} diff --git a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpOption.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpOption.cs new file mode 100644 index 0000000..937ff90 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpOption.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace IPMeow.Dhcp.Server.Dhcp4; + +public enum Dhcp4OptionCode : byte +{ + Pad = 0, + SubnetMask = 1, + TimeOffset = 2, + Router = 3, + TimeServer = 4, + NameServer = 5, + Dns = 6, + LogServer = 7, + CookieServer = 8, + LprServer = 9, + ImpressServer = 10, + ResourceLocationServer = 11, + Hostname = 12, + BootFileSize = 13, + MeritDumpFile = 14, + DomainName = 15, + SwapServer = 16, + RootPath = 17, + ExtensionsPath = 18, + IPForwardingEnabled = 19, + NonLocalSourceRoutingEnabled = 20, + PolicyFilter = 21, + MaximumDatagramReassembly = 22, + DefaultTtl = 23, + PmtuTimeout = 24, + PmtuPlateauTable = 25, + IfaceMtu = 26, + AllSubnetsLocal = 27, + BroadcastAddress = 28, + MaskDiscoveryEnabled = 29, + MaskSupplier = 30, + RouterDiscoveryEnabled = 31, + RouterSolicitationAddress = 32, + StaticRoute = 33, + TrailerEncapsulationEnabled = 34, + ArpCacheTimeout = 35, + EthernetEncapsulation = 36, + TcpTtl = 37, + TcpKeepaliveInterval = 38, + TcpKeepaliveGarbage = 39, + NisDomain = 40, + NisServers = 41, + NtpServers = 42, + VendorSpecific = 43, + NBNameServer = 44, + NBDistroServer = 45, + // + RequestedAddress = 50, + IPLeaseTime = 51, + OptionOverload = 52, + TftpServerName = 66, + BootfileName = 67, + DhcpMessageType = 53, + ServerIdentifier = 54, + ParameterRequest = 55, + TextMessage = 56, + MaximumTextMessageLen = 57, + RenewalTime = 58, + RebindingTime = 59, + VendorClassId = 60, + ClientId = 61, + + // + End = 255 +} + +public enum Dhcp4MessageType : byte +{ + DhcpDiscover = 1, + DhcpOffer = 2, + DhcpRequest = 3, + DhcpDecline = 4, + DhcpPack = 5, + DhcpNak = 6, + DhcpRelease = 7, + DhcpInform = 8, + DhcpForceRenew = 9, + DhcpLeaseQuery = 10, + DhcpLeaseUnassigned = 11, + DhcpLeaseUnknown = 12, + DhcpLeaseActive = 13, + DhcpBulkLeaseQuery = 14, + DhcpLeaseQueryDone = 15, + DhcpActiveLeaseQuery = 16, + DhcpLeaseQueryStatus = 17, + DhcpTls = 18 +} + +public interface IDhcpOption +{ + +} diff --git a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs index b4c7a5f..7819787 100644 --- a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpPacket.cs @@ -7,109 +7,61 @@ 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 required BootpHeader Header { get; init; } + + public ICollection Options { get; init; } = null!; public bool IsValidated { get; set; } - public override string ToString() - { - return $"{OpType}, htype: {HType}, hlen: {HLen}, hops: {Hops}\nXid: {Xid}, Seconds: {Seconds}, Flags: {Flags:x}\nci: {ClientAddress}\nyi: {YourAddress}, ns: {NextServerAddress}, ra: {RelayAgentAddress}\nhw: {MacAddress.Parse(GetMacAddress())}\nhost: {ServerHostName}\nboot: {BootFileName}"; - } - 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[] GetMacAddress() => ClientHWAddress[0..6]; - - 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) + public static DhcpPacket? Deserialize(byte[] data) { BitEnumerator enu = new(data); - if (data.Length < 236 || data.Length > 576) + if (data.Length < 240 || data.Length > 576) { - throw new ArgumentException("Invalid datagram."); + return null; } - var OpType = (DhcpOpType)enu.TakeByte(); + BootpHeader? header = BootpHeader.Deserialize(data[..240]); - 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); + if (header == null) + { + return null; + } - var BootFileName = Encoding.UTF8.GetString(file.TakeWhile(x => x != 0).ToArray()); - var ServerHostName = Encoding.UTF8.GetString(sname.TakeWhile(x => x != 0).ToArray()); + //Check if hardware address is 48 bits + if (header.HLen != 6) + { + return null; + } + + //Validate presence of magic cookie + byte[] magic = enu.TakeBytes(4); + if (magic[0] != 0x63 || magic[1] != 0x82 || magic[2] != 0x53 || magic[3] != 0x63) + { + return null; + } + + List options = new(); + + Dhcp4OptionCode opcode; + + while ((opcode = (Dhcp4OptionCode)enu.TakeByte()) != Dhcp4OptionCode.End) + { + if (opcode == Dhcp4OptionCode.Pad) + { + continue; + } + + byte len = enu.TakeByte(); + byte[] optionData = enu.TakeBytes(len); + } 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 + Header = header, + Options = options }; } @@ -118,20 +70,7 @@ public class DhcpPacket 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()); + writer.Write(Header.Serialize()); return buffer.GetBuffer(); } diff --git a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs index 6a381cc..325e0ac 100644 --- a/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs +++ b/src/IPMeow.Dhcp/Server/Dhcp4/DhcpServer.cs @@ -1,5 +1,7 @@ +using System.Diagnostics; using System.Net; using System.Net.Sockets; +using IPMeow.Lib.Address; using IPMeow.Lib.Request; namespace IPMeow.Dhcp.Server.Dhcp4; @@ -21,38 +23,48 @@ public class Dhcp4Server : IDhcpServer, IDisposable protected async Task Receive(CancellationToken token) { var datagram = await _udp.ReceiveAsync(token); - Console.WriteLine($"New v4 request: {datagram.Buffer.Length}"); - if (datagram.Buffer.Length < 236 || datagram.Buffer.Length > 576) + if (datagram.Buffer.Length < 240 || datagram.Buffer.Length > 576) { + Debug.Write($"Invalid packet received. (Expected: 240-576; received: {datagram.Buffer.Length})"); return; } var packet = DhcpPacket.Deserialize(datagram.Buffer); + if (packet is null) + { + Debug.Write("Failed packet parsing. Invalid structure."); + return; + } + if (!ValidateIncoming(packet)) { + Debug.Write("Packet validation failed."); return; } packet.IsValidated = true; - Console.WriteLine("Solicitation: "); - Console.WriteLine(packet.ToString()); - + DhcpRequestedEvent.Invoke(this, new() + { + IsV6 = false, + MacAddress = MacAddress.Parse(packet.GetMacAddress()), + RequestOrigin = packet.ClientAddress + }); //TODO } public bool ValidateIncoming(DhcpPacket packet) { Console.WriteLine(packet.ToString()); - if (packet.OpType != DhcpOpType.BOOTREQUEST) + if (packet.Header.OpType != BootpOpType.BOOTREQUEST) { return false; } //Discard unsupported - bool supported = packet.HLen == 6 && packet.HType == 1; + bool supported = packet.Header.HLen == 6 && packet.Header.HType == 1; if (!supported) { diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs index a8a826e..dd744c4 100644 --- a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpOption.cs @@ -5,30 +5,63 @@ namespace IPMeow.Dhcp.Server.Dhcp6; public enum DhcpOptionCode : short { + [ORO] ClientId = 1, + [ORO(false)] ServerId = 2, + [ORO(false)] IANonTempAssoc = 3, + [ORO(false)] IATempAssoc = 4, + [ORO(false)] IAAddr = 5, + [ORO(false)] OptionRequest = 6, + [ORO] Preference = 7, + [ORO] ElapsedTime = 8, + [ORO(false)] ServerUnicast = 12, + [ORO(false)] StatusCode = 13, + [ORO] VendorClass = 16, + [ORO] VendorOptions = 17, + [ORO] IfaceId = 18, + [ORO] DnsServers = 23, + [ORO] DomainList = 24, + [ORO] SntpServers = 31, + [ORO] SubscriberId = 38, + [ORO] ClientFqdn = 39, + [ORO] V6Lost = 51, + [ORO] NtpServer = 56, + [ORO] Addrsel = 84, + [ORO] AddrselTable = 85, } +[AttributeUsage(AttributeTargets.Field)] +public class OROAttribute : Attribute +{ + public OROAttribute(bool isOro = true) + { + IsORO = isOro; + } + + public bool IsORO { get; } +} + public interface IDhcpOption { public DhcpOptionCode Code { get; } diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs index 16dd7b1..83eefb6 100644 --- a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpServer.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Net; using System.Net.Sockets; using IPMeow.Lib.Request; @@ -32,14 +33,19 @@ public class Dhcp6Server : IDhcpServer, IDisposable { var datagram = await _listener.ReceiveAsync(token); - Console.WriteLine($"From v6: {datagram.RemoteEndPoint}, len: {datagram.Buffer.Length}"); + Debug.Print($"From v6: {datagram.RemoteEndPoint}, len: {datagram.Buffer.Length}"); var packet = DhcpPacket.Deserialize(datagram.Buffer); - Console.WriteLine($"New v6 packet: {packet.Xid}, len: {datagram.Buffer.Length}"); - - if (packet.MessageType is DhcpMessageType.Invalid or DhcpMessageType.Solicit or DhcpMessageType.Confirm or DhcpMessageType.Rebind) + if (packet.MessageType is DhcpMessageType.Invalid or DhcpMessageType.Advertise or DhcpMessageType.Reply or DhcpMessageType.Reconfigure or DhcpMessageType.RelayRepl) { + //Drop + return; + } + + if (packet.Options.Any(o => !o.Code.IsValidORO())) + { + //Reply with invalid return; } diff --git a/src/IPMeow.Dhcp/Server/Dhcp6/DhcpUtils.cs b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpUtils.cs new file mode 100644 index 0000000..0b04966 --- /dev/null +++ b/src/IPMeow.Dhcp/Server/Dhcp6/DhcpUtils.cs @@ -0,0 +1,18 @@ +using System.Reflection; + +namespace IPMeow.Dhcp.Server.Dhcp6; + +internal static class DhcpUtils +{ + internal static bool IsValidORO(this DhcpOptionCode code) + { + var fieldName = Enum.GetName(code); + if (fieldName is null) + { + return false; + } + + var oro = typeof(DhcpOptionCode).GetField(fieldName)!.GetCustomAttribute(); + return oro?.IsORO ?? false; + } +}