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}