Use Generic Host model. Refactor server implementations.
This commit is contained in:
parent
dbffe9e4cc
commit
51909d18f4
28 changed files with 501 additions and 134 deletions
|
@ -4,8 +4,8 @@
|
||||||
<ProjectReference Include="..\IPMeow.Lib\IPMeow.Lib.csproj" />
|
<ProjectReference Include="..\IPMeow.Lib\IPMeow.Lib.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
using IPMeow.Dhcp.Server.Dhcp4;
|
using IPMeow.Dhcp.Server.Dhcp4;
|
||||||
using IPMeow.Lib.Host;
|
using IPMeow.Dhcp.Server.Dhcp6;
|
||||||
|
using IPMeow.Dhcp.Standalone;
|
||||||
|
using IPMeow.Lib.Request;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp
|
namespace IPMeow.Dhcp
|
||||||
|
@ -8,7 +11,13 @@ namespace IPMeow.Dhcp
|
||||||
{
|
{
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
var app = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder(args).Build();
|
var app = Host.CreateDefaultBuilder(args).ConfigureServices(s =>
|
||||||
|
{
|
||||||
|
s.AddScoped<HttpClient>();
|
||||||
|
s.AddSingleton<IAddressProvider<AddressRequestContext>, DecoupledIPAddressProvider>();
|
||||||
|
s.AddHostedService<Dhcp4Server>();
|
||||||
|
s.AddHostedService<Dhcp6Server>();
|
||||||
|
}).Build();
|
||||||
|
|
||||||
Console.WriteLine("Listening on DHCP...");
|
Console.WriteLine("Listening on DHCP...");
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using IPMeow.Lib;
|
using IPMeow.Lib;
|
||||||
|
using IPMeow.Lib.Address;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server.Dhcp4;
|
namespace IPMeow.Dhcp.Server.Dhcp4;
|
||||||
|
|
||||||
|
@ -20,7 +21,12 @@ public class DhcpPacket
|
||||||
public required byte[] ClientHWAddress { get; init; }
|
public required byte[] ClientHWAddress { get; init; }
|
||||||
public required string ServerHostName { get; init; }
|
public required string ServerHostName { get; init; }
|
||||||
public required string BootFileName { get; init; }
|
public required string BootFileName { get; init; }
|
||||||
public bool? IsValidated { get; set; }
|
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()
|
public byte[] GetClientHwAddress()
|
||||||
{
|
{
|
||||||
|
@ -36,6 +42,8 @@ public class DhcpPacket
|
||||||
return buf;
|
return buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] GetMacAddress() => ClientHWAddress[0..6];
|
||||||
|
|
||||||
public byte[] GetSnameBytes()
|
public byte[] GetSnameBytes()
|
||||||
{
|
{
|
||||||
var buf = new byte[64];
|
var buf = new byte[64];
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using IPMeow.Lib.Address;
|
using IPMeow.Lib.Request;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server.Dhcp4;
|
namespace IPMeow.Dhcp.Server.Dhcp4;
|
||||||
|
|
||||||
|
@ -8,27 +8,20 @@ public class Dhcp4Server : IDhcpServer, IDisposable
|
||||||
{
|
{
|
||||||
public const int clientPort = 68, serverPort = 67;
|
public const int clientPort = 68, serverPort = 67;
|
||||||
|
|
||||||
public Dhcp4Server(IAddressProvider addressProvider)
|
public Dhcp4Server(IAddressProvider<AddressRequestContext> addressProvider)
|
||||||
{
|
{
|
||||||
_addrProvider = addressProvider;
|
_addrProvider = addressProvider;
|
||||||
_udp = new UdpClient(serverPort, AddressFamily.InterNetwork);
|
_udp = new UdpClient(serverPort, AddressFamily.InterNetwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IAddressProvider _addrProvider;
|
private readonly IAddressProvider<AddressRequestContext> _addrProvider;
|
||||||
|
|
||||||
public event EventHandler<DhcpRequestEvent> DhcpRequestedEvent;
|
public event EventHandler<DhcpRequestEventArgs> DhcpRequestedEvent = delegate { };
|
||||||
|
|
||||||
public async Task Run()
|
protected async Task Receive(CancellationToken token)
|
||||||
{
|
{
|
||||||
while (true)
|
var datagram = await _udp.ReceiveAsync(token);
|
||||||
{
|
Console.WriteLine($"New v4 request: {datagram.Buffer.Length}");
|
||||||
await Receive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task Receive()
|
|
||||||
{
|
|
||||||
var datagram = await _udp.ReceiveAsync();
|
|
||||||
|
|
||||||
if (datagram.Buffer.Length < 236 || datagram.Buffer.Length > 576)
|
if (datagram.Buffer.Length < 236 || datagram.Buffer.Length > 576)
|
||||||
{
|
{
|
||||||
|
@ -36,19 +29,23 @@ public class Dhcp4Server : IDhcpServer, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
var packet = DhcpPacket.Deserialize(datagram.Buffer);
|
var packet = DhcpPacket.Deserialize(datagram.Buffer);
|
||||||
packet.IsValidated = ValidateIncoming(packet);
|
|
||||||
|
|
||||||
if (!packet.IsValidated.Value)
|
if (!ValidateIncoming(packet))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
packet.IsValidated = true;
|
||||||
|
|
||||||
Console.WriteLine("Solicitation: ");
|
Console.WriteLine("Solicitation: ");
|
||||||
Console.WriteLine(packet.ToString());
|
Console.WriteLine(packet.ToString());
|
||||||
|
|
||||||
|
//TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ValidateIncoming(DhcpPacket packet)
|
public bool ValidateIncoming(DhcpPacket packet)
|
||||||
{
|
{
|
||||||
|
Console.WriteLine(packet.ToString());
|
||||||
if (packet.OpType != DhcpOpType.BOOTREQUEST)
|
if (packet.OpType != DhcpOpType.BOOTREQUEST)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
@ -72,7 +69,31 @@ public class Dhcp4Server : IDhcpServer, IDisposable
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
GC.SuppressFinalize(this);
|
||||||
|
_udp.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Run(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Receive(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
new Thread(async () => await Run(cancellationToken)).Start();
|
||||||
|
Console.WriteLine("V4 started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly UdpClient _udp;
|
private readonly UdpClient _udp;
|
||||||
|
|
|
@ -15,8 +15,18 @@ public enum DhcpOptionCode : short
|
||||||
ElapsedTime = 8,
|
ElapsedTime = 8,
|
||||||
ServerUnicast = 12,
|
ServerUnicast = 12,
|
||||||
StatusCode = 13,
|
StatusCode = 13,
|
||||||
|
VendorClass = 16,
|
||||||
|
VendorOptions = 17,
|
||||||
|
IfaceId = 18,
|
||||||
|
DnsServers = 23,
|
||||||
|
DomainList = 24,
|
||||||
|
SntpServers = 31,
|
||||||
|
SubscriberId = 38,
|
||||||
|
ClientFqdn = 39,
|
||||||
|
V6Lost = 51,
|
||||||
|
NtpServer = 56,
|
||||||
|
Addrsel = 84,
|
||||||
|
AddrselTable = 85,
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IDhcpOption
|
public interface IDhcpOption
|
||||||
|
@ -37,7 +47,7 @@ public class ClientIdentifierOption : IDhcpOption
|
||||||
|
|
||||||
public object GetData()
|
public object GetData()
|
||||||
{
|
{
|
||||||
return new Duid(Data);
|
return Duid.Parse(Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +61,7 @@ public class ServerIdentifierOption : IDhcpOption
|
||||||
|
|
||||||
public object GetData()
|
public object GetData()
|
||||||
{
|
{
|
||||||
return new Duid(Data);
|
return Duid.Parse(Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,3 +165,101 @@ public class StatusCodeOption : IDhcpOption
|
||||||
return new DhcpStatus(code, message);
|
return new DhcpStatus(code, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class VendorClassOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InterfaceIdOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubscriberIdOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class V6LostOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ClientFqdnOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DnsServersOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NtpServerOption : IDhcpOption
|
||||||
|
{
|
||||||
|
public DhcpOptionCode Code => DhcpOptionCode.VendorClass;
|
||||||
|
|
||||||
|
public required byte[] Data { get; set; }
|
||||||
|
|
||||||
|
public Type DataType => typeof(byte[]);
|
||||||
|
|
||||||
|
public object GetData()
|
||||||
|
{
|
||||||
|
return Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,14 +16,69 @@ public class DhcpPacket
|
||||||
{
|
{
|
||||||
Data = data
|
Data = data
|
||||||
},
|
},
|
||||||
DhcpOptionCode.ServerId => throw new NotImplementedException(),
|
DhcpOptionCode.ServerId => new ServerIdentifierOption()
|
||||||
DhcpOptionCode.IANonTempAssoc => throw new NotImplementedException(),
|
{
|
||||||
DhcpOptionCode.IATempAssoc => throw new NotImplementedException(),
|
Data = data
|
||||||
DhcpOptionCode.IAAddr => throw new NotImplementedException(),
|
},
|
||||||
DhcpOptionCode.OptionRequest => throw new NotImplementedException(),
|
DhcpOptionCode.IANonTempAssoc => new NonTempIdentityAssociationOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.IATempAssoc => new TempIdentityAssociationOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.IAAddr => new IAAddressOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.OptionRequest => new OptionRequestOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
DhcpOptionCode.Preference => throw new NotImplementedException(),
|
DhcpOptionCode.Preference => throw new NotImplementedException(),
|
||||||
DhcpOptionCode.ElapsedTime => throw new NotImplementedException(),
|
DhcpOptionCode.ElapsedTime => new ElapsedTimeOption()
|
||||||
DhcpOptionCode.StatusCode => throw new NotImplementedException(),
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.StatusCode => new StatusCodeOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.ServerUnicast => new ServerUnicastOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.VendorClass => new VendorClassOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.IfaceId => new InterfaceIdOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.DnsServers => new DnsServersOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.SubscriberId => new SubscriberIdOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.ClientFqdn => new ClientFqdnOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.V6Lost => new V6LostOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.NtpServer => new NtpServerOption()
|
||||||
|
{
|
||||||
|
Data = data
|
||||||
|
},
|
||||||
|
DhcpOptionCode.Addrsel => throw new NotImplementedException(),
|
||||||
|
DhcpOptionCode.AddrselTable => throw new NotImplementedException(),
|
||||||
_ => throw new InvalidOperationException($"Invalid option type {type}")
|
_ => throw new InvalidOperationException($"Invalid option type {type}")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,13 +91,22 @@ public class DhcpPacket
|
||||||
|
|
||||||
DhcpMessageType MessageType = (DhcpMessageType)header[0];
|
DhcpMessageType MessageType = (DhcpMessageType)header[0];
|
||||||
|
|
||||||
uint Xid = BitConverter.ToUInt32(header, 1);
|
header[0] = 0;
|
||||||
|
uint Xid = BitConverter.ToUInt32(header);
|
||||||
|
|
||||||
|
Console.WriteLine("Preview:\n{0}", string.Join(' ', data.Select(b => b.ToString("x2"))));
|
||||||
|
|
||||||
|
Console.WriteLine($"v6: {Xid}, {enu.Position}, {MessageType}, {data.Length}");
|
||||||
|
|
||||||
|
List<IDhcpOption> options = new();
|
||||||
|
|
||||||
while (enu.Position < data.Length)
|
while (enu.Position < data.Length)
|
||||||
{
|
{
|
||||||
short optionCode = enu.TakeInt16();
|
DhcpOptionCode optionCode = (DhcpOptionCode)enu.TakeInt16();
|
||||||
short optionLen = enu.TakeInt16();
|
short optionLen = enu.TakeInt16();
|
||||||
|
|
||||||
|
Console.WriteLine($"D: {data.Length}, {enu.Position}, {optionCode}, {optionLen}");
|
||||||
|
|
||||||
if (enu.Position + optionLen > data.Length)
|
if (enu.Position + optionLen > data.Length)
|
||||||
{
|
{
|
||||||
throw new DataMisalignedException("Invalid option length.");
|
throw new DataMisalignedException("Invalid option length.");
|
||||||
|
@ -50,13 +114,17 @@ public class DhcpPacket
|
||||||
|
|
||||||
byte[] optionData = enu.TakeBytes(optionLen);
|
byte[] optionData = enu.TakeBytes(optionLen);
|
||||||
|
|
||||||
|
Console.WriteLine(string.Join(' ', optionData.Select(b => b.ToString("x2"))));
|
||||||
|
|
||||||
|
IDhcpOption option = GetOption(optionCode, optionData);
|
||||||
|
options.Add(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new()
|
return new()
|
||||||
{
|
{
|
||||||
MessageType = MessageType,
|
MessageType = MessageType,
|
||||||
Xid = Xid
|
Xid = Xid,
|
||||||
|
Options = options
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using IPMeow.Lib.Address;
|
using IPMeow.Lib.Request;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server.Dhcp6;
|
namespace IPMeow.Dhcp.Server.Dhcp6;
|
||||||
|
|
||||||
|
@ -8,17 +8,18 @@ public class Dhcp6Server : IDhcpServer, IDisposable
|
||||||
{
|
{
|
||||||
public const int clientPort = 546, serverPort = 547;
|
public const int clientPort = 546, serverPort = 547;
|
||||||
|
|
||||||
public Dhcp6Server(IAddressProvider addressProvider)
|
public Dhcp6Server(IAddressProvider<AddressRequestContext> addressProvider)
|
||||||
{
|
{
|
||||||
_addrProvider = addressProvider;
|
_addrProvider = addressProvider;
|
||||||
_listener = new(new IPEndPoint(IPAddress.Parse("ff02::1:2"), serverPort));
|
_listener = new(serverPort, AddressFamily.InterNetworkV6);
|
||||||
|
_listener.JoinMulticastGroup(IPAddress.Parse("ff02::1:2"));
|
||||||
_sender = new(clientPort, AddressFamily.InterNetworkV6);
|
_sender = new(clientPort, AddressFamily.InterNetworkV6);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly IAddressProvider _addrProvider;
|
private readonly IAddressProvider<AddressRequestContext> _addrProvider;
|
||||||
private readonly UdpClient _listener, _sender;
|
private readonly UdpClient _listener, _sender;
|
||||||
|
|
||||||
public event EventHandler<DhcpRequestEvent> DhcpRequestedEvent;
|
public event EventHandler<DhcpRequestEventArgs> DhcpRequestedEvent = delegate { };
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
@ -27,24 +28,44 @@ public class Dhcp6Server : IDhcpServer, IDisposable
|
||||||
_sender.Dispose();
|
_sender.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Run()
|
protected async Task Receive(CancellationToken token)
|
||||||
{
|
{
|
||||||
while (true)
|
var datagram = await _listener.ReceiveAsync(token);
|
||||||
{
|
|
||||||
await Receive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task Receive()
|
Console.WriteLine($"From v6: {datagram.RemoteEndPoint}, len: {datagram.Buffer.Length}");
|
||||||
{
|
|
||||||
var datagram = await _listener.ReceiveAsync();
|
|
||||||
|
|
||||||
var packet = DhcpPacket.Deserialize(datagram.Buffer);
|
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.Solicit or DhcpMessageType.Confirm or DhcpMessageType.Rebind)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Run(CancellationToken token)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await Receive(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
new Thread(async () => await Run(cancellationToken)).Start();
|
||||||
|
Console.WriteLine("V6 started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,33 @@ using System.Text;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server.Dhcp6;
|
namespace IPMeow.Dhcp.Server.Dhcp6;
|
||||||
|
|
||||||
|
public enum DhcpStatusCode : short
|
||||||
|
{
|
||||||
|
Success = 0,
|
||||||
|
UnspecifiedFail = 1,
|
||||||
|
NoAddrAvailable = 2,
|
||||||
|
NoBinding = 3,
|
||||||
|
NotOnLink = 4,
|
||||||
|
UseMulticast = 5,
|
||||||
|
NoPrefixAvailable = 6,
|
||||||
|
UnknownQueryType = 7,
|
||||||
|
MalformedQuery = 8,
|
||||||
|
NotConfigured = 9,
|
||||||
|
NotAllowed = 10,
|
||||||
|
QueryTerminated = 11,
|
||||||
|
DataMissing = 12,
|
||||||
|
CatchUpComplete = 13,
|
||||||
|
NotSupported = 14,
|
||||||
|
TlsConnRefused = 15,
|
||||||
|
AddrInUse = 16,
|
||||||
|
ConfigConflict = 17,
|
||||||
|
MissingBindingInfo = 18,
|
||||||
|
OutdatedBindingInfo = 19,
|
||||||
|
ServerShuttingDown = 20,
|
||||||
|
DnsUpdateNotSupported = 21,
|
||||||
|
ExcessiveTimeSkew = 22
|
||||||
|
}
|
||||||
|
|
||||||
public struct DhcpStatus
|
public struct DhcpStatus
|
||||||
{
|
{
|
||||||
public DhcpStatus(ushort code, string? message)
|
public DhcpStatus(ushort code, string? message)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
using IPMeow.Lib.Request;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server.Dhcp6;
|
namespace IPMeow.Dhcp.Server.Dhcp6;
|
||||||
|
|
||||||
public enum DuidType : short
|
public enum DuidType : short
|
||||||
|
@ -8,12 +10,36 @@ public enum DuidType : short
|
||||||
Uuid = 4
|
Uuid = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct Duid
|
public readonly struct Duid : IRequestClientIdentifier
|
||||||
{
|
{
|
||||||
public Duid(byte[] data)
|
private Duid(DuidType type, byte[] data)
|
||||||
{
|
{
|
||||||
Type = (DuidType)BitConverter.ToInt16(data.AsSpan()[0..2]);
|
Type = type;
|
||||||
Data = data[2..];
|
Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Duid Invalid = new(0, Array.Empty<byte>());
|
||||||
|
|
||||||
|
public static bool TryParse(byte[] data, out Duid duid)
|
||||||
|
{
|
||||||
|
short dType = BitConverter.ToInt16(data.AsSpan()[0..2]);
|
||||||
|
|
||||||
|
if (!Enum.IsDefined(typeof(DuidType), dType))
|
||||||
|
{
|
||||||
|
duid = Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] duidData = data[2..];
|
||||||
|
|
||||||
|
duid = new((DuidType)dType, duidData);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Duid Parse(byte[] data)
|
||||||
|
{
|
||||||
|
return TryParse(data, out Duid duid) ? duid : throw new InvalidDataException("Invalid data supplied.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public DuidType Type { get; }
|
public DuidType Type { get; }
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using IPMeow.Lib.Address;
|
using IPMeow.Lib.Address;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server;
|
namespace IPMeow.Dhcp.Server;
|
||||||
|
|
||||||
public class DhcpRequestEvent
|
public class DhcpRequestEventArgs : EventArgs
|
||||||
{
|
{
|
||||||
public bool IsV6 { get; init; }
|
public bool IsV6 { get; init; }
|
||||||
public IPAddress? RequestOrigin { get; init; }
|
public IPAddress? RequestOrigin { get; init; }
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Server;
|
namespace IPMeow.Dhcp.Server;
|
||||||
|
|
||||||
public interface IDhcpServer
|
public interface IDhcpServer : IHostedService
|
||||||
{
|
{
|
||||||
Task Run();
|
event EventHandler<DhcpRequestEventArgs> DhcpRequestedEvent;
|
||||||
|
|
||||||
event EventHandler<DhcpRequestEvent> DhcpRequestedEvent;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,20 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using IPMeow.Lib.Request;
|
||||||
using IPMeow.Lib.Address;
|
|
||||||
|
|
||||||
namespace IPMeow.Dhcp.Standalone;
|
namespace IPMeow.Dhcp.Standalone;
|
||||||
public class DecoupledIPv6AddressProvider : IPv6AddressProvider
|
|
||||||
|
public class DecoupledIPAddressProvider : IAddressProvider<AddressRequestContext>
|
||||||
{
|
{
|
||||||
public DecoupledIPv6AddressProvider(HttpClient http)
|
public DecoupledIPAddressProvider(HttpClient http)
|
||||||
{
|
{
|
||||||
_http = http;
|
_http = http;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
public IPv6Address GetAddress()
|
public IPAddress GetAddress(AddressRequestContext ctx)
|
||||||
{
|
|
||||||
_http.GetAsync("/api/request/6/free");
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPv6Address GetAddress(MacAddress mac)
|
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
_http.GetAsync($"/api/request/{(ctx.IsV6 ? "6" : "4")}/free");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class DecoupledIPv4AddressProvider : IPv4AddressProvider
|
|
||||||
{
|
|
||||||
public DecoupledIPv4AddressProvider(HttpClient http)
|
|
||||||
{
|
|
||||||
_http = http;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly HttpClient _http;
|
|
||||||
|
|
||||||
public IPv4Address GetAddress()
|
|
||||||
{
|
|
||||||
_http.GetAsync("/api/request/4/free");
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public IPv4Address GetAddress(MacAddress mac)
|
|
||||||
{
|
|
||||||
_http.GetAsync($"/api/request/4/mac/{mac}");
|
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace IPMeow.Lib.Address;
|
|
||||||
|
|
||||||
public interface IAddressProvider
|
|
||||||
{
|
|
||||||
public IPAddress GetAddress();
|
|
||||||
public IPAddress GetAddress(MacAddress mac);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPv4AddressProvider
|
|
||||||
{
|
|
||||||
public IPv4Address GetAddress();
|
|
||||||
public IPv4Address GetAddress(MacAddress mac);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IPv6AddressProvider
|
|
||||||
{
|
|
||||||
public IPv6Address GetAddress();
|
|
||||||
public IPv6Address GetAddress(MacAddress mac);
|
|
||||||
}
|
|
|
@ -22,7 +22,7 @@ public readonly struct MacAddress
|
||||||
return mac.Value;
|
return mac.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ArgumentException("Invalid address byte length. Expected 6 octets.");
|
throw new ArgumentException($"Invalid address byte length. Expected 6 octets. Got {addr.Length} octets.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MacAddress Parse(string addr)
|
public static MacAddress Parse(string addr)
|
||||||
|
@ -32,7 +32,7 @@ public readonly struct MacAddress
|
||||||
return mac.Value;
|
return mac.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new FormatException("Invalid address format.");
|
throw new FormatException($"Invalid address format: {addr}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParse(byte[] addr, [NotNullWhen(true)] out MacAddress? mac)
|
public static bool TryParse(byte[] addr, [NotNullWhen(true)] out MacAddress? mac)
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
namespace IPMeow.Lib;
|
namespace IPMeow.Lib;
|
||||||
|
|
||||||
public class BitEnumerator
|
public class BitEnumerator
|
||||||
|
@ -26,7 +28,7 @@ public class BitEnumerator
|
||||||
|
|
||||||
public short TakeInt16()
|
public short TakeInt16()
|
||||||
{
|
{
|
||||||
short num = BitConverter.ToInt16(_data.AsSpan()[_position..(_position + 2)]);
|
short num = BinaryPrimitives.ReadInt16BigEndian(_data.AsSpan()[_position..(_position + 2)]);
|
||||||
|
|
||||||
_position += 2;
|
_position += 2;
|
||||||
|
|
||||||
|
@ -35,7 +37,7 @@ public class BitEnumerator
|
||||||
|
|
||||||
public int TakeInt32()
|
public int TakeInt32()
|
||||||
{
|
{
|
||||||
int num = BitConverter.ToInt32(_data.AsSpan()[_position..(_position + 4)]);
|
int num = BinaryPrimitives.ReadInt32BigEndian(_data.AsSpan()[_position..(_position + 4)]);
|
||||||
|
|
||||||
_position += 4;
|
_position += 4;
|
||||||
|
|
||||||
|
@ -44,7 +46,7 @@ public class BitEnumerator
|
||||||
|
|
||||||
public long TakeInt64()
|
public long TakeInt64()
|
||||||
{
|
{
|
||||||
long num = BitConverter.ToInt64(_data.AsSpan()[_position..(_position + 8)]);
|
long num = BinaryPrimitives.ReadInt64BigEndian(_data.AsSpan()[_position..(_position + 8)]);
|
||||||
|
|
||||||
_position += 8;
|
_position += 8;
|
||||||
|
|
||||||
|
@ -53,7 +55,7 @@ public class BitEnumerator
|
||||||
|
|
||||||
public ushort TakeUInt16()
|
public ushort TakeUInt16()
|
||||||
{
|
{
|
||||||
ushort num = BitConverter.ToUInt16(_data.AsSpan()[_position..(_position + 2)]);
|
ushort num = BinaryPrimitives.ReadUInt16BigEndian(_data.AsSpan()[_position..(_position + 2)]);
|
||||||
|
|
||||||
_position += 2;
|
_position += 2;
|
||||||
|
|
||||||
|
@ -62,7 +64,7 @@ public class BitEnumerator
|
||||||
|
|
||||||
public uint TakeUInt32()
|
public uint TakeUInt32()
|
||||||
{
|
{
|
||||||
uint num = BitConverter.ToUInt32(_data.AsSpan()[_position..(_position + 4)]);
|
uint num = BinaryPrimitives.ReadUInt32BigEndian(_data.AsSpan()[_position..(_position + 4)]);
|
||||||
|
|
||||||
_position += 4;
|
_position += 4;
|
||||||
|
|
||||||
|
@ -71,7 +73,7 @@ public class BitEnumerator
|
||||||
|
|
||||||
public ulong TakeUInt64()
|
public ulong TakeUInt64()
|
||||||
{
|
{
|
||||||
ulong num = BitConverter.ToUInt64(_data.AsSpan()[_position..(_position + 8)]);
|
ulong num = BinaryPrimitives.ReadUInt64BigEndian(_data.AsSpan()[_position..(_position + 8)]);
|
||||||
|
|
||||||
_position += 8;
|
_position += 8;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,17 @@ public class Host : IBindable
|
||||||
{
|
{
|
||||||
[Key]
|
[Key]
|
||||||
public required string Id { get; set; }
|
public required string Id { get; set; }
|
||||||
public DateTime? LastPing { get; set; }
|
[MaxLength(16)]
|
||||||
|
public byte[] Duid { get; set; } = default!;
|
||||||
|
public HostType HostType { get; set; }
|
||||||
public required IPAddressCollection Addresses { get; set; }
|
public required IPAddressCollection Addresses { get; set; }
|
||||||
public required MacAddress Mac { get; set; }
|
public required MacAddress Mac { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum HostType : byte
|
||||||
|
{
|
||||||
|
Physical = 0b1,
|
||||||
|
Virtual = 0b10,
|
||||||
|
Container = 0b100,
|
||||||
|
Other = 0b1000000,
|
||||||
|
}
|
||||||
|
|
15
src/IPMeow.Lib/Host/HostInterface.cs
Normal file
15
src/IPMeow.Lib/Host/HostInterface.cs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace IPMeow.Lib.Host;
|
||||||
|
|
||||||
|
public class HostInterface
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
public required string Id { get; set; }
|
||||||
|
public byte[] IAID { get; set; } = default!;
|
||||||
|
public DateTime LastPinged { get; set; }
|
||||||
|
public DateTime LastAssignment { get; set; }
|
||||||
|
|
||||||
|
public string HostId { get; set; } = default!;
|
||||||
|
public virtual Host Host { get; set; } = default!;
|
||||||
|
}
|
9
src/IPMeow.Lib/Request/AddressRequestContext.cs
Normal file
9
src/IPMeow.Lib/Request/AddressRequestContext.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using IPMeow.Lib.Address;
|
||||||
|
|
||||||
|
namespace IPMeow.Lib.Request;
|
||||||
|
|
||||||
|
public class AddressRequestContext
|
||||||
|
{
|
||||||
|
public bool IsV6 { get; set; }
|
||||||
|
public MacAddress? MacAddress { get; set; }
|
||||||
|
}
|
8
src/IPMeow.Lib/Request/IAddressProvider.cs
Normal file
8
src/IPMeow.Lib/Request/IAddressProvider.cs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace IPMeow.Lib.Request;
|
||||||
|
|
||||||
|
public interface IAddressProvider<T> where T : AddressRequestContext
|
||||||
|
{
|
||||||
|
public IPAddress GetAddress(T ctx);
|
||||||
|
}
|
6
src/IPMeow.Lib/Request/IRequestClientIdentifier.cs
Normal file
6
src/IPMeow.Lib/Request/IRequestClientIdentifier.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace IPMeow.Lib.Request;
|
||||||
|
|
||||||
|
public interface IRequestClientIdentifier
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
21
src/IPMeow.Server/Controllers/AddressController.cs
Normal file
21
src/IPMeow.Server/Controllers/AddressController.cs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
using IPMeow.Lib.Request;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace IPMeow.Server.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class AddressController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpPost("request")]
|
||||||
|
public async Task<IActionResult> RequestAddress(AddressRequestContext ctx)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> ConfirmRequest()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
26
src/IPMeow.Server/Data/IpamContext.cs
Normal file
26
src/IPMeow.Server/Data/IpamContext.cs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
using IPMeow.Lib.Host;
|
||||||
|
using IPMeow.Server.Data.Model;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace IPMeow.Server.Data;
|
||||||
|
|
||||||
|
public class IpamContext : DbContext
|
||||||
|
{
|
||||||
|
public DbSet<Lib.Host.Host> Hosts => Set<Lib.Host.Host>();
|
||||||
|
public DbSet<HostInterface> HostInterfaces => Set<HostInterface>();
|
||||||
|
public DbSet<AddressAssignment> Assignments => Set<AddressAssignment>();
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder opt)
|
||||||
|
{
|
||||||
|
opt.LogTo(t => Console.WriteLine(t));
|
||||||
|
opt.EnableDetailedErrors();
|
||||||
|
opt.EnableSensitiveDataLogging();
|
||||||
|
base.OnConfiguring(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder mb)
|
||||||
|
{
|
||||||
|
mb.Entity<AddressAssignment>().HasKey(ass => new { Address = ass.Address.GetAddressBytes(), HostId = ass.HostInterfaceId });
|
||||||
|
base.OnModelCreating(mb);
|
||||||
|
}
|
||||||
|
}
|
13
src/IPMeow.Server/Data/Model/AddressAssignment.cs
Normal file
13
src/IPMeow.Server/Data/Model/AddressAssignment.cs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Net;
|
||||||
|
using IPMeow.Lib.Host;
|
||||||
|
|
||||||
|
namespace IPMeow.Server.Data.Model;
|
||||||
|
|
||||||
|
public class AddressAssignment
|
||||||
|
{
|
||||||
|
public required string HostInterfaceId { get; set; }
|
||||||
|
public required IPAddress Address { get; set; }
|
||||||
|
public DateTime LastAssigned { get; set; }
|
||||||
|
|
||||||
|
public virtual HostInterface HostInterface { get; set; } = default!;
|
||||||
|
}
|
9
src/IPMeow.Server/Data/Model/AddressBlock.cs
Normal file
9
src/IPMeow.Server/Data/Model/AddressBlock.cs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
using System.Net;
|
||||||
|
|
||||||
|
namespace IPMeow.Server.Data.Model;
|
||||||
|
|
||||||
|
public class AddressBlock
|
||||||
|
{
|
||||||
|
public required IPAddress Address { get; set; }
|
||||||
|
public required int Prefix { get; set; }
|
||||||
|
}
|
6
src/IPMeow.Server/Data/Model/AddressPolicy.cs
Normal file
6
src/IPMeow.Server/Data/Model/AddressPolicy.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace IPMeow.Server.Data.Model;
|
||||||
|
|
||||||
|
public class AddressPolicy
|
||||||
|
{
|
||||||
|
public bool IsTemporary { get; set; }
|
||||||
|
}
|
|
@ -1,5 +1,14 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\IPMeow.Lib\IPMeow.Lib.csproj" />
|
||||||
|
<ProjectReference Include="..\IPMeow.Dhcp\IPMeow.Dhcp.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
|
using IPMeow.Server.Data;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<IpamContext>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using IPMeow.Lib;
|
|
||||||
using IPMeow.Lib.Address;
|
using IPMeow.Lib.Address;
|
||||||
|
|
||||||
namespace IPMeow.Test;
|
namespace IPMeow.Test;
|
||||||
|
|
Loading…
Reference in a new issue