diff --git a/Apps/LogExporterApp/App.cs b/Apps/LogExporterApp/App.cs
new file mode 100644
index 00000000..b0956388
--- /dev/null
+++ b/Apps/LogExporterApp/App.cs
@@ -0,0 +1,216 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using DnsServerCore.ApplicationCommon;
+using LogExporter.Strategy;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using TechnitiumLibrary;
+using TechnitiumLibrary.Net.Dns;
+using TechnitiumLibrary.Net.Dns.ResourceRecords;
+
+namespace LogExporter
+{
+ public sealed class App : IDnsApplication, IDnsQueryLogger
+ {
+ #region variables
+
+ private const int BULK_INSERT_COUNT = 1000;
+
+ private const int DEFAULT_QUEUE_CAPACITY = 1000;
+
+ private const int QUEUE_TIMER_INTERVAL = 10000;
+
+ private readonly ExportManager _exportManager = new ExportManager();
+
+ private BlockingCollection _logBuffer;
+
+ private readonly object _queueTimerLock = new object();
+
+ private BufferManagementConfig _config;
+
+ private IDnsServer _dnsServer;
+
+ private Timer _queueTimer;
+
+ private bool disposedValue;
+
+ private readonly IReadOnlyList _emptyList = [];
+
+ #endregion variables
+
+ #region constructor
+
+ public App()
+ {
+ }
+
+ #endregion constructor
+
+ #region IDisposable
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ lock (_queueTimerLock)
+ {
+ _queueTimer?.Dispose();
+ }
+
+ ExportLogsAsync().Sync(); //flush any pending logs
+
+ _logBuffer.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public async Task InitializeAsync(IDnsServer dnsServer, string config)
+ {
+ _dnsServer = dnsServer;
+ _config = BufferManagementConfig.Deserialize(config);
+ if(_config == null)
+ {
+ throw new DnsClientException("Invalid application configuration.");
+ }
+
+ if (_config.MaxLogEntries != null)
+ {
+ _logBuffer = new BlockingCollection(_config.MaxLogEntries.Value);
+ }
+ else
+ {
+ _logBuffer = new BlockingCollection(DEFAULT_QUEUE_CAPACITY);
+ }
+
+ RegisterExportTargets();
+
+ lock (_queueTimerLock)
+ {
+ _queueTimer = new Timer(async (object _) =>
+ {
+ try
+ {
+ await ExportLogsAsync();
+ }
+ catch (Exception ex)
+ {
+ _dnsServer.WriteLog(ex);
+ }
+ }, null, QUEUE_TIMER_INTERVAL, Timeout.Infinite);
+ }
+
+ await Task.CompletedTask;
+ }
+
+ public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
+ {
+ _logBuffer.Add(new LogEntry(timestamp, remoteEP, protocol, request, response));
+
+ return Task.CompletedTask;
+ }
+
+ public async Task QueryLogsAsync(long pageNumber, int entriesPerPage, bool descendingOrder, DateTime? start, DateTime? end, IPAddress clientIpAddress, DnsTransportProtocol? protocol, DnsServerResponseType? responseType, DnsResponseCode? rcode, string qname, DnsResourceRecordType? qtype, DnsClass? qclass)
+ {
+ return await Task.FromResult(new DnsLogPage(0, 0, 0, _emptyList));
+ }
+
+ #endregion public
+
+ #region private
+
+ private async Task ExportLogsAsync(CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var logs = new List(BULK_INSERT_COUNT);
+
+ while (true)
+ {
+ while ((logs.Count < BULK_INSERT_COUNT) && _logBuffer.TryTake(out LogEntry? log))
+ {
+ if (log != null)
+ logs.Add(log);
+ }
+
+ if (logs.Count > 0)
+ {
+ await _exportManager.ImplementStrategyForAsync(logs, cancellationToken);
+
+ logs.Clear();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _dnsServer?.WriteLog(ex);
+ }
+ }
+
+ private void RegisterExportTargets()
+ {
+ foreach (var target in _config.Targets)
+ {
+ if (target.Enabled)
+ {
+ switch (target.Type.ToLower())
+ {
+ case "file":
+ _exportManager.RegisterStrategy("file", new FileExportStrategy(target.Path));
+ break;
+
+ case "http":
+ _exportManager.RegisterStrategy("http", new HttpExportStrategy(target.Endpoint, target.Method, target.Headers));
+ break;
+
+ case "syslog":
+ _exportManager.RegisterStrategy("syslog", new SyslogExportStrategy(target.Address, target.Port, target.Protocol));
+ break;
+ }
+ }
+ }
+ }
+
+ #endregion private
+
+ #region properties
+
+ public string Description
+ {
+ get { return "The app allows exporting logs to a third party sink using an internal buffer."; }
+ }
+
+ #endregion properties
+ }
+}
\ No newline at end of file
diff --git a/Apps/LogExporterApp/BufferManagementConfig.cs b/Apps/LogExporterApp/BufferManagementConfig.cs
new file mode 100644
index 00000000..396f3c5d
--- /dev/null
+++ b/Apps/LogExporterApp/BufferManagementConfig.cs
@@ -0,0 +1,70 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace LogExporter
+{
+ public class BufferManagementConfig
+ {
+ [JsonPropertyName("maxLogEntries")]
+ public int? MaxLogEntries { get; set; }
+
+ [JsonPropertyName("targets")]
+ public List Targets { get; set; }
+
+ // Load configuration from JSON
+ public static BufferManagementConfig? Deserialize(string json)
+ {
+ return JsonSerializer.Deserialize(json);
+ }
+ }
+
+ public class Target
+ {
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ [JsonPropertyName("enabled")]
+ public bool Enabled { get; set; }
+
+ [JsonPropertyName("path")]
+ public string Path { get; set; }
+
+ [JsonPropertyName("endpoint")]
+ public string Endpoint { get; set; }
+
+ [JsonPropertyName("method")]
+ public string Method { get; set; }
+
+ [JsonPropertyName("headers")]
+ public Dictionary Headers { get; set; }
+
+ [JsonPropertyName("address")]
+ public string Address { get; set; }
+
+ [JsonPropertyName("port")]
+ public int? Port { get; set; }
+
+ [JsonPropertyName("protocol")]
+ public string Protocol { get; set; }
+ }
+}
diff --git a/Apps/LogExporterApp/LogEntry.cs b/Apps/LogExporterApp/LogEntry.cs
new file mode 100644
index 00000000..1cc081e1
--- /dev/null
+++ b/Apps/LogExporterApp/LogEntry.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Text.Json.Serialization;
+using System.Text.Json;
+using TechnitiumLibrary.Net.Dns;
+using TechnitiumLibrary.Net.Dns.ResourceRecords;
+using System.Collections.Generic;
+
+namespace LogExporter
+{
+ public class LogEntry
+ {
+ public DateTime Timestamp { get; set; }
+ public string ClientIp { get; set; }
+ public int ClientPort { get; set; }
+ public bool DnssecOk { get; set; }
+ public DnsTransportProtocol Protocol { get; set; }
+ public DnsResponseCode ResponseCode { get; set; }
+ public List Questions { get; set; }
+ public List Answers { get; set; }
+ public object? RequestTag { get; set; }
+ public object? ResponseTag { get; set; }
+
+ public LogEntry(DateTime timestamp, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram request, DnsDatagram response)
+ {
+ // Assign timestamp and ensure it's in UTC
+ Timestamp = timestamp.Kind == DateTimeKind.Utc ? timestamp : timestamp.ToUniversalTime();
+
+ // Extract client information
+ ClientIp = remoteEP.Address.ToString();
+ ClientPort = remoteEP.Port;
+ DnssecOk = request.DnssecOk;
+ Protocol = protocol;
+ ResponseCode = response.RCODE;
+
+ // Extract request information
+ Questions = new List(request.Question.Count);
+ if (request.Question?.Count > 0)
+ {
+ Questions.AddRange(request.Question.Select(questionRecord => new Question
+ {
+ QuestionName = questionRecord.Name,
+ QuestionType = questionRecord.Type,
+ QuestionClass = questionRecord.Class,
+ Size = questionRecord.UncompressedLength
+ }));
+ }
+
+ // Convert answer section into a simple string summary (comma-separated for multiple answers)
+ Answers = new List(response.Answer.Count);
+ if (response.Answer?.Count > 0)
+ {
+ Answers.AddRange(response.Answer.Select(record => new Answer
+ {
+ RecordType = record.Type,
+ RecordData = record.RDATA.ToString(),
+ RecordClass = record.Class,
+ RecordTtl = record.TTL,
+ Size = record.UncompressedLength,
+ DnssecStatus = record.DnssecStatus
+ }));
+ }
+
+ if (request.Tag != null)
+ {
+ RequestTag = request.Tag;
+ }
+
+ if (response.Tag != null)
+ {
+ ResponseTag = response.Tag;
+ }
+ }
+
+ public override string ToString()
+ {
+ return JsonSerializer.Serialize(this, DnsLogSerializerOptions.Default);
+ }
+
+ public class Question
+ {
+ public string QuestionName { get; set; }
+ public DnsResourceRecordType? QuestionType { get; set; }
+ public DnsClass? QuestionClass { get; set; }
+ public int Size { get; set; }
+ }
+
+ public class Answer
+ {
+ public DnsResourceRecordType RecordType { get; set; }
+ public string RecordData { get; set; }
+ public DnsClass RecordClass { get; set; }
+ public uint RecordTtl { get; set; }
+ public int Size { get; set; }
+ public DnssecStatus DnssecStatus { get; set; }
+ }
+ }
+
+ // Custom DateTime converter to handle UTC serialization in ISO 8601 format
+ public class JsonDateTimeConverter : JsonConverter
+ {
+ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ var dts = reader.GetString();
+ return dts == null ? DateTime.MinValue : DateTime.Parse(dts);
+ }
+
+ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
+ {
+ writer.WriteStringValue(value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
+ }
+ }
+
+ // Setup reusable options with a single instance
+ public static class DnsLogSerializerOptions
+ {
+ public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
+ {
+ WriteIndented = false, // Newline delimited logs should not be multiline
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase
+ Converters = { new JsonStringEnumConverter(), new JsonDateTimeConverter() }, // Handle enums and DateTime conversion
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding
+ NumberHandling = JsonNumberHandling.Strict,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values
+ };
+ }
+}
diff --git a/Apps/LogExporterApp/LogExporterApp.csproj b/Apps/LogExporterApp/LogExporterApp.csproj
new file mode 100644
index 00000000..6c6107d2
--- /dev/null
+++ b/Apps/LogExporterApp/LogExporterApp.csproj
@@ -0,0 +1,49 @@
+
+
+
+ net8.0
+ false
+ true
+ 1.0
+ false
+ Technitium
+ Technitium DNS Server
+ Zafer Balkan
+ LogExporterApp
+ LogExporter
+ https://technitium.com/dns/
+ https://github.com/TechnitiumSoftware/DnsServer
+ The app allows exporting logs to a third party sink using an internal buffer.
+ false
+ Library
+ enable
+
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+ ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll
+ false
+
+
+ ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll
+ false
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Apps/LogExporterApp/Strategy/ExportManager.cs b/Apps/LogExporterApp/Strategy/ExportManager.cs
new file mode 100644
index 00000000..abed40bc
--- /dev/null
+++ b/Apps/LogExporterApp/Strategy/ExportManager.cs
@@ -0,0 +1,53 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LogExporter.Strategy
+{
+ public class ExportManager
+ {
+ private readonly Dictionary _exportStrategies;
+
+ public ExportManager()
+ {
+ _exportStrategies = new Dictionary();
+ }
+
+ public void RegisterStrategy(string key, IExportStrategy strategy)
+ {
+ _exportStrategies[key.ToLower()] = strategy;
+ }
+
+ public IExportStrategy? GetStrategy(string key)
+ {
+ return _exportStrategies.ContainsKey(key.ToLower()) ? _exportStrategies[key.ToLower()] : null;
+ }
+
+ public async Task ImplementStrategyForAsync(List logs, CancellationToken cancellationToken = default)
+ {
+ foreach (var strategy in _exportStrategies.Values)
+ {
+ await strategy.ExportLogsAsync(logs, cancellationToken).ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/Apps/LogExporterApp/Strategy/FileExportStrategy.cs b/Apps/LogExporterApp/Strategy/FileExportStrategy.cs
new file mode 100644
index 00000000..3f10cffa
--- /dev/null
+++ b/Apps/LogExporterApp/Strategy/FileExportStrategy.cs
@@ -0,0 +1,84 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LogExporter.Strategy
+{
+ public class FileExportStrategy : IExportStrategy
+ {
+ private readonly string _filePath;
+ private static readonly SemaphoreSlim _fileSemaphore = new SemaphoreSlim(1, 1);
+ private bool disposedValue;
+
+ public FileExportStrategy(string filePath)
+ {
+ _filePath = filePath;
+ }
+
+ public async Task ExportLogsAsync(List logs, CancellationToken cancellationToken = default)
+ {
+ var jsonLogs = new StringBuilder(logs.Count);
+ foreach (var log in logs)
+ {
+ jsonLogs.AppendLine(log.ToString());
+ }
+
+ // Wait to enter the semaphore
+ await _fileSemaphore.WaitAsync(cancellationToken);
+ try
+ {
+ // Use a FileStream with exclusive access
+ using var fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.None);
+ using var writer = new StreamWriter(fileStream);
+ await writer.WriteAsync(jsonLogs.ToString());
+ }
+ finally
+ {
+ // Release the semaphore
+ _fileSemaphore.Release();
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ _fileSemaphore.Release();
+ _fileSemaphore.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ System.GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/Apps/LogExporterApp/Strategy/HttpExportStrategy.cs b/Apps/LogExporterApp/Strategy/HttpExportStrategy.cs
new file mode 100644
index 00000000..ce0157bb
--- /dev/null
+++ b/Apps/LogExporterApp/Strategy/HttpExportStrategy.cs
@@ -0,0 +1,91 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LogExporter.Strategy
+{
+ public class HttpExportStrategy : IExportStrategy
+ {
+ private readonly string _endpoint;
+ private readonly string _method;
+ private readonly Dictionary _headers;
+ private readonly HttpClient _httpClient;
+ private bool disposedValue;
+
+ public HttpExportStrategy(string endpoint, string method, Dictionary headers)
+ {
+ _endpoint = endpoint;
+ _method = method;
+ _headers = headers;
+ _httpClient = new HttpClient();
+ }
+
+ public async Task ExportLogsAsync(List logs, CancellationToken cancellationToken = default)
+ {
+ var jsonLogs = new StringBuilder(logs.Count);
+ foreach (var log in logs)
+ {
+ jsonLogs.AppendLine(log.ToString());
+ }
+ var request = new HttpRequestMessage
+ {
+ RequestUri = new Uri(_endpoint),
+ Method = new HttpMethod(_method),
+ Content = new StringContent(jsonLogs.ToString(), Encoding.UTF8, "application/json")
+ };
+
+ foreach (var header in _headers)
+ {
+ request.Headers.Add(header.Key, header.Value);
+ }
+
+ var response = await _httpClient.SendAsync(request, cancellationToken);
+ if (!response.IsSuccessStatusCode)
+ {
+ throw new Exception($"Failed to export logs to {_endpoint}: {response.StatusCode}");
+ }
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ _httpClient.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/Apps/LogExporterApp/Strategy/IExportStrategy.cs b/Apps/LogExporterApp/Strategy/IExportStrategy.cs
new file mode 100644
index 00000000..c8aae94d
--- /dev/null
+++ b/Apps/LogExporterApp/Strategy/IExportStrategy.cs
@@ -0,0 +1,34 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LogExporter.Strategy
+{
+ ///
+ /// Strategu interface to decide the sinks for exporting the logs.
+ ///
+ public interface IExportStrategy: IDisposable
+ {
+ Task ExportLogsAsync(List logs, CancellationToken cancellationToken = default);
+ }
+}
diff --git a/Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs b/Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs
new file mode 100644
index 00000000..ab6c7624
--- /dev/null
+++ b/Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs
@@ -0,0 +1,168 @@
+/*
+Technitium DNS Server
+Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+*/
+
+using SyslogNet.Client;
+using SyslogNet.Client.Serialization;
+using SyslogNet.Client.Transport;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace LogExporter.Strategy
+{
+ public class SyslogExportStrategy : IExportStrategy
+ {
+ private readonly string _processId;
+ private readonly string _host;
+ private const string _appName = "Technitium DNS Server";
+ private const string _msgId = "dnslog";
+ private const string _sdId = "dnsparams";
+
+ private readonly ISyslogMessageSender _sender;
+ private readonly ISyslogMessageSerializer _serializer;
+ private bool disposedValue;
+
+ public SyslogExportStrategy(string address, int? port, string protocol = "udp")
+ {
+ port ??= 514;
+ _sender = protocol switch
+ {
+ "tls" => new SyslogEncryptedTcpSender(address, port.Value),
+ "tcp" => new SyslogTcpSender(address, port.Value),
+ "udp" => new SyslogUdpSender(address, port.Value),
+ "local" => new SyslogLocalSender(),
+ _ => throw new Exception("Invalid protocol specified"),
+ };
+
+ _serializer = new SyslogRfc5424MessageSerializer();
+ _processId = Environment.ProcessId.ToString();
+ _host = Environment.MachineName;
+ }
+
+ public Task ExportLogsAsync(List logs, CancellationToken cancellationToken = default)
+ {
+ return Task.Run(() =>
+ {
+ var messages = new List(logs.Select(Convert));
+ _sender.Send(messages, _serializer);
+
+ }
+ , cancellationToken);
+ }
+
+ private SyslogMessage Convert(LogEntry log)
+ {
+ // Create the structured data with all key details from LogEntry
+ var elements = new StructuredDataElement(_sdId, new Dictionary
+ {
+ { "timestamp", log.Timestamp.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") },
+ { "clientIp", log.ClientIp },
+ { "clientPort", log.ClientPort.ToString() },
+ { "dnssecOk", log.DnssecOk.ToString() },
+ { "protocol", log.Protocol.ToString() },
+ { "rCode", log.ResponseCode.ToString() }
+ });
+
+ // Add each question to the structured data
+ if (log.Questions != null && log.Questions.Count > 0)
+ {
+ for (int i = 0; i < log.Questions.Count; i++)
+ {
+ var question = log.Questions[i];
+ elements.Parameters.Add($"qName_{i}", question.QuestionName);
+ elements.Parameters.Add($"qType_{i}", question.QuestionType.HasValue ? question.QuestionType.Value.ToString() : "unknown");
+ elements.Parameters.Add($"qClass_{i}", question.QuestionClass.HasValue ? question.QuestionClass.Value.ToString() : "unknown");
+ elements.Parameters.Add($"qSize_{i}", question.Size.ToString());
+ }
+ }
+
+ // Add each answer to the structured data
+ if (log.Answers != null && log.Answers.Count > 0)
+ {
+ for (int i = 0; i < log.Answers.Count; i++)
+ {
+ var answer = log.Answers[i];
+ elements.Parameters.Add($"aType_{i}", answer.RecordType.ToString());
+ elements.Parameters.Add($"aData_{i}", answer.RecordData);
+ elements.Parameters.Add($"aClass_{i}", answer.RecordClass.ToString());
+ elements.Parameters.Add($"aTtl_{i}", answer.RecordTtl.ToString());
+ elements.Parameters.Add($"aSize_{i}", answer.Size.ToString());
+ elements.Parameters.Add($"aDnssecStatus_{i}", answer.DnssecStatus.ToString());
+ }
+ }
+
+ // Include request and response tags if present
+ if (log.RequestTag != null)
+ {
+ elements.Parameters.Add("requestTag", log.RequestTag.ToString());
+ }
+
+ if (log.ResponseTag != null)
+ {
+ elements.Parameters.Add("responseTag", log.ResponseTag.ToString());
+ }
+
+ // Build a comprehensive message summary
+ string questionSummary = log.Questions?.Count > 0
+ ? string.Join(", ", log.Questions.Select((q, index) => $"{q.QuestionName} (Type: {q.QuestionType}, Class: {q.QuestionClass}, Size: {q.Size})"))
+ : "No Questions";
+
+ string answerSummary = log.Answers?.Count > 0
+ ? string.Join(", ", log.Answers.Select((a, index) => $"{a.RecordData} (Type: {a.RecordType}, Class: {a.RecordClass}, TTL: {a.RecordTtl}, Size: {a.Size}, DNSSEC: {a.DnssecStatus})"))
+ : "No Answers";
+
+ string messageSummary = $"{log.ClientIp}:{log.ClientPort} {log.Protocol} DNSSEC={log.DnssecOk} {questionSummary} {log.ResponseCode} {answerSummary}";
+
+ // Create and return the syslog message
+ return new SyslogMessage(
+ log.Timestamp,
+ Facility.UserLevelMessages,
+ Severity.Informational,
+ _host,
+ _appName,
+ _processId,
+ _msgId,
+ messageSummary,
+ elements
+ );
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposedValue)
+ {
+ if (disposing)
+ {
+ _sender.Dispose();
+ }
+
+ disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+ }
+}
diff --git a/Apps/LogExporterApp/dnsApp.config b/Apps/LogExporterApp/dnsApp.config
new file mode 100644
index 00000000..0766f415
--- /dev/null
+++ b/Apps/LogExporterApp/dnsApp.config
@@ -0,0 +1,26 @@
+{
+ "maxLogEntries": 10000,
+ "targets": [
+ {
+ "type": "file",
+ "enabled": true,
+ "path": "/var/log/dns_logs.json"
+ },
+ {
+ "type": "http",
+ "enabled": false,
+ "endpoint": "http://example.com/logs",
+ "method": "POST",
+ "headers": {
+ "Authorization": "Bearer abc123"
+ }
+ },
+ {
+ "type": "syslog",
+ "enabled": false,
+ "address": "127.0.0.1",
+ "port": 514,
+ "protocol": "udp"
+ }
+ ]
+}
diff --git a/DnsServer.sln b/DnsServer.sln
index 06764378..05f4513d 100644
--- a/DnsServer.sln
+++ b/DnsServer.sln
@@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DropRequestsApp", "Apps\Dro
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QueryLogsSqliteApp", "Apps\QueryLogsSqliteApp\QueryLogsSqliteApp.csproj", "{186DEF23-863E-4954-BE16-5E5FCA75ECA2}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogExporterApp", "Apps\LogExporterApp\LogExporterApp.csproj", "{6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AdvancedBlockingApp", "Apps\AdvancedBlockingApp\AdvancedBlockingApp.csproj", "{A4C31093-CA65-42D4-928A-11907076C0DE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NxDomainApp", "Apps\NxDomainApp\NxDomainApp.csproj", "{BB0010FC-20E9-4397-BF9B-C9955D9AD339}"
@@ -61,6 +63,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DnsRebindingProtectionApp",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FilterAaaaApp", "Apps\FilterAaaaApp\FilterAaaaApp.csproj", "{0A9B7F39-80DA-4084-AD47-8707576927ED}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3746EF13-91C5-4858-9DC2-D3C2504BD135}"
+ ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -119,6 +126,10 @@ Global
{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{186DEF23-863E-4954-BE16-5E5FCA75ECA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5}.Release|Any CPU.Build.0 = Release|Any CPU
{A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A4C31093-CA65-42D4-928A-11907076C0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A4C31093-CA65-42D4-928A-11907076C0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -192,6 +203,7 @@ Global
{099D27AF-3AEB-495A-A5D0-46DA59CC9213} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{738079D1-FA5A-40CD-8A27-D831919EE209} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{186DEF23-863E-4954-BE16-5E5FCA75ECA2} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
+ {6F9BCCA9-6422-484B-A065-EF8AF9DA74B5} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{A4C31093-CA65-42D4-928A-11907076C0DE} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{BB0010FC-20E9-4397-BF9B-C9955D9AD339} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}
{45C6F9AD-57D6-4D6D-9498-10B5C828E47E} = {938BF8EF-74B9-4FE0-B46F-11EBB7A4B3D2}