Abstract away BOOTP

This commit is contained in:
femsci 2023-06-14 11:23:05 +02:00
parent 51909d18f4
commit 0823cc25d3
Signed by: femsci
GPG key ID: 08F7911F0E650C67
12 changed files with 494 additions and 120 deletions

35
.vscode/launch.json vendored Normal file
View file

@ -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"
}
]
}

41
.vscode/tasks.json vendored Normal file
View file

@ -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"
}
]
}

View file

@ -71,4 +71,3 @@ EXPOSE 80
EXPOSE 443
ENTRYPOINT [ "dotnet", "IPMeow.Viewer.dll" ]

66
README.md Normal file
View file

@ -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
```

View file

@ -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();
}
}

View file

@ -11,9 +11,3 @@ public enum DhcpMessageType
DhcpRelease,
DhcpInform,
}
public enum DhcpOpType : byte
{
BOOTREQUEST = 0x1,
BOOTREPLY = 0x2
}

View file

@ -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
{
}

View file

@ -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<IDhcpOption> 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<IDhcpOption> 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();
}

View file

@ -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)
{

View file

@ -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; }

View file

@ -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;
}

View file

@ -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<OROAttribute>();
return oro?.IsORO ?? false;
}
}