diff --git a/misc/Ae.Dns.Console/Program.cs b/misc/Ae.Dns.Console/Program.cs index 710ff96..b3c5e29 100644 --- a/misc/Ae.Dns.Console/Program.cs +++ b/misc/Ae.Dns.Console/Program.cs @@ -210,10 +210,14 @@ async Task ReportStats(CancellationToken token) dnsClient = new DnsAppMetricsClient(metrics, dnsClient); // Create a "raw" client which deals with buffers directly - var rawClient = ActivatorUtilities.CreateInstance(provider, dnsClient); + IDnsRawClient rawClient = ActivatorUtilities.CreateInstance(provider, dnsClient); - IDnsServer tcpServer = ActivatorUtilities.CreateInstance(provider, rawClient); - IDnsServer udpServer = ActivatorUtilities.CreateInstance(provider, rawClient); + // Create a dormant capture client for debugging purposes + DnsCaptureRawClient captureRawClient = ActivatorUtilities.CreateInstance(provider, rawClient); + + // Create two servers, TCP and UDP to serve answers + IDnsServer tcpServer = ActivatorUtilities.CreateInstance(provider, captureRawClient); + IDnsServer udpServer = ActivatorUtilities.CreateInstance(provider, captureRawClient); // Add a very basic stats panel var builder = Host.CreateDefaultBuilder() @@ -230,6 +234,7 @@ async Task ReportStats(CancellationToken token) }) .ConfigureServices(x => { + x.AddSingleton(captureRawClient); x.AddSingleton(mainCache); x.AddSingleton(new DnsMiddlewareConfig()); x.AddSingleton(dnsClient); diff --git a/misc/Ae.Dns.Console/Startup.cs b/misc/Ae.Dns.Console/Startup.cs index 686a654..999d85e 100644 --- a/misc/Ae.Dns.Console/Startup.cs +++ b/misc/Ae.Dns.Console/Startup.cs @@ -150,6 +150,55 @@ async Task GroupToTable(IEnumerable> groups, params await context.Response.WriteAsync(""); } + if (context.Request.Path.StartsWithSegments("/captures")) + { + var captureClient = context.RequestServices.GetRequiredService(); + + if (context.Request.Path.StartsWithSegments("/captures/on")) + { + captureClient.IsEnabled = true; + context.Response.Redirect("/captures"); + return; + } + + if (context.Request.Path.StartsWithSegments("/captures/off")) + { + captureClient.IsEnabled = false; + context.Response.Redirect("/captures"); + return; + } + + if (context.Request.Path.StartsWithSegments("/captures/clear")) + { + captureClient.Captures.Clear(); + context.Response.Redirect("/captures"); + return; + } + + context.Response.StatusCode = StatusCodes.Status200OK; + context.Response.ContentType = "text/html; charset=utf-8"; + + await context.Response.WriteAsync($"

Captures

"); + await context.Response.WriteAsync($"

Captures are {(captureClient.IsEnabled ? "on" : "off")}, captured = {captureClient.Captures.Count()}

"); + await context.Response.WriteAsync($"

Controls: {(captureClient.IsEnabled ? "Stop" : "Start")} Capturing Clear Captures

"); + + await context.Response.WriteAsync($"
    "); + + foreach (var capture in captureClient.Captures) + { + await context.Response.WriteAsync($"
  • "); + await context.Response.WriteAsync($"Served by {capture.Request.ServerName} from {capture.Request.SourceEndpoint}"); + await context.Response.WriteAsync($"
    {capture.Response.Query}
    "); + await context.Response.WriteAsync($"
    Query bytes: {DnsByteExtensions.ToDebugString(capture.Query)}
    "); + await context.Response.WriteAsync($"
    {capture.Response.Answer}
    "); + await context.Response.WriteAsync($"
    Answer bytes: {DnsByteExtensions.ToDebugString(capture.Answer)}
    "); + await context.Response.WriteAsync($"
  • "); + + } + + await context.Response.WriteAsync($"
"); + } + if (context.Request.Path == "/") { var dnsClient = context.RequestServices.GetRequiredService(); diff --git a/src/Ae.Dns.Client/DnsCaptureRawClient.cs b/src/Ae.Dns.Client/DnsCaptureRawClient.cs new file mode 100644 index 0000000..eed9c5c --- /dev/null +++ b/src/Ae.Dns.Client/DnsCaptureRawClient.cs @@ -0,0 +1,99 @@ +using Ae.Dns.Protocol; +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; + +namespace Ae.Dns.Client +{ + /// + /// Defines a client which can be set to capture DNS packets. + /// + public sealed class DnsCaptureRawClient : IDnsRawClient + { + private readonly IDnsRawClient _innerClient; + + /// + /// The raw bytes for the capture. + /// + public sealed class Capture + { + /// + /// The incoming request. + /// + public DnsRawClientRequest Request { get; set; } + /// + /// The raw query. + /// + public ReadOnlyMemory Query { get; set; } + /// + /// The raw answer. + /// + public ReadOnlyMemory Answer { get; set; } + /// + /// The outgoing response. + /// + public DnsRawClientResponse Response { get; set; } + } + + /// + /// Whether packet capture is enabled. + /// + public bool IsEnabled { get; set; } + + /// + /// An optional filter for the packet capture. + /// + public Func CaptureFilter { get; set; } = (request, response) => true; + + /// + /// The raw captures. + /// + public ConcurrentBag Captures { get; } = new ConcurrentBag(); + + /// + /// Construct a new (inactive) packet capturing client, delegating to the specified . + /// + /// + public DnsCaptureRawClient(IDnsRawClient innerClient) + { + _innerClient = innerClient; + } + + /// + public void Dispose() => _innerClient.Dispose(); + + /// + public async Task Query(Memory buffer, DnsRawClientRequest request, CancellationToken token = default) + { + if (!IsEnabled) + { + return await _innerClient.Query(buffer, request, token); + } + + // Unfortunately we must copy the query before + // we know whether we need it, otherwise it will + // overwritten in the buffer by the answer. + var queryBuffer = new byte[request.QueryLength]; + buffer.Slice(0, request.QueryLength).CopyTo(queryBuffer); + + var response = await _innerClient.Query(buffer, request, token); + + if (CaptureFilter(request, response)) + { + var answerBuffer = new byte[response.AnswerLength]; + buffer.Slice(0, response.AnswerLength).CopyTo(answerBuffer); + + Captures.Add(new Capture + { + Request = request, + Query = queryBuffer, + Answer = answerBuffer, + Response = response + }); + } + + return response; + } + } +} diff --git a/src/Ae.Dns.Protocol/Properties/AssemblyInfo.cs b/src/Ae.Dns.Protocol/Properties/AssemblyInfo.cs index 6ca5580..1b45237 100644 --- a/src/Ae.Dns.Protocol/Properties/AssemblyInfo.cs +++ b/src/Ae.Dns.Protocol/Properties/AssemblyInfo.cs @@ -5,4 +5,5 @@ [assembly: InternalsVisibleTo("Ae.Dns.Server.Http")] [assembly: InternalsVisibleTo("Ae.Dns.Tests")] [assembly: InternalsVisibleTo("Ae.Dns.Benchmarks")] -[assembly: InternalsVisibleTo("Ae.Dns.Crawler")] \ No newline at end of file +[assembly: InternalsVisibleTo("Ae.Dns.Crawler")] +[assembly: InternalsVisibleTo("Ae.Dns.Console")] \ No newline at end of file