1
0
Fork 0

Migrate existing code

This commit is contained in:
Jöran Malek 2025-01-29 23:53:57 +01:00
parent 384ff4a6f3
commit 9b49f880a2
30 changed files with 1789 additions and 5 deletions

View 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);
}

View 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);
}

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

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

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

View 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);
}
}

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

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

View 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]);
}
}
}

View 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);
}
}

View file

@ -0,0 +1,9 @@
namespace DotNetDDI.Integrations.Kea;
public enum LeaseType : byte
{
NonTempraryIPv6 = 0,
TemporaryIPv6 = 1,
IPv6Prefix = 2,
IPv4 = 3
}