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 Lifetimes => [600, 3600]; private readonly PowerDnsOptions _options; private readonly ReaderWriterLockSlim _recordLock = new(); private readonly List _records = []; private readonly SemaphoreSlim _syncLock = new(1, 1); public DnsRepository(IOptions options) { _options = options.Value; } public List Find(Predicate query) { bool enteredLock = false; try { enteredLock = _recordLock.TryEnterReadLock(Timeout.Infinite); return _records.FindAll(query); } finally { if (enteredLock) { _recordLock.ExitReadLock(); } } } public Task> FindAsync(Predicate query, CancellationToken cancellationToken = default) { return Task.Factory.StartNew(state => Find((Predicate)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 Matches(DnsRecord query) { LinkedList 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.Default.Equals(recordHWAddr, queryHWAddr): list.AddLast(i); continue; } if (EqualityComparer.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; }