diff --git a/README.md b/README.md deleted file mode 100644 index eb95879..0000000 --- a/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# pdns-dhcp - -Enabling PowerDNS to query popular supported Dhcp-servers. - -This project was born out of the necessity for my home-lab network to be able to resolve both IPv4 and IPv6 addresses from one Dhcp-service. - -Theoretically Kea can update DNS servers using RFC2136 nsupdate-mechanisms using kea-ddns, but this interoperation can cause issues in networks with devices sharing a hostname (i.e. DHCID records), missing update requests due to service restarts or temporary connectivity issues. - -## Scope - -At the moment there is no need to implement more than is minimally required to get Dhcp4 and Dhcp6 leases queryable by PowerDNS using the memfile "database" of Kea using the remote backend with unix domain sockets. - -Following parts may be implemented later as I see fit: -- Different PowerDNS remote backends - - mainly HTTP REST -- Support different Kea lease databases - - MySQL - - PostgreSQL - -## Building - -Requires .NET 8 SDK -Create binary using -> dotnet publish -c Release -p:PublishTrimmed=true -p:PublishSingleFile=true --self-contained - -## Usage - -Install, and configure Kea (optionally with Stork) Dhcp4, Dhcp6 or both. -Make sure to enable the memfile lease store. - -Install and configure PowerDNS, including the [remote backend](https://doc.powerdns.com/authoritative/backends/remote.html). -A sample configuration file is provided. - -Deploy pdns-dhcp to /opt/pdns-dhcp -Setup systemd using the provided socket and service units, configure as necessary. - -Start Kea, pdns-dhcp and PowerDNS. - -**To be done**: Packaging for common Linux distributions. -Deb-packages (Debian) -RPM-packages (EL) - -### Configuration - -pdns-dhcp can be configured using environment variables or the appsettings.json file - [Configuration#Binding hierarchies](https://learn.microsoft.com/dotnet/core/extensions/configuration#binding-hierarchies) describes the naming scheme in this section. - -Default configuration: -``` -Dhcp:Kea:Dhcp4:Leases=/var/lib/kea/kea-leases4.csv -Dhcp:Kea:Dhcp6:Leases=/var/lib/kea/kea-leases6.csv -PowerDns:UniqueHostnames=true -PowerDns:Listener:Socket=/run/pdns-dhcp/pdns.sock -``` - -`Dhcp:Kea` allows configuring `Dhcp4` and `Dhcp6` lease file watchers, respective for each of both services. - -In `PowerDns:Listener:Socket` you can optionally configure the unix domain socket to be used in case Systemd isn't providing them (e.g. when starting the service manually). - -pdns-dhcp continuously monitors the Dhcp service leases and upon seeing a new lease all previous records that match in hostname and lease type (IPv4, IPv6) are replaced. If you want to change this behavior you can opt-out of this behavior by setting `PowerDns:UniqueHostnames=false`. - -See [Logging in C#](https://learn.microsoft.com/dotnet/core/extensions/logging?tabs=command-line#configure-logging-without-code) for options related to logging. - -## Acknowledgments - -Incorporates following libraries directly: - -**.NET Foundation and Contributors** -- [CommunityToolkit.HighPerformance](https://github.com/CommunityToolkit/dotnet) - MIT -- [dotNext](https://github.com/dotnet/dotNext) - MIT -- Several runtime libraries, as part of [.NET](https://github.com/dotnet/runtime) - - Microsoft.AspNetCore.App - - Microsoft.Extensions.Configuration.Binder - - Microsoft.Extensions.Hosting.Systemd - - System.IO.Pipelines - -**[Nietras](https://github.com/nietras)** -- [Sep](https://github.com/nietras/Sep) - MIT - -Incorporates data structures and protocol implementations as required for interop scenarios: - -- [kea](https://gitlab.isc.org/isc-projects/kea) by [ISC](https://isc.org/) - MPL 2.0 -- [PowerDNS](https://github.com/PowerDNS/pdns) by [PowerDNS.COM BV](https://www.powerdns.com/) and contributors - GPL 2.0 diff --git a/ext/kea/kea-leases4.csv b/ext/kea/dhcp4.leases similarity index 100% rename from ext/kea/kea-leases4.csv rename to ext/kea/dhcp4.leases diff --git a/ext/kea/kea-leases6.csv b/ext/kea/dhcp6.leases similarity index 100% rename from ext/kea/kea-leases6.csv rename to ext/kea/dhcp6.leases diff --git a/ext/powerdns/pdns-dhcp.conf b/ext/powerdns/pdns-dhcp.conf deleted file mode 100644 index 6cf4ed2..0000000 --- a/ext/powerdns/pdns-dhcp.conf +++ /dev/null @@ -1,2 +0,0 @@ -launch+=remote -remote-connection-string+=unix:path=/run/pdns-dhcp/pdns.sock diff --git a/ext/systemd/pdns-dhcp.socket b/ext/systemd/pdns-dhcp.socket index f13126c..b514f4b 100644 --- a/ext/systemd/pdns-dhcp.socket +++ b/ext/systemd/pdns-dhcp.socket @@ -1,8 +1,11 @@ +# WARNING +# This currently not supported. + [Unit] Description=pdns-dhcp PowerDNS Remote Socket [Socket] -ListenStream=/run/pdns-dhcp/pdns.sock +ListenStream=/run/pdns-dhcp/pdns-dhcp.sock Accept=no [Install] diff --git a/src/pdns-dhcp/Kea/KeaDhcpLease.cs b/src/pdns-dhcp/Kea/KeaDhcpLease.cs index ae1f965..4254440 100644 --- a/src/pdns-dhcp/Kea/KeaDhcpLease.cs +++ b/src/pdns-dhcp/Kea/KeaDhcpLease.cs @@ -43,7 +43,7 @@ public static class KeaDhcpLease if (converted) { - writer.Write([escapedChar]); + writer.Write(escapedChar); } else { diff --git a/src/pdns-dhcp/Kea/KeaDhcpLeaseWatcher.cs b/src/pdns-dhcp/Kea/KeaDhcpLeaseWatcher.cs index 13ff3b0..c3487fd 100644 --- a/src/pdns-dhcp/Kea/KeaDhcpLeaseWatcher.cs +++ b/src/pdns-dhcp/Kea/KeaDhcpLeaseWatcher.cs @@ -170,15 +170,11 @@ public sealed class KeaDhcpLeaseWatcher : IHostedService { 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); + TaskCreationOptions.AttachedToParent, TaskScheduler.Default); continue; } @@ -206,7 +202,7 @@ public sealed class KeaDhcpLeaseWatcher : IHostedService } else { - await waitHandle.WaitOneAsync(stoppingToken).ConfigureAwait(false); + await waitHandle.WaitOneAsync(stoppingToken).ConfigureAwait(continueOnCapturedContext: false); } } } diff --git a/src/pdns-dhcp/Kea/KeaService.cs b/src/pdns-dhcp/Kea/KeaService.cs index c166227..863be32 100644 --- a/src/pdns-dhcp/Kea/KeaService.cs +++ b/src/pdns-dhcp/Kea/KeaService.cs @@ -49,6 +49,6 @@ public class KeaService : IHostedService 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); + await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(continueOnCapturedContext: false); } } diff --git a/src/pdns-dhcp/PowerDns/Methods.cs b/src/pdns-dhcp/PowerDns/Methods.cs index 1a6a64a..bc823e6 100644 --- a/src/pdns-dhcp/PowerDns/Methods.cs +++ b/src/pdns-dhcp/PowerDns/Methods.cs @@ -1,12 +1,21 @@ +using System.Text.Json.Serialization; + namespace pdns_dhcp.PowerDns; public interface IMethod; -public record MethodBase(string Method); +[JsonPolymorphic(TypeDiscriminatorPropertyName = "method")] +[JsonDerivedType(typeof(InitializeMethod), "initialize")] +[JsonDerivedType(typeof(LookupMethod), "lookup")] +public class Method; -public record Method(string Method, TParam Parameters) : MethodBase(Method) - where TParam : MethodParameters; +public abstract class Method(TParam parameters) : Method + where TParam : MethodParameters +{ + [JsonPropertyName("parameters")] + public TParam Parameters => parameters; +} -public record InitializeMethod(InitializeParameters Parameters) : Method("initialize", Parameters), IMethod; +public class InitializeMethod(InitializeParameters parameters) : Method(parameters), IMethod; -public record LookupMethod(LookupParameters Parameters) : Method("lookup", Parameters), IMethod; +public class LookupMethod(LookupParameters parameters) : Method(parameters), IMethod; diff --git a/src/pdns-dhcp/PowerDns/Parameters.cs b/src/pdns-dhcp/PowerDns/Parameters.cs index 4d11a5d..93238c1 100644 --- a/src/pdns-dhcp/PowerDns/Parameters.cs +++ b/src/pdns-dhcp/PowerDns/Parameters.cs @@ -1,43 +1,16 @@ -using System.Text; using System.Text.Json; using System.Text.Json.Serialization; namespace pdns_dhcp.PowerDns; -public record class Parameters; - [JsonDerivedType(typeof(InitializeParameters))] [JsonDerivedType(typeof(LookupParameters))] +public record class Parameters; + public record class MethodParameters : Parameters { [JsonExtensionData] public Dictionary 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( diff --git a/src/pdns-dhcp/PowerDns/PowerDnsHandler.cs b/src/pdns-dhcp/PowerDns/PowerDnsHandler.cs index fa8b598..e2ee4c6 100644 --- a/src/pdns-dhcp/PowerDns/PowerDnsHandler.cs +++ b/src/pdns-dhcp/PowerDns/PowerDnsHandler.cs @@ -1,5 +1,4 @@ using System.Buffers; -using System.Collections.ObjectModel; using System.IO.Pipelines; using System.Net.Sockets; using System.Text.Json; @@ -16,34 +15,9 @@ namespace pdns_dhcp.PowerDns; public class PowerDnsHandler : ConnectionHandler { - delegate MethodBase? HandlerConverter(in JsonElement element); - - private static readonly ReadOnlyDictionary Converters; - private readonly ILogger _logger; private readonly DnsRepository _repository; - static PowerDnsHandler() - { - Dictionary converters = new(StringComparer.OrdinalIgnoreCase) - { - ["initialize"] = ToInitialize, - ["lookup"] = 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 logger) { _logger = logger; @@ -67,48 +41,21 @@ public class PowerDnsHandler : ConnectionHandler foreach (var memory in read.Buffer) { buffer.Write(memory.Span); - if (!ConsumeJson(buffer, json, ref state)) + if (ConsumeJson(buffer, json, ref state)) { - continue; - } + var method = JsonSerializer.Deserialize(json.WrittenSpan, PowerDnsSerializerContext.Default.Method)!; + json.Clear(); + state = default; - Reply reply = BoolReply.False; - try - { - MethodBase method; + Reply reply = BoolReply.False; 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) { } - 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); + .ConfigureAwait(continueOnCapturedContext: false); } } @@ -164,20 +111,9 @@ public class PowerDnsHandler : ConnectionHandler 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(methodName, parameters.Deserialize(PowerDnsSerializerContext.Default.MethodParameters)!), - _ => converter(parameters) - }; - } } - private ValueTask Handle(MethodBase method, CancellationToken cancellationToken = default) + private ValueTask Handle(Method method, CancellationToken cancellationToken = default) { return method switch { @@ -187,20 +123,15 @@ public class PowerDnsHandler : ConnectionHandler _ => LogUnhandled(_logger, method) }; - static ValueTask LogUnhandled(ILogger logger, MethodBase method) + static ValueTask LogUnhandled(ILogger logger, Method method) { - logger.LogWarning("Unhandled {Method}", method); + logger.LogWarning("Unhandled Method {Method}", method); return ValueTask.FromResult(BoolReply.False); } } private ValueTask HandleInitialize(InitializeParameters parameters) { - if (_logger.IsEnabled(LogLevel.Information)) - { - _logger.LogInformation("Handling {parameters}", parameters); - } - return ValueTask.FromResult(BoolReply.True); } @@ -220,11 +151,6 @@ public class PowerDnsHandler : ConnectionHandler return ValueTask.FromResult(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 FindByName((AddressFamily Family, ReadOnlyMemory Qname) query, DnsRepository repository, ILogger logger) diff --git a/src/pdns-dhcp/PowerDns/PowerDnsSerializerContext.cs b/src/pdns-dhcp/PowerDns/PowerDnsSerializerContext.cs index 942bc8c..20fddce 100644 --- a/src/pdns-dhcp/PowerDns/PowerDnsSerializerContext.cs +++ b/src/pdns-dhcp/PowerDns/PowerDnsSerializerContext.cs @@ -3,7 +3,8 @@ using System.Text.Json.Serialization; namespace pdns_dhcp.PowerDns; [JsonSerializable(typeof(Reply))] -[JsonSerializable(typeof(MethodParameters))] +[JsonSerializable(typeof(Method))] +[JsonSerializable(typeof(Parameters))] [JsonSourceGenerationOptions( DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, NumberHandling = JsonNumberHandling.AllowReadingFromString, diff --git a/src/pdns-dhcp/Program.cs b/src/pdns-dhcp/Program.cs index c7b4a15..71dfeac 100644 --- a/src/pdns-dhcp/Program.cs +++ b/src/pdns-dhcp/Program.cs @@ -60,23 +60,26 @@ builder.Services.Configure(options => builder.WebHost.ConfigureKestrel((context, options) => { - bool isSystemd = false; - options.UseSystemd(options => - { - isSystemd = true; - options.UseConnectionHandler(); - }); - - if (!isSystemd && context.Configuration.GetRequiredSection("PowerDns:Listener").Get() is { } pdnsOptions) + if (context.Configuration.GetRequiredSection("PowerDns:Listener").Get() is { } pdnsOptions) { var path = PathEx.ExpandPath(pdnsOptions.Socket); FileInfo file = new(path); file.Directory!.Create(); - file.Delete(); - options.ListenUnixSocket(path, options => + bool isSystemd = false; + options.UseSystemd(options => { + isSystemd = true; options.UseConnectionHandler(); }); + + if (!isSystemd) + { + file.Delete(); + options.ListenUnixSocket(path, options => + { + options.UseConnectionHandler(); + }); + } } }); diff --git a/src/pdns-dhcp/Services/DhcpWatcher.cs b/src/pdns-dhcp/Services/DhcpWatcher.cs index da6c4c1..0ce62ed 100644 --- a/src/pdns-dhcp/Services/DhcpWatcher.cs +++ b/src/pdns-dhcp/Services/DhcpWatcher.cs @@ -45,6 +45,6 @@ public class DhcpWatcher : IHostedService 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); + await Task.WhenAny(waitTask, taskCompletionSource.Task).ConfigureAwait(continueOnCapturedContext: false); } } diff --git a/src/pdns-dhcp/appsettings.Development.json b/src/pdns-dhcp/appsettings.Development.json index a2fbe6c..dd2f5bf 100644 --- a/src/pdns-dhcp/appsettings.Development.json +++ b/src/pdns-dhcp/appsettings.Development.json @@ -2,10 +2,10 @@ "Dhcp": { "Kea": { "Dhcp4": { - "Leases": "../../ext/kea/kea-leases4.csv" + "Leases": "../../ext/kea/dhcp4.leases" }, "Dhcp6": { - "Leases": "../../ext/kea/kea-leases6.csv" + "Leases": "../../ext/kea/dhcp6.leases" } } }, diff --git a/src/pdns-dhcp/appsettings.json b/src/pdns-dhcp/appsettings.json index bda961c..e345227 100644 --- a/src/pdns-dhcp/appsettings.json +++ b/src/pdns-dhcp/appsettings.json @@ -2,10 +2,10 @@ "Dhcp": { "Kea": { "Dhcp4": { - "Leases": "/var/lib/kea/kea-leases4.csv" + "Leases": "/var/lib/kea/dhcp4.leases" }, "Dhcp6": { - "Leases": "/var/lib/kea/kea-leases6.csv" + "Leases": "/var/lib/kea/dhcp6.leases" } } }, diff --git a/src/pdns-dhcp/pdns-dhcp.csproj b/src/pdns-dhcp/pdns-dhcp.csproj index 96c0157..965bf09 100644 --- a/src/pdns-dhcp/pdns-dhcp.csproj +++ b/src/pdns-dhcp/pdns-dhcp.csproj @@ -15,8 +15,9 @@ - + +