Migrate existing code
This commit is contained in:
parent
384ff4a6f3
commit
9b49f880a2
30 changed files with 1789 additions and 5 deletions
|
|
@ -8,6 +8,13 @@
|
||||||
"libman"
|
"libman"
|
||||||
],
|
],
|
||||||
"rollForward": false
|
"rollForward": false
|
||||||
|
},
|
||||||
|
"dotnet-outdated-tool": {
|
||||||
|
"version": "4.6.4",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-outdated"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
10
Integrations/Kea/IKeaDhcpLeaseHandler.cs
Normal file
10
Integrations/Kea/IKeaDhcpLeaseHandler.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
using DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
using nietras.SeparatedValues;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public interface IKeaDhcpLeaseHandler
|
||||||
|
{
|
||||||
|
DhcpLeaseChange? Handle(in SepReader.Row row);
|
||||||
|
}
|
||||||
12
Integrations/Kea/IKeaFactory.cs
Normal file
12
Integrations/Kea/IKeaFactory.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public interface IKeaFactory
|
||||||
|
{
|
||||||
|
KeaDhcp4LeaseHandler CreateHandler4();
|
||||||
|
|
||||||
|
KeaDhcp6LeaseHandler CreateHandler6();
|
||||||
|
|
||||||
|
KeaDhcpLeaseWatcher CreateWatcher(IKeaDhcpLeaseHandler handler, KeaDhcpServerOptions options);
|
||||||
|
}
|
||||||
142
Integrations/Kea/KeaDhcp4Lease.cs
Normal file
142
Integrations/Kea/KeaDhcp4Lease.cs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
|
using nietras.SeparatedValues;
|
||||||
|
|
||||||
|
using Cell = System.ReadOnlySpan<char>;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
using Lease = KeaDhcp4Lease;
|
||||||
|
|
||||||
|
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/dhcpsrv/csv_lease_file4.h
|
||||||
|
public record struct KeaDhcp4Lease(
|
||||||
|
IPAddress Address,
|
||||||
|
PhysicalAddress HWAddr,
|
||||||
|
string? ClientId,
|
||||||
|
TimeSpan ValidLifetime,
|
||||||
|
DateTimeOffset Expire,
|
||||||
|
uint SubnetId,
|
||||||
|
bool FqdnFwd,
|
||||||
|
bool FqdnRev,
|
||||||
|
string Hostname,
|
||||||
|
uint State,
|
||||||
|
string? UserContext,
|
||||||
|
uint PoolId)
|
||||||
|
{
|
||||||
|
public static Lease? Parse(in SepReader.Row row)
|
||||||
|
{
|
||||||
|
Lease result = new();
|
||||||
|
for (int i = 0; i < row.ColCount; i++)
|
||||||
|
{
|
||||||
|
if (Parse(ref result, i, row[i].Span) == false)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool? Parse(ref Lease lease, int column, in Cell span)
|
||||||
|
{
|
||||||
|
return column switch
|
||||||
|
{
|
||||||
|
0 => ToIPAddress(ref lease, span),
|
||||||
|
1 when !span.IsWhiteSpace() => ToHWAddr(ref lease, span),
|
||||||
|
2 => ToClientId(ref lease, span),
|
||||||
|
3 => ToValidLifetime(ref lease, span),
|
||||||
|
4 => ToExpire(ref lease, span),
|
||||||
|
5 => ToSubnetId(ref lease, span),
|
||||||
|
6 => ToFqdnFwd(ref lease, span),
|
||||||
|
7 => ToFqdnRev(ref lease, span),
|
||||||
|
8 => ToHostname(ref lease, span),
|
||||||
|
9 => ToState(ref lease, span),
|
||||||
|
10 => ToUserContext(ref lease, span),
|
||||||
|
11 => ToPoolId(ref lease, span),
|
||||||
|
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool ToIPAddress(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = IPAddress.TryParse(span, out var address);
|
||||||
|
lease.Address = address!;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToHWAddr(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = PhysicalAddress.TryParse(span, out var hwaddr);
|
||||||
|
lease.HWAddr = hwaddr!;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToClientId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
lease.ClientId = span.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToValidLifetime(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var validLifetime);
|
||||||
|
lease.ValidLifetime = TimeSpan.FromSeconds(validLifetime);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToExpire(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = ulong.TryParse(span, out var expire);
|
||||||
|
lease.Expire = DateTimeOffset.FromUnixTimeSeconds(unchecked((long)expire));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToSubnetId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var subnetId);
|
||||||
|
lease.SubnetId = subnetId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToFqdnFwd(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = byte.TryParse(span, out var fqdnFwd);
|
||||||
|
lease.FqdnFwd = fqdnFwd != 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToFqdnRev(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = byte.TryParse(span, out var fqdnRev);
|
||||||
|
lease.FqdnRev = fqdnRev != 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToHostname(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
lease.Hostname = KeaDhcpLease.Unescape(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToState(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var state);
|
||||||
|
lease.State = state;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToUserContext(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
lease.UserContext = KeaDhcpLease.Unescape(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToPoolId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var poolId);
|
||||||
|
lease.PoolId = poolId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Integrations/Kea/KeaDhcp4LeaseHandler.cs
Normal file
32
Integrations/Kea/KeaDhcp4LeaseHandler.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
using DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
using nietras.SeparatedValues;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public class KeaDhcp4LeaseHandler : IKeaDhcpLeaseHandler
|
||||||
|
{
|
||||||
|
public DhcpLeaseChange? Handle(in SepReader.Row row)
|
||||||
|
{
|
||||||
|
if (KeaDhcp4Lease.Parse(row) is not { } lease)
|
||||||
|
{
|
||||||
|
goto exitNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lease.State != 0)
|
||||||
|
{
|
||||||
|
goto exitNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
DhcpLeaseIdentifier identifier = lease.ClientId switch
|
||||||
|
{
|
||||||
|
string clientId when !string.IsNullOrWhiteSpace(clientId) => new DhcpLeaseClientIdentifier(clientId),
|
||||||
|
_ => new DhcpLeaseHWAddrIdentifier(lease.HWAddr)
|
||||||
|
};
|
||||||
|
|
||||||
|
return new(lease.Address, lease.Hostname, identifier, lease.ValidLifetime);
|
||||||
|
|
||||||
|
exitNull:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
196
Integrations/Kea/KeaDhcp6Lease.cs
Normal file
196
Integrations/Kea/KeaDhcp6Lease.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
|
||||||
|
using nietras.SeparatedValues;
|
||||||
|
|
||||||
|
using Cell = System.ReadOnlySpan<char>;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
using Lease = KeaDhcp6Lease;
|
||||||
|
|
||||||
|
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/dhcpsrv/csv_lease_file6.h
|
||||||
|
public record struct KeaDhcp6Lease(
|
||||||
|
IPAddress Address,
|
||||||
|
string DUId,
|
||||||
|
TimeSpan ValidLifetime,
|
||||||
|
DateTimeOffset Expire,
|
||||||
|
uint SubnetId,
|
||||||
|
uint PrefLifetime,
|
||||||
|
LeaseType LeaseType,
|
||||||
|
uint IAId,
|
||||||
|
byte PrefixLen,
|
||||||
|
bool FqdnFwd,
|
||||||
|
bool FqdnRev,
|
||||||
|
string Hostname,
|
||||||
|
PhysicalAddress HWAddr,
|
||||||
|
uint State,
|
||||||
|
string? UserContext,
|
||||||
|
ushort? HWType,
|
||||||
|
uint? HWAddrSource,
|
||||||
|
uint PoolId)
|
||||||
|
{
|
||||||
|
public static Lease? Parse(in SepReader.Row row)
|
||||||
|
{
|
||||||
|
Lease result = new();
|
||||||
|
for (int i = 0; i < row.ColCount; i++)
|
||||||
|
{
|
||||||
|
if (Parse(ref result, i, row[i].Span) == false)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool? Parse(ref Lease lease, int column, in Cell span)
|
||||||
|
{
|
||||||
|
return column switch
|
||||||
|
{
|
||||||
|
0 => ToAddress(ref lease, span),
|
||||||
|
1 => ToDUId(ref lease, span),
|
||||||
|
2 => ToValidLifetime(ref lease, span),
|
||||||
|
3 => ToExpire(ref lease, span),
|
||||||
|
4 => ToSubnetId(ref lease, span),
|
||||||
|
5 => ToPrefLifetime(ref lease, span),
|
||||||
|
6 => ToLeaseType(ref lease, span),
|
||||||
|
7 => ToIAId(ref lease, span),
|
||||||
|
8 => ToPrefixLen(ref lease, span),
|
||||||
|
9 => ToFqdnFwd(ref lease, span),
|
||||||
|
10 => ToFqdnRev(ref lease, span),
|
||||||
|
11 => ToHostname(ref lease, span),
|
||||||
|
12 when !span.IsWhiteSpace() => ToHWAddr(ref lease, span),
|
||||||
|
13 => ToState(ref lease, span),
|
||||||
|
14 when !span.IsWhiteSpace() => ToUserContext(ref lease, span),
|
||||||
|
15 => ToHWType(ref lease, span),
|
||||||
|
16 => ToHWAddrSource(ref lease, span),
|
||||||
|
17 => ToPoolId(ref lease, span),
|
||||||
|
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool ToAddress(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = IPAddress.TryParse(span, out var address);
|
||||||
|
lease.Address = address!;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToDUId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
lease.DUId = span.ToString();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToValidLifetime(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var validLifetime);
|
||||||
|
lease.ValidLifetime = TimeSpan.FromSeconds(validLifetime);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToExpire(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = ulong.TryParse(span, out var expire);
|
||||||
|
lease.Expire = DateTimeOffset.FromUnixTimeSeconds(unchecked((long)expire));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToSubnetId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var subnetId);
|
||||||
|
lease.SubnetId = subnetId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToPrefLifetime(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var prefLifetime);
|
||||||
|
lease.PrefLifetime = prefLifetime;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToLeaseType(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = byte.TryParse(span, out var leaseType);
|
||||||
|
lease.LeaseType = (LeaseType)leaseType;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToIAId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var iaId);
|
||||||
|
lease.IAId = iaId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToPrefixLen(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = byte.TryParse(span, out var prefixLen);
|
||||||
|
lease.PrefixLen = prefixLen;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToFqdnFwd(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = byte.TryParse(span, out var fqdnFwd);
|
||||||
|
lease.FqdnFwd = fqdnFwd != 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToFqdnRev(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = byte.TryParse(span, out var fqdnRev);
|
||||||
|
lease.FqdnRev = fqdnRev != 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToHostname(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
lease.Hostname = KeaDhcpLease.Unescape(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToHWAddr(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = PhysicalAddress.TryParse(span, out var hwAddr);
|
||||||
|
lease.HWAddr = hwAddr!;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToState(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var state);
|
||||||
|
lease.State = state;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToUserContext(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
lease.UserContext = KeaDhcpLease.Unescape(span);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToHWType(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = ushort.TryParse(span, out var hwType);
|
||||||
|
lease.HWType = hwType;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToHWAddrSource(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var hwAddrSource);
|
||||||
|
lease.HWAddrSource = hwAddrSource;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToPoolId(ref Lease lease, in Cell span)
|
||||||
|
{
|
||||||
|
bool result = uint.TryParse(span, out var poolId);
|
||||||
|
lease.PoolId = poolId;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Integrations/Kea/KeaDhcp6LeaseHandler.cs
Normal file
24
Integrations/Kea/KeaDhcp6LeaseHandler.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
using DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
using nietras.SeparatedValues;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public class KeaDhcp6LeaseHandler : IKeaDhcpLeaseHandler
|
||||||
|
{
|
||||||
|
public DhcpLeaseChange? Handle(in SepReader.Row row)
|
||||||
|
{
|
||||||
|
if (KeaDhcp6Lease.Parse(row) is not { } lease)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
DhcpLeaseIdentifier identifier = lease.DUId switch
|
||||||
|
{
|
||||||
|
string clientId when !string.IsNullOrWhiteSpace(clientId) => new DhcpLeaseClientIdentifier(clientId),
|
||||||
|
_ => new DhcpLeaseHWAddrIdentifier(lease.HWAddr)
|
||||||
|
};
|
||||||
|
|
||||||
|
return new(lease.Address, lease.Hostname, identifier, lease.ValidLifetime);
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Integrations/Kea/KeaDhcpLease.cs
Normal file
63
Integrations/Kea/KeaDhcpLease.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
using DotNext.Buffers;
|
||||||
|
|
||||||
|
using CommunityToolkit.HighPerformance.Buffers;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public static class KeaDhcpLease
|
||||||
|
{
|
||||||
|
private static ReadOnlySpan<char> EscapeTag => ['&', '#', 'x'];
|
||||||
|
|
||||||
|
// ref: https://github.com/isc-projects/kea/blob/Kea-2.5.3/src/lib/util/csv_file.cc#L479
|
||||||
|
public static string Unescape(in ReadOnlySpan<char> text)
|
||||||
|
{
|
||||||
|
return text.IndexOf(EscapeTag) switch
|
||||||
|
{
|
||||||
|
-1 => text.ToString(),
|
||||||
|
int i => SlowPath(i, text)
|
||||||
|
};
|
||||||
|
|
||||||
|
static string SlowPath(int esc_pos, in ReadOnlySpan<char> text)
|
||||||
|
{
|
||||||
|
SpanReader<char> reader = new(text);
|
||||||
|
using ArrayPoolBufferWriter<char> writer = new(text.Length);
|
||||||
|
while (reader.RemainingCount > 0)
|
||||||
|
{
|
||||||
|
writer.Write(reader.Read(esc_pos));
|
||||||
|
reader.Advance(EscapeTag.Length);
|
||||||
|
|
||||||
|
bool converted = false;
|
||||||
|
char escapedChar = default;
|
||||||
|
if (reader.RemainingCount >= 2)
|
||||||
|
{
|
||||||
|
if (byte.TryParse(reader.RemainingSpan[..2], NumberStyles.AllowHexSpecifier, null, out var b))
|
||||||
|
{
|
||||||
|
converted = true;
|
||||||
|
escapedChar = (char)b;
|
||||||
|
reader.Advance(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (converted)
|
||||||
|
{
|
||||||
|
writer.Write([escapedChar]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.Write(EscapeTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
esc_pos = reader.RemainingSpan.IndexOf(EscapeTag);
|
||||||
|
if (esc_pos == -1)
|
||||||
|
{
|
||||||
|
writer.Write(reader.ReadToEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.WrittenSpan.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
267
Integrations/Kea/KeaDhcpLeaseWatcher.cs
Normal file
267
Integrations/Kea/KeaDhcpLeaseWatcher.cs
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
using DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
using nietras.SeparatedValues;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public sealed class KeaDhcpLeaseWatcher : IHostedService
|
||||||
|
{
|
||||||
|
private static readonly FileStreamOptions LeaseFileStreamOptions = new()
|
||||||
|
{
|
||||||
|
Access = FileAccess.Read,
|
||||||
|
Mode = FileMode.Open,
|
||||||
|
Options = FileOptions.SequentialScan | FileOptions.Asynchronous,
|
||||||
|
Share = (FileShare)7,
|
||||||
|
};
|
||||||
|
private static readonly SepReaderOptions MemfileReader = Sep.New(',').Reader(o => o with
|
||||||
|
{
|
||||||
|
DisableColCountCheck = true,
|
||||||
|
DisableFastFloat = true
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly Decoder _decoder;
|
||||||
|
private readonly FileSystemWatcher _fsw;
|
||||||
|
private readonly IKeaDhcpLeaseHandler _handler;
|
||||||
|
private readonly string _leaseFile;
|
||||||
|
private readonly Pipe _pipe;
|
||||||
|
private readonly DhcpLeaseQueue _queue;
|
||||||
|
private Channel<FileSystemEventArgs>? _eventChannel;
|
||||||
|
private Task? _executeTask;
|
||||||
|
private CancellationTokenSource? _stoppingCts;
|
||||||
|
|
||||||
|
private KeaDhcpServerOptions Options { get; }
|
||||||
|
|
||||||
|
public KeaDhcpLeaseWatcher(KeaDhcpServerOptions options, IKeaDhcpLeaseHandler handler, DhcpLeaseQueue queue)
|
||||||
|
{
|
||||||
|
Options = options = options with { Leases = PathEx.ExpandPath(options.Leases) };
|
||||||
|
_handler = handler;
|
||||||
|
_queue = queue;
|
||||||
|
|
||||||
|
var leases = options.Leases.AsSpan();
|
||||||
|
if (leases.IsWhiteSpace())
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"{nameof(options.Leases)} must not be empty.", nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
var leaseFile = Path.GetFileName(leases);
|
||||||
|
var leaseDirectory = Path.GetDirectoryName(leases).ToString();
|
||||||
|
if (!Directory.Exists(leaseDirectory))
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"{nameof(options.Leases)} must point to a file in an existing path.", nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
_decoder = Encoding.UTF8.GetDecoder();
|
||||||
|
_leaseFile = leaseFile.ToString();
|
||||||
|
_fsw = new(leaseDirectory, _leaseFile);
|
||||||
|
_fsw.Changed += OnLeaseChanged;
|
||||||
|
_fsw.Error += OnLeaseError;
|
||||||
|
_pipe = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_stoppingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
_executeTask = Reading(_stoppingCts.Token);
|
||||||
|
return _executeTask is { IsCompleted: true } completed ? completed : Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_executeTask is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fsw.EnableRaisingEvents = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_stoppingCts!.Cancel();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TaskCompletionSource taskCompletionSource = new();
|
||||||
|
using (cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource))
|
||||||
|
{
|
||||||
|
await Task.WhenAny(_executeTask, taskCompletionSource.Task).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Reading(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
using CancellationTokenSource loopCts = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
|
||||||
|
var loopToken = loopCts.Token;
|
||||||
|
_eventChannel = Channel.CreateUnbounded<FileSystemEventArgs>();
|
||||||
|
_fsw.EnableRaisingEvents = true;
|
||||||
|
using AutoResetEvent resetEvent = new(false);
|
||||||
|
Task reader = Task.CompletedTask;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (
|
||||||
|
EventArgs readArgs = EventArgs.Empty;
|
||||||
|
!loopToken.IsCancellationRequested;
|
||||||
|
readArgs = await _eventChannel.Reader.ReadAsync(loopToken))
|
||||||
|
{
|
||||||
|
// Guard for Deleted and renamed away events,
|
||||||
|
// both have to stop this reader immediately.
|
||||||
|
// Just wait for the file being created/renamed to _leaseFile.
|
||||||
|
// Described in [The LFC Process](https://kea.readthedocs.io/en/latest/arm/lfc.html#kea-lfc)
|
||||||
|
switch (readArgs)
|
||||||
|
{
|
||||||
|
case FileSystemEventArgs { ChangeType: WatcherChangeTypes.Deleted }:
|
||||||
|
case RenamedEventArgs renamed when renamed.OldName == _leaseFile:
|
||||||
|
loopCts.Cancel();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader is not { IsCompleted: false })
|
||||||
|
{
|
||||||
|
// In any case that the reader failed (for whatever reason)
|
||||||
|
// restart now.
|
||||||
|
// Incoming event could be Changed/Created/Renamed
|
||||||
|
// This doesn't care, as we already lost the file handle.
|
||||||
|
reader = FileReader(resetEvent, stoppingToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resetEvent.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_eventChannel.Writer.TryComplete();
|
||||||
|
if (reader is { IsCompleted: false })
|
||||||
|
{
|
||||||
|
await reader.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pipe.Reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FileReader(AutoResetEvent waitHandle, CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
SepReader? reader = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PipeWriter writer = _pipe.Writer;
|
||||||
|
using (stoppingToken.Register(s => ((PipeWriter)s!).Complete(null), writer))
|
||||||
|
{
|
||||||
|
using var file = new FileStream(Options.Leases, LeaseFileStreamOptions);
|
||||||
|
|
||||||
|
bool awaitLineFeed = false;
|
||||||
|
int newLinesEncountered = 0;
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
for (; newLinesEncountered > 0; newLinesEncountered--)
|
||||||
|
{
|
||||||
|
if (reader is null)
|
||||||
|
{
|
||||||
|
// LongRunning, force spawning a thread
|
||||||
|
// As this may block for a long time.
|
||||||
|
reader = await Task.Factory.StartNew(
|
||||||
|
s => MemfileReader.From((Stream)s!),
|
||||||
|
_pipe.Reader.AsStream(),
|
||||||
|
stoppingToken,
|
||||||
|
TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning,
|
||||||
|
TaskScheduler.Default)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reader.MoveNext())
|
||||||
|
{
|
||||||
|
// TODO Error state.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_handler.Handle(reader.Current) is not { } lease)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _queue.Write(lease, stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var memory = writer.GetMemory();
|
||||||
|
int read = await file.ReadAsync(memory, stoppingToken);
|
||||||
|
if (read > 0)
|
||||||
|
{
|
||||||
|
CountNewLines(_decoder, memory[..read], ref newLinesEncountered, ref awaitLineFeed);
|
||||||
|
writer.Advance(read);
|
||||||
|
await writer.FlushAsync(stoppingToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await waitHandle.WaitOneAsync(stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
reader?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void CountNewLines(Decoder decoder, in Memory<byte> memory, ref int newLinesEncountered, ref bool awaitLineFeed)
|
||||||
|
{
|
||||||
|
Span<char> buffer = stackalloc char[128];
|
||||||
|
bool completed = false;
|
||||||
|
ReadOnlySequence<byte> sequence = new(memory);
|
||||||
|
var reader = new SequenceReader<byte>(sequence);
|
||||||
|
while (!reader.End)
|
||||||
|
{
|
||||||
|
decoder.Convert(reader.UnreadSpan, buffer, false, out var bytesUsed, out var charsUsed, out completed);
|
||||||
|
reader.Advance(bytesUsed);
|
||||||
|
foreach (ref readonly char c in buffer[..charsUsed])
|
||||||
|
{
|
||||||
|
if (awaitLineFeed || c == '\n')
|
||||||
|
{
|
||||||
|
newLinesEncountered++;
|
||||||
|
awaitLineFeed = false;
|
||||||
|
}
|
||||||
|
else if (c == '\r')
|
||||||
|
{
|
||||||
|
awaitLineFeed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLeaseChanged(object sender, FileSystemEventArgs e)
|
||||||
|
{
|
||||||
|
if (_eventChannel?.Writer is not { } writer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning disable CA2012
|
||||||
|
var task = writer.WriteAsync(e, CancellationToken.None);
|
||||||
|
#pragma warning restore
|
||||||
|
if (task.IsCompleted)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLeaseError(object sender, ErrorEventArgs e)
|
||||||
|
{
|
||||||
|
_eventChannel?.Writer.Complete(e.GetException());
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Integrations/Kea/KeaFactoryServices.cs
Normal file
39
Integrations/Kea/KeaFactoryServices.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public static class KeaFactoryServices
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddKeaFactory(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<IKeaFactory, KeaFactory>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class KeaFactory(IServiceProvider services) : IKeaFactory
|
||||||
|
{
|
||||||
|
private ObjectFactory<KeaDhcp4LeaseHandler>? _cachedCreateHandler4;
|
||||||
|
private ObjectFactory<KeaDhcp6LeaseHandler>? _cachedCreateHandler6;
|
||||||
|
private ObjectFactory<KeaDhcpLeaseWatcher>? _cachedCreateWatcher;
|
||||||
|
|
||||||
|
KeaDhcp4LeaseHandler IKeaFactory.CreateHandler4()
|
||||||
|
{
|
||||||
|
_cachedCreateHandler4 ??= ActivatorUtilities.CreateFactory<KeaDhcp4LeaseHandler>([]);
|
||||||
|
return _cachedCreateHandler4(services, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeaDhcp6LeaseHandler IKeaFactory.CreateHandler6()
|
||||||
|
{
|
||||||
|
_cachedCreateHandler6 ??= ActivatorUtilities.CreateFactory<KeaDhcp6LeaseHandler>([]);
|
||||||
|
return _cachedCreateHandler6(services, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
KeaDhcpLeaseWatcher IKeaFactory.CreateWatcher(IKeaDhcpLeaseHandler handler, KeaDhcpServerOptions options)
|
||||||
|
{
|
||||||
|
_cachedCreateWatcher ??= ActivatorUtilities.CreateFactory<KeaDhcpLeaseWatcher>([typeof(IKeaDhcpLeaseHandler), typeof(KeaDhcpServerOptions)]);
|
||||||
|
return _cachedCreateWatcher(services, [handler, options]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Integrations/Kea/KeaService.cs
Normal file
54
Integrations/Kea/KeaService.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public class KeaService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ImmutableArray<IHostedService> _services;
|
||||||
|
|
||||||
|
public KeaService(KeaDhcpOptions options, IKeaFactory factory)
|
||||||
|
{
|
||||||
|
var services = ImmutableArray.CreateBuilder<IHostedService>();
|
||||||
|
|
||||||
|
if (options.Dhcp4 is { } dhcp4Options)
|
||||||
|
{
|
||||||
|
services.Add(factory.CreateWatcher(factory.CreateHandler4(), dhcp4Options));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.Dhcp6 is { } dhcp6Options)
|
||||||
|
{
|
||||||
|
services.Add(factory.CreateWatcher(factory.CreateHandler6(), dhcp6Options));
|
||||||
|
}
|
||||||
|
|
||||||
|
_services = services.DrainToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Task[] tasks = new Task[_services.Length];
|
||||||
|
for (int i = 0; i < tasks.Length; i++)
|
||||||
|
{
|
||||||
|
tasks[i] = _services[i].StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Task[] tasks = new Task[_services.Length];
|
||||||
|
for (int i = 0; i < tasks.Length; i++)
|
||||||
|
{
|
||||||
|
tasks[i] = _services[i].StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var waitTask = Task.WhenAll(tasks);
|
||||||
|
TaskCompletionSource taskCompletionSource = new();
|
||||||
|
using var registration = cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource);
|
||||||
|
await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Integrations/Kea/LeaseType.cs
Normal file
9
Integrations/Kea/LeaseType.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace DotNetDDI.Integrations.Kea;
|
||||||
|
|
||||||
|
public enum LeaseType : byte
|
||||||
|
{
|
||||||
|
NonTempraryIPv6 = 0,
|
||||||
|
TemporaryIPv6 = 1,
|
||||||
|
IPv6Prefix = 2,
|
||||||
|
IPv4 = 3
|
||||||
|
}
|
||||||
12
Integrations/PowerDns/Methods.cs
Normal file
12
Integrations/PowerDns/Methods.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace DotNetDDI.Integrations.PowerDns;
|
||||||
|
|
||||||
|
public interface IMethod;
|
||||||
|
|
||||||
|
public record MethodBase(string Method);
|
||||||
|
|
||||||
|
public record Method<TParam>(string Method, TParam Parameters) : MethodBase(Method)
|
||||||
|
where TParam : MethodParameters;
|
||||||
|
|
||||||
|
public record InitializeMethod(InitializeParameters Parameters) : Method<InitializeParameters>("initialize", Parameters), IMethod;
|
||||||
|
|
||||||
|
public record LookupMethod(LookupParameters Parameters) : Method<LookupParameters>("lookup", Parameters), IMethod;
|
||||||
55
Integrations/PowerDns/Parameters.cs
Normal file
55
Integrations/PowerDns/Parameters.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.PowerDns;
|
||||||
|
|
||||||
|
public record class Parameters;
|
||||||
|
|
||||||
|
[JsonDerivedType(typeof(InitializeParameters))]
|
||||||
|
[JsonDerivedType(typeof(LookupParameters))]
|
||||||
|
public record class MethodParameters : Parameters
|
||||||
|
{
|
||||||
|
[JsonExtensionData]
|
||||||
|
public Dictionary<string, JsonElement> AdditionalProperties { get; set; } = [];
|
||||||
|
|
||||||
|
protected override bool PrintMembers(StringBuilder builder)
|
||||||
|
{
|
||||||
|
if (base.PrintMembers(builder))
|
||||||
|
{
|
||||||
|
builder.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append("AdditionalProperties = [");
|
||||||
|
bool append = false;
|
||||||
|
foreach (var kv in AdditionalProperties)
|
||||||
|
{
|
||||||
|
if (append)
|
||||||
|
{
|
||||||
|
builder.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
append = true;
|
||||||
|
builder.Append(kv.Key);
|
||||||
|
builder.Append(" = ");
|
||||||
|
builder.Append(kv.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(']');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record class InitializeParameters(
|
||||||
|
[property: JsonPropertyName("command")] string Command,
|
||||||
|
[property: JsonPropertyName("timeout")] int Timeout
|
||||||
|
) : MethodParameters;
|
||||||
|
|
||||||
|
public record class LookupParameters(
|
||||||
|
[property: JsonPropertyName("qtype")] string Qtype,
|
||||||
|
[property: JsonPropertyName("qname")] string Qname,
|
||||||
|
[property: JsonPropertyName("remote")] string Remote,
|
||||||
|
[property: JsonPropertyName("local")] string Local,
|
||||||
|
[property: JsonPropertyName("real-remote")] string RealRemote,
|
||||||
|
[property: JsonPropertyName("zone-id")] int ZoneId
|
||||||
|
) : MethodParameters;
|
||||||
272
Integrations/PowerDns/PowerDnsHandler.cs
Normal file
272
Integrations/PowerDns/PowerDnsHandler.cs
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO.Pipelines;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
using CommunityToolkit.HighPerformance;
|
||||||
|
using CommunityToolkit.HighPerformance.Buffers;
|
||||||
|
|
||||||
|
using DotNetDDI.Services.Dns;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Connections;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.PowerDns;
|
||||||
|
|
||||||
|
public class PowerDnsHandler : ConnectionHandler
|
||||||
|
{
|
||||||
|
delegate MethodBase? HandlerConverter(in JsonElement element);
|
||||||
|
|
||||||
|
private static readonly ReadOnlyDictionary<string, HandlerConverter> Converters;
|
||||||
|
|
||||||
|
private readonly ILogger<PowerDnsHandler> _logger;
|
||||||
|
private readonly DnsRepository _repository;
|
||||||
|
|
||||||
|
static PowerDnsHandler()
|
||||||
|
{
|
||||||
|
Dictionary<string, HandlerConverter> converters = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["initialize"] = new(ToInitialize),
|
||||||
|
["lookup"] = new(ToLookup)
|
||||||
|
};
|
||||||
|
|
||||||
|
Converters = converters.AsReadOnly();
|
||||||
|
|
||||||
|
static InitializeMethod ToInitialize(in JsonElement element)
|
||||||
|
{
|
||||||
|
return new(element.Deserialize(PowerDnsSerializerContext.Default.InitializeParameters)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LookupMethod ToLookup(in JsonElement element)
|
||||||
|
{
|
||||||
|
return new(element.Deserialize(PowerDnsSerializerContext.Default.LookupParameters)!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PowerDnsHandler(DnsRepository repository, ILogger<PowerDnsHandler> logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnConnectedAsync(ConnectionContext connection)
|
||||||
|
{
|
||||||
|
var input = connection.Transport.Input;
|
||||||
|
JsonReaderState state = default;
|
||||||
|
using ArrayPoolBufferWriter<byte> json = new();
|
||||||
|
using ArrayPoolBufferWriter<byte> buffer = new();
|
||||||
|
using var writer = connection.Transport.Output.AsStream();
|
||||||
|
while (!connection.ConnectionClosed.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (await ReadAsync(input, connection.ConnectionClosed) is not { IsCanceled: false } read)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var memory in read.Buffer)
|
||||||
|
{
|
||||||
|
buffer.Write(memory.Span);
|
||||||
|
if (!ConsumeJson(buffer, json, ref state))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Reply reply = BoolReply.False;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MethodBase method;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var jsonDocument = JsonDocument.Parse(json.WrittenMemory);
|
||||||
|
var root = jsonDocument.RootElement;
|
||||||
|
if (!root.TryGetProperty("method", out var methodElement))
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Json Document missing required property method: {document}", jsonDocument);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Parse(methodElement, root.GetProperty("parameters")) is not { } methodLocal)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
method = methodLocal;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
json.Clear();
|
||||||
|
state = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply = await Handle(method, connection.ConnectionClosed).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.LogError(e, "Error");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await JsonSerializer.SerializeAsync(writer, reply, PowerDnsSerializerContext.Default.Reply, connection.ConnectionClosed)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.AdvanceTo(read.Buffer.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async ValueTask<ReadResult?> ReadAsync(PipeReader reader, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await reader.ReadAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ConsumeJson(ArrayPoolBufferWriter<byte> inflight, ArrayPoolBufferWriter<byte> json, ref JsonReaderState state)
|
||||||
|
{
|
||||||
|
bool final = false;
|
||||||
|
Utf8JsonReader reader = new(inflight.WrittenSpan, false, state);
|
||||||
|
while (!final && reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == 0)
|
||||||
|
{
|
||||||
|
final = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = reader.CurrentState;
|
||||||
|
int consumed = (int)reader.BytesConsumed;
|
||||||
|
if (consumed > 0)
|
||||||
|
{
|
||||||
|
json.Write(inflight.WrittenSpan[..consumed]);
|
||||||
|
|
||||||
|
Span<byte> buffer = default;
|
||||||
|
var remaining = inflight.WrittenCount - consumed;
|
||||||
|
if (remaining > 0)
|
||||||
|
{
|
||||||
|
buffer = inflight.GetSpan(remaining)[..remaining];
|
||||||
|
inflight.WrittenSpan[consumed..].CopyTo(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear only clears up until WrittenCount
|
||||||
|
// thus data after write-head is safe
|
||||||
|
inflight.Clear();
|
||||||
|
if (!buffer.IsEmpty)
|
||||||
|
{
|
||||||
|
inflight.Write(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return final;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MethodBase? Parse(in JsonElement element, in JsonElement parameters)
|
||||||
|
{
|
||||||
|
HandlerConverter? converter = default;
|
||||||
|
return element.GetString() switch
|
||||||
|
{
|
||||||
|
null => null,
|
||||||
|
{ } methodName when !Converters.TryGetValue(methodName, out converter) => new Method<MethodParameters>(methodName, parameters.Deserialize(PowerDnsSerializerContext.Default.MethodParameters)!),
|
||||||
|
_ => converter(parameters)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<Reply> Handle(MethodBase method, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return method switch
|
||||||
|
{
|
||||||
|
InitializeMethod { Parameters: { } init } => HandleInitialize(init),
|
||||||
|
LookupMethod { Parameters: { } lookup } => HandleLookup(lookup),
|
||||||
|
|
||||||
|
_ => LogUnhandled(_logger, method)
|
||||||
|
};
|
||||||
|
|
||||||
|
static ValueTask<Reply> LogUnhandled(ILogger logger, MethodBase method)
|
||||||
|
{
|
||||||
|
logger.LogWarning("Unhandled {Method}", method);
|
||||||
|
return ValueTask.FromResult<Reply>(BoolReply.False);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<Reply> HandleInitialize(InitializeParameters parameters)
|
||||||
|
{
|
||||||
|
if (_logger.IsEnabled(LogLevel.Information))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Handling {parameters}", parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValueTask.FromResult<Reply>(BoolReply.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueTask<Reply> HandleLookup(LookupParameters parameters)
|
||||||
|
{
|
||||||
|
AddressFamily? qtype = parameters.Qtype.ToUpperInvariant() switch
|
||||||
|
{
|
||||||
|
"ANY" => AddressFamily.Unknown,
|
||||||
|
"A" => AddressFamily.InterNetwork,
|
||||||
|
"AAAA" => AddressFamily.InterNetworkV6,
|
||||||
|
_ => default(AddressFamily?)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (qtype is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Unhandled QType {QType}", parameters.Qtype);
|
||||||
|
return ValueTask.FromResult<Reply>(BoolReply.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_logger.IsEnabled(LogLevel.Information))
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Lookup {key} in {family}", parameters.Qname, parameters.Qtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FindByName(((AddressFamily)qtype, parameters.Qname.AsMemory()), _repository, _logger);
|
||||||
|
|
||||||
|
static async ValueTask<Reply> FindByName((AddressFamily Family, ReadOnlyMemory<char> Qname) query, DnsRepository repository, ILogger logger)
|
||||||
|
{
|
||||||
|
QueryResult[] records = [];
|
||||||
|
|
||||||
|
var qname = query.Qname.Trim().TrimEnd(".");
|
||||||
|
if (qname.Span.IsWhiteSpace())
|
||||||
|
{
|
||||||
|
goto exitEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await repository.FindAsync(record =>
|
||||||
|
{
|
||||||
|
if ((record.RecordType & query.Family) != record.RecordType)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return qname.Span.Equals(record.FQDN, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (results.Count == 0)
|
||||||
|
{
|
||||||
|
goto exitEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
records = new QueryResult[results.Count];
|
||||||
|
for (int i = 0; i < results.Count; i++)
|
||||||
|
{
|
||||||
|
DnsRecord record = results[i];
|
||||||
|
#pragma warning disable CS8509 // RecordType is by convention InterNetwork or InterNetworkV6
|
||||||
|
records[i] = new(record.RecordType switch
|
||||||
|
#pragma warning restore
|
||||||
|
{
|
||||||
|
AddressFamily.InterNetwork => "A",
|
||||||
|
AddressFamily.InterNetworkV6 => "AAAA"
|
||||||
|
}, record.FQDN, record.Address.ToString(), (int)record.Lifetime.TotalSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
exitEmpty:
|
||||||
|
return new LookupReply(records);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Integrations/PowerDns/PowerDnsSerializerContext.cs
Normal file
13
Integrations/PowerDns/PowerDnsSerializerContext.cs
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.PowerDns;
|
||||||
|
|
||||||
|
[JsonSerializable(typeof(Reply))]
|
||||||
|
[JsonSerializable(typeof(MethodParameters))]
|
||||||
|
[JsonSourceGenerationOptions(
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||||
|
UseStringEnumConverter = true,
|
||||||
|
WriteIndented = false
|
||||||
|
)]
|
||||||
|
internal partial class PowerDnsSerializerContext : JsonSerializerContext;
|
||||||
39
Integrations/PowerDns/Replies.cs
Normal file
39
Integrations/PowerDns/Replies.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Integrations.PowerDns;
|
||||||
|
|
||||||
|
[JsonDerivedType(typeof(BoolReply))]
|
||||||
|
[JsonDerivedType(typeof(LookupReply))]
|
||||||
|
public abstract class Reply;
|
||||||
|
|
||||||
|
public abstract class Reply<T>(T result) : Reply
|
||||||
|
{
|
||||||
|
[JsonPropertyName("result")]
|
||||||
|
public T Result => result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BoolReply(bool result) : Reply<bool>(result)
|
||||||
|
{
|
||||||
|
public static BoolReply False { get; } = new BoolReply(false);
|
||||||
|
|
||||||
|
public static BoolReply True { get; } = new BoolReply(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LookupReply(QueryResult[] result) : Reply<QueryResult[]>(result);
|
||||||
|
|
||||||
|
public record QueryResult(
|
||||||
|
[property: JsonPropertyName("qtype")] string QType,
|
||||||
|
[property: JsonPropertyName("qname")] string QName,
|
||||||
|
[property: JsonPropertyName("content")] string Content,
|
||||||
|
[property: JsonPropertyName("ttl")] int TTL
|
||||||
|
)
|
||||||
|
{
|
||||||
|
[JsonPropertyName("auth")]
|
||||||
|
public bool? Auth { get; init; } = default;
|
||||||
|
|
||||||
|
[JsonPropertyName("domain_id")]
|
||||||
|
public int? DomainId { get; init; } = default;
|
||||||
|
|
||||||
|
[JsonPropertyName("scopeMask")]
|
||||||
|
public int? ScopeMask { get; init; } = default;
|
||||||
|
}
|
||||||
6
Options/DhcpOptions.cs
Normal file
6
Options/DhcpOptions.cs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace DotNetDDI.Options;
|
||||||
|
|
||||||
|
public class DhcpOptions
|
||||||
|
{
|
||||||
|
public KeaDhcpOptions? Kea { get; set; }
|
||||||
|
}
|
||||||
10
Options/KeaDhcpOptions.cs
Normal file
10
Options/KeaDhcpOptions.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace DotNetDDI.Options;
|
||||||
|
|
||||||
|
public class KeaDhcpOptions
|
||||||
|
{
|
||||||
|
public KeaDhcpServerOptions? Dhcp4 { get; set; }
|
||||||
|
|
||||||
|
public KeaDhcpServerOptions? Dhcp6 { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record class KeaDhcpServerOptions(string Leases);
|
||||||
10
Options/PowerDnsOptions.cs
Normal file
10
Options/PowerDnsOptions.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
namespace DotNetDDI.Options;
|
||||||
|
|
||||||
|
public class PowerDnsOptions
|
||||||
|
{
|
||||||
|
public PowerDnsListenerOptions Listener { get; init; } = default!;
|
||||||
|
|
||||||
|
public bool UniqueHostnames { get; init; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record class PowerDnsListenerOptions(string Socket);
|
||||||
|
|
@ -30,8 +30,7 @@ app.UseRouting();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapStaticAssets();
|
app.UseStaticFiles();
|
||||||
app.MapRazorPages()
|
app.MapRazorPages();
|
||||||
.WithStaticAssets();
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
|
||||||
36
Services/Dhcp/DhcpLeaseQueue.cs
Normal file
36
Services/Dhcp/DhcpLeaseQueue.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
public class DhcpLeaseQueue
|
||||||
|
{
|
||||||
|
private readonly Channel<DhcpLeaseChange> _pipe;
|
||||||
|
private readonly ChannelReader<DhcpLeaseChange> _reader;
|
||||||
|
private readonly ChannelWriter<DhcpLeaseChange> _writer;
|
||||||
|
|
||||||
|
public ref readonly ChannelReader<DhcpLeaseChange> Reader => ref _reader;
|
||||||
|
|
||||||
|
public DhcpLeaseQueue()
|
||||||
|
{
|
||||||
|
_pipe = Channel.CreateUnbounded<DhcpLeaseChange>();
|
||||||
|
_reader = _pipe.Reader;
|
||||||
|
_writer = _pipe.Writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask Write(DhcpLeaseChange change, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return _writer.WriteAsync(change, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct DhcpLeaseChange(IPAddress Address, string FQDN, DhcpLeaseIdentifier Identifier, TimeSpan Lifetime)
|
||||||
|
{
|
||||||
|
public AddressFamily LeaseType { get; } = Address.AddressFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DhcpLeaseIdentifier;
|
||||||
|
public record DhcpLeaseClientIdentifier(string ClientId) : DhcpLeaseIdentifier;
|
||||||
|
public record DhcpLeaseHWAddrIdentifier(PhysicalAddress HWAddr) : DhcpLeaseIdentifier;
|
||||||
44
Services/Dhcp/DhcpQueueWorker.cs
Normal file
44
Services/Dhcp/DhcpQueueWorker.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Threading.Channels;
|
||||||
|
|
||||||
|
using DotNetDDI.Services.Dns;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
public class DhcpQueueWorker : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly ChannelReader<DhcpLeaseChange> _channelReader;
|
||||||
|
private readonly DnsRepository _repository;
|
||||||
|
|
||||||
|
public DhcpQueueWorker(DhcpLeaseQueue queue, DnsRepository repository)
|
||||||
|
{
|
||||||
|
_channelReader = queue.Reader;
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
while (await _channelReader.WaitToReadAsync(stoppingToken).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
while (_channelReader.TryRead(out var lease))
|
||||||
|
{
|
||||||
|
DnsRecordIdentifier identifier = lease.Identifier switch
|
||||||
|
{
|
||||||
|
DhcpLeaseClientIdentifier clientId => new DnsRecordClientIdentifier(clientId.ClientId),
|
||||||
|
DhcpLeaseHWAddrIdentifier hwAddr => new DnsRecordHWAddrIdentifier(hwAddr.HWAddr),
|
||||||
|
_ => throw new ArgumentException(nameof(lease.Identifier))
|
||||||
|
};
|
||||||
|
|
||||||
|
TimeSpan lifetime = lease.Lifetime.TotalSeconds switch
|
||||||
|
{
|
||||||
|
<= 1800 => TimeSpan.FromSeconds(600),
|
||||||
|
>= 10800 => TimeSpan.FromSeconds(3600),
|
||||||
|
{ } seconds => TimeSpan.FromSeconds(seconds / 3)
|
||||||
|
};
|
||||||
|
|
||||||
|
await _repository.Record(new DnsRecord(lease.Address, lease.FQDN, identifier, lifetime), stoppingToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Services/Dhcp/DhcpWatcher.cs
Normal file
50
Services/Dhcp/DhcpWatcher.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
public class DhcpWatcher : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ImmutableArray<IHostedService> _services;
|
||||||
|
|
||||||
|
public DhcpWatcher(IOptions<DhcpOptions> options, IDhcpWatcherFactory factory)
|
||||||
|
{
|
||||||
|
var dhcpOptions = options.Value;
|
||||||
|
var services = ImmutableArray.CreateBuilder<IHostedService>();
|
||||||
|
if (dhcpOptions.Kea is { } keaOptions)
|
||||||
|
{
|
||||||
|
services.Add(factory.KeaService(keaOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
_services = services.DrainToImmutable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Task[] tasks = new Task[_services.Length];
|
||||||
|
for (int i = 0; i < tasks.Length; i++)
|
||||||
|
{
|
||||||
|
tasks[i] = _services[i].StartAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Task[] tasks = new Task[_services.Length];
|
||||||
|
for (int i = 0; i < tasks.Length; i++)
|
||||||
|
{
|
||||||
|
tasks[i] = _services[i].StopAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var waitTask = Task.WhenAll(tasks);
|
||||||
|
TaskCompletionSource taskCompletionSource = new();
|
||||||
|
using var registration = cancellationToken.Register(s => ((TaskCompletionSource)s!).SetCanceled(), taskCompletionSource);
|
||||||
|
await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Services/Dhcp/DhcpWatcherFactoryServices.cs
Normal file
26
Services/Dhcp/DhcpWatcherFactoryServices.cs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
using DotNetDDI.Integrations.Kea;
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
public static class DhcpWatcherFactoryServices
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddDhcpWatcherFactory(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<IDhcpWatcherFactory, DhcpWatcherFactory>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DhcpWatcherFactory(IServiceProvider services) : IDhcpWatcherFactory
|
||||||
|
{
|
||||||
|
private ObjectFactory<KeaService>? _cachedKeaService;
|
||||||
|
|
||||||
|
KeaService IDhcpWatcherFactory.KeaService(KeaDhcpOptions options)
|
||||||
|
{
|
||||||
|
_cachedKeaService ??= ActivatorUtilities.CreateFactory<KeaService>([typeof(KeaDhcpOptions)]);
|
||||||
|
return _cachedKeaService(services, [options]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Services/Dhcp/IDhcpWatcherFactory.cs
Normal file
9
Services/Dhcp/IDhcpWatcherFactory.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
using DotNetDDI.Integrations.Kea;
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Services.Dhcp;
|
||||||
|
|
||||||
|
public interface IDhcpWatcherFactory
|
||||||
|
{
|
||||||
|
KeaService KeaService(KeaDhcpOptions options);
|
||||||
|
}
|
||||||
157
Services/Dns/DnsRepository.cs
Normal file
157
Services/Dns/DnsRepository.cs
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
|
||||||
|
using DotNetDDI.Options;
|
||||||
|
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
using Timeout = System.Threading.Timeout;
|
||||||
|
|
||||||
|
namespace DotNetDDI.Services.Dns;
|
||||||
|
|
||||||
|
public class DnsRepository
|
||||||
|
{
|
||||||
|
private static ReadOnlySpan<int> Lifetimes => [600, 3600];
|
||||||
|
|
||||||
|
private readonly PowerDnsOptions _options;
|
||||||
|
private readonly ReaderWriterLockSlim _recordLock = new();
|
||||||
|
private readonly List<DnsRecord> _records = [];
|
||||||
|
private readonly SemaphoreSlim _syncLock = new(1, 1);
|
||||||
|
|
||||||
|
public DnsRepository(IOptions<PowerDnsOptions> options)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DnsRecord> Find(Predicate<DnsRecord> query)
|
||||||
|
{
|
||||||
|
bool enteredLock = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
enteredLock = _recordLock.TryEnterReadLock(Timeout.Infinite);
|
||||||
|
return _records.FindAll(query);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (enteredLock)
|
||||||
|
{
|
||||||
|
_recordLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<List<DnsRecord>> FindAsync(Predicate<DnsRecord> query, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.Factory.StartNew(state => Find((Predicate<DnsRecord>)state!), query,
|
||||||
|
cancellationToken, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask Record(DnsRecord record, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
bool entered = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entered = await _syncLock.WaitAsync(Timeout.Infinite, cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
|
||||||
|
RecordContinuation(record);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (entered)
|
||||||
|
{
|
||||||
|
_syncLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RecordContinuation(DnsRecord record)
|
||||||
|
{
|
||||||
|
var search = Matches(record);
|
||||||
|
bool entered = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entered = _recordLock.TryEnterWriteLock(Timeout.Infinite);
|
||||||
|
|
||||||
|
if (search.First is { } node)
|
||||||
|
{
|
||||||
|
search.RemoveFirst();
|
||||||
|
_records[node.Value] = record;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_records.Add(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (search.Last is { } replace)
|
||||||
|
{
|
||||||
|
search.RemoveLast();
|
||||||
|
var last = _records.Count - 1;
|
||||||
|
if (replace.Value < last)
|
||||||
|
{
|
||||||
|
_records[replace.Value] = _records[last];
|
||||||
|
}
|
||||||
|
|
||||||
|
_records.RemoveAt(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (entered)
|
||||||
|
{
|
||||||
|
_recordLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkedList<int> Matches(DnsRecord query)
|
||||||
|
{
|
||||||
|
LinkedList<int> list = [];
|
||||||
|
|
||||||
|
for (int i = 0; i < _records.Count; i++)
|
||||||
|
{
|
||||||
|
var record = _records[i];
|
||||||
|
if (record.RecordType != query.RecordType)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ((record.Identifier, query.Identifier))
|
||||||
|
{
|
||||||
|
case (
|
||||||
|
DnsRecordClientIdentifier { ClientId: { } recordClientId },
|
||||||
|
DnsRecordClientIdentifier { ClientId: { } queryClientId }
|
||||||
|
) when StringComparer.InvariantCultureIgnoreCase.Equals(recordClientId, queryClientId):
|
||||||
|
|
||||||
|
case (
|
||||||
|
DnsRecordHWAddrIdentifier { HWAddr: { } recordHWAddr },
|
||||||
|
DnsRecordHWAddrIdentifier { HWAddr: { } queryHWAddr }
|
||||||
|
) when EqualityComparer<PhysicalAddress>.Default.Equals(recordHWAddr, queryHWAddr):
|
||||||
|
|
||||||
|
list.AddLast(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EqualityComparer<IPAddress>.Default.Equals(record.Address, query.Address))
|
||||||
|
{
|
||||||
|
list.AddLast(i);
|
||||||
|
}
|
||||||
|
else if (_options.UniqueHostnames && StringComparer.OrdinalIgnoreCase.Equals(record.FQDN, query.FQDN))
|
||||||
|
{
|
||||||
|
list.AddLast(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Remove duplication
|
||||||
|
public record DnsRecordIdentifier;
|
||||||
|
public record DnsRecordClientIdentifier(string ClientId) : DnsRecordIdentifier;
|
||||||
|
public record DnsRecordHWAddrIdentifier(PhysicalAddress HWAddr) : DnsRecordIdentifier;
|
||||||
|
// /TODO
|
||||||
|
|
||||||
|
public record DnsRecord(IPAddress Address, string FQDN, DnsRecordIdentifier Identifier, TimeSpan Lifetime)
|
||||||
|
{
|
||||||
|
public AddressFamily RecordType { get; } = Address.AddressFamily;
|
||||||
|
}
|
||||||
113
System/IO/Paths.cs
Normal file
113
System/IO/Paths.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
#if !(LINUX || MACOS || WINDOWS)
|
||||||
|
#define TARGET_ANY
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if TARGET_ANY || LINUX
|
||||||
|
#define TARGET_LINUX
|
||||||
|
#endif
|
||||||
|
#if TARGET_ANY || MACOS
|
||||||
|
#define TARGET_MACOS
|
||||||
|
#endif
|
||||||
|
#if TARGET_ANY || WINDOWS
|
||||||
|
#define TARGET_WINDOWS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
|
||||||
|
using CommunityToolkit.HighPerformance.Buffers;
|
||||||
|
|
||||||
|
namespace System.IO;
|
||||||
|
|
||||||
|
public static class PathEx
|
||||||
|
{
|
||||||
|
public static string ExpandPath(string path)
|
||||||
|
{
|
||||||
|
#if TARGET_ANY
|
||||||
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
#if TARGET_LINUX || TARGET_MACOS
|
||||||
|
return ExpandPathUnixImpl(path);
|
||||||
|
#endif
|
||||||
|
#if TARGET_ANY
|
||||||
|
}
|
||||||
|
else if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
#endif
|
||||||
|
#if TARGET_WINDOWS
|
||||||
|
return ExpandPathWindowsImpl(path);
|
||||||
|
#endif
|
||||||
|
#if TARGET_ANY
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TARGET_LINUX || TARGET_MACOS
|
||||||
|
private static string ExpandPathUnixImpl(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = new SequenceReader<char>(new(path.AsMemory()));
|
||||||
|
if (!reader.TryReadTo(out ReadOnlySpan<char> read, '$'))
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
using ArrayPoolBufferWriter<char> result = new();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
result.Write(read);
|
||||||
|
|
||||||
|
int skip = 0;
|
||||||
|
if (reader.UnreadSpan[0] == '{' && reader.UnreadSpan.IndexOf('}') is not -1 and int s)
|
||||||
|
{
|
||||||
|
read = reader.UnreadSpan[1..s];
|
||||||
|
skip = s + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var propertyReader = new SequenceReader<char>(reader.UnreadSequence);
|
||||||
|
while (!propertyReader.End && propertyReader.UnreadSpan[0] is char c && (c == '_' || char.IsAsciiLetterOrDigit(c)))
|
||||||
|
{
|
||||||
|
propertyReader.Advance(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
read = reader.UnreadSpan[..(int)propertyReader.Consumed];
|
||||||
|
skip = read.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skip != 0 && Environment.GetEnvironmentVariable(read.ToString()) is { } env)
|
||||||
|
{
|
||||||
|
result.Write(env);
|
||||||
|
reader.Advance(skip);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.Write(['$']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reader.TryReadTo(out read, '$'))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Write(reader.UnreadSpan);
|
||||||
|
return result.WrittenSpan.ToString();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if TARGET_WINDOWS
|
||||||
|
private static string ExpandPathWindowsImpl(string path)
|
||||||
|
{
|
||||||
|
return Environment.ExpandEnvironmentVariables(path);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
67
System/Threading/Tasks/AsyncWaitHandle.cs
Normal file
67
System/Threading/Tasks/AsyncWaitHandle.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
namespace System.Threading.Tasks;
|
||||||
|
|
||||||
|
public static class AsyncWaitHandle
|
||||||
|
{
|
||||||
|
public static Task WaitOneAsync(this WaitHandle waitHandle, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
WaitOneAsyncState state = new(waitHandle, cancellationToken);
|
||||||
|
return state.WaitOneAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class WaitOneAsyncState : IDisposable
|
||||||
|
{
|
||||||
|
private readonly CancellationTokenRegistration _cancellationTokenRegistration;
|
||||||
|
private readonly RegisteredWaitHandle _registeredWaitHandle;
|
||||||
|
private readonly TaskCompletionSource _tcs;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public WaitOneAsyncState(WaitHandle waitHandle, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_tcs = new();
|
||||||
|
_cancellationTokenRegistration = cancellationToken.Register((state, token) => ((WaitOneAsyncState)state!).Canceled(token), this);
|
||||||
|
_registeredWaitHandle = ThreadPool.RegisterWaitForSingleObject(waitHandle, (state, timeout) => ((WaitOneAsyncState)state!).Signaled(timeout), this, Timeout.Infinite, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_registeredWaitHandle.Unregister(default);
|
||||||
|
_cancellationTokenRegistration.Dispose();
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task WaitOneAsync()
|
||||||
|
{
|
||||||
|
const TaskContinuationOptions options = TaskContinuationOptions.AttachedToParent | TaskContinuationOptions.ExecuteSynchronously;
|
||||||
|
return _tcs.Task.ContinueWith((upstream, state) => ((WaitOneAsyncState)state!).Continuation(upstream), this, options).Unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Canceled(CancellationToken token)
|
||||||
|
{
|
||||||
|
_tcs.SetCanceled(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task Continuation(Task task)
|
||||||
|
{
|
||||||
|
Dispose();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Signaled(bool timeout)
|
||||||
|
{
|
||||||
|
if (timeout)
|
||||||
|
{
|
||||||
|
Canceled(CancellationToken.None);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_tcs.SetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,14 +6,25 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<UserSecretsId>aspnet-netddi-40b2ab4f-2391-4151-8f6d-f213efd89115</UserSecretsId>
|
<UserSecretsId>aspnet-netddi-40b2ab4f-2391-4151-8f6d-f213efd89115</UserSecretsId>
|
||||||
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>True</NoDefaultLaunchSettingsFile>
|
||||||
|
<RootNamespace>DotNetDDI</RootNamespace>
|
||||||
|
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.HighPerformance" Version="8.4.0" />
|
||||||
|
<PackageReference Include="DotNext" Version="5.18.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
|
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="8.0.12" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.12" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.12" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.1">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Systemd" Version="9.0.1" />
|
||||||
|
<PackageReference Include="Sep" Version="0.8.0" />
|
||||||
|
<PackageReference Include="System.IO.Pipelines" Version="9.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue