diff --git a/Apps/LogExporterApp/App.cs b/Apps/LogExporterApp/App.cs new file mode 100644 index 00000000..b4d70e46 --- /dev/null +++ b/Apps/LogExporterApp/App.cs @@ -0,0 +1,214 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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; + +namespace LogExporter +{ + public sealed class App : IDnsApplication, IDnsQueryLogger + { + #region variables + + IDnsServer? _dnsServer; + BufferManagementConfig? _config; + + readonly ExportManager _exportManager = new ExportManager(); + + bool _enableLogging; + + readonly ConcurrentQueue _queuedLogs = new ConcurrentQueue(); + readonly Timer _queueTimer; + const int QUEUE_TIMER_INTERVAL = 10000; + const int BULK_INSERT_COUNT = 1000; + + bool _disposed; + + #endregion + + #region constructor + + public App() + { + _queueTimer = new Timer(HandleExportLogCallback); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _queueTimer?.Dispose(); + + ExportLogsAsync().Sync(); //flush any pending logs + + _exportManager.Dispose(); + } + + _disposed = true; + } + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + _dnsServer = dnsServer; + _config = BufferManagementConfig.Deserialize(config); + + if (_config is null) + throw new DnsClientException("Invalid application configuration."); + + if (_config.FileTarget!.Enabled) + { + _exportManager.RemoveStrategy(typeof(FileExportStrategy)); + _exportManager.AddStrategy(new FileExportStrategy(_config.FileTarget!.Path)); + } + else + { + _exportManager.RemoveStrategy(typeof(FileExportStrategy)); + } + + if (_config.HttpTarget!.Enabled) + { + _exportManager.RemoveStrategy(typeof(HttpExportStrategy)); + _exportManager.AddStrategy(new HttpExportStrategy(_config.HttpTarget.Endpoint, _config.HttpTarget.Headers)); + } + else + { + _exportManager.RemoveStrategy(typeof(HttpExportStrategy)); + } + + if (_config.SyslogTarget!.Enabled) + { + _exportManager.RemoveStrategy(typeof(SyslogExportStrategy)); + _exportManager.AddStrategy(new SyslogExportStrategy(_config.SyslogTarget.Address, _config.SyslogTarget.Port, _config.SyslogTarget.Protocol)); + } + else + { + _exportManager.RemoveStrategy(typeof(SyslogExportStrategy)); + } + + _enableLogging = _exportManager.HasStrategy(); + + if (_enableLogging) + _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite); + else + _queueTimer.Change(Timeout.Infinite, Timeout.Infinite); + + return Task.CompletedTask; + } + + public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) + { + if (_enableLogging) + { + if (_queuedLogs.Count < _config!.MaxQueueSize) + _queuedLogs.Enqueue(new LogEntry(timestamp, remoteEP, protocol, request, response)); + } + + return Task.CompletedTask; + } + + #endregion + + #region private + + private async Task ExportLogsAsync() + { + try + { + List logs = new List(BULK_INSERT_COUNT); + + while (true) + { + while (logs.Count < BULK_INSERT_COUNT && _queuedLogs.TryDequeue(out LogEntry? log)) + { + logs.Add(log); + } + + if (logs.Count < 1) + break; + + await _exportManager.ImplementStrategyAsync(logs); + + logs.Clear(); + } + } + catch (Exception ex) + { + _dnsServer?.WriteLog(ex); + } + } + + private async void HandleExportLogCallback(object? state) + { + try + { + // Process logs within the timer interval, then let the timer reschedule + await ExportLogsAsync(); + } + catch (Exception ex) + { + _dnsServer?.WriteLog(ex); + } + finally + { + try + { + _queueTimer?.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite); + } + catch (ObjectDisposedException) + { } + } + } + + #endregion + + #region properties + + public string Description + { + get { return "Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols)."; } + } + + #endregion + } +} diff --git a/Apps/LogExporterApp/BufferManagementConfig.cs b/Apps/LogExporterApp/BufferManagementConfig.cs new file mode 100644 index 00000000..198a452b --- /dev/null +++ b/Apps/LogExporterApp/BufferManagementConfig.cs @@ -0,0 +1,94 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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("maxQueueSize")] + public int MaxQueueSize + { get; set; } + + [JsonPropertyName("file")] + public FileTarget? FileTarget { get; set; } + + [JsonPropertyName("http")] + public HttpTarget? HttpTarget { get; set; } + + [JsonPropertyName("syslog")] + public SyslogTarget? SyslogTarget { get; set; } + + // Load configuration from JSON + public static BufferManagementConfig? Deserialize(string json) + { + return JsonSerializer.Deserialize(json, DnsConfigSerializerOptions.Default); + } + } + + public class TargetBase + { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + } + + public class SyslogTarget : TargetBase + { + [JsonPropertyName("address")] + public string Address { get; set; } + + [JsonPropertyName("port")] + public int? Port { get; set; } + + [JsonPropertyName("protocol")] + public string? Protocol { get; set; } + } + + public class FileTarget : TargetBase + { + [JsonPropertyName("path")] + public string Path { get; set; } + } + + public class HttpTarget : TargetBase + { + [JsonPropertyName("endpoint")] + public string Endpoint { get; set; } + + [JsonPropertyName("headers")] + public Dictionary? Headers { get; set; } + } + + // Setup reusable options with a single instance + public static class DnsConfigSerializerOptions + { + public static readonly JsonSerializerOptions Default = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Convert properties to camelCase + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // For safe encoding + NumberHandling = JsonNumberHandling.Strict, + AllowTrailingCommas = true, // Allow trailing commas in JSON + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, // Convert dictionary keys to camelCase + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull // Ignore null values + }; + } +} diff --git a/Apps/LogExporterApp/LogEntry.cs b/Apps/LogExporterApp/LogEntry.cs new file mode 100644 index 00000000..1a2ac5ab --- /dev/null +++ b/Apps/LogExporterApp/LogEntry.cs @@ -0,0 +1,138 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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 System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Text.Json.Serialization; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace LogExporter +{ + public class LogEntry + { + public DateTime Timestamp { get; private set; } + public string ClientIp { get; private set; } + public DnsTransportProtocol Protocol { get; private set; } + public DnsServerResponseType ResponseType { get; private set; } + public double? ResponseRtt { get; private set; } + public DnsResponseCode ResponseCode { get; private set; } + public DnsQuestion? Question { get; private set; } + public List Answers { get; private 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(); + Protocol = protocol; + ResponseType = response.Tag == null ? DnsServerResponseType.Recursive : (DnsServerResponseType)response.Tag; + + if ((ResponseType == DnsServerResponseType.Recursive) && (response.Metadata is not null)) + ResponseRtt = response.Metadata.RoundTripTime; + + ResponseCode = response.RCODE; + + // Extract request information + if (request.Question.Count > 0) + { + DnsQuestionRecord query = request.Question[0]; + + Question = new DnsQuestion + { + QuestionName = query.Name, + QuestionType = query.Type, + QuestionClass = query.Class, + }; + } + + // 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 DnsResourceRecord + { + Name = record.Name, + RecordType = record.Type, + RecordClass = record.Class, + RecordTtl = record.TTL, + RecordData = record.RDATA.ToString(), + DnssecStatus = record.DnssecStatus, + })); + } + } + + public class DnsQuestion + { + public string QuestionName { get; set; } + public DnsResourceRecordType QuestionType { get; set; } + public DnsClass QuestionClass { get; set; } + } + + public class DnsResourceRecord + { + public string Name { get; set; } + public DnsResourceRecordType RecordType { get; set; } + public DnsClass RecordClass { get; set; } + public uint RecordTtl { get; set; } + public string RecordData { get; set; } + public DnssecStatus DnssecStatus { get; set; } + } + + public override string ToString() + { + return JsonSerializer.Serialize(this, DnsLogSerializerOptions.Default); + } + + // 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) + { + string? 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 + }; + } + } +} \ No newline at end of file diff --git a/Apps/LogExporterApp/LogExporterApp.csproj b/Apps/LogExporterApp/LogExporterApp.csproj new file mode 100644 index 00000000..337b3bb4 --- /dev/null +++ b/Apps/LogExporterApp/LogExporterApp.csproj @@ -0,0 +1,53 @@ + + + + net8.0 + false + true + 1.0 + false + Technitium + Technitium DNS Server + Zafer Balkan + LogExporterApp + LogExporter + https://technitium.com/dns/ + https://github.com/TechnitiumSoftware/DnsServer + Allows exporting query logs to third party sinks. It supports exporting to File, HTTP endpoint, and Syslog (UDP, TCP, TLS, and Local protocols). + 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..50fe2e96 --- /dev/null +++ b/Apps/LogExporterApp/Strategy/ExportManager.cs @@ -0,0 +1,82 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace LogExporter.Strategy +{ + public sealed class ExportManager : IDisposable + { + #region variables + + readonly ConcurrentDictionary _exportStrategies = new ConcurrentDictionary(); + + #endregion + + #region IDisposable + + public void Dispose() + { + foreach (KeyValuePair exportStrategy in _exportStrategies) + exportStrategy.Value.Dispose(); + } + + #endregion + + #region public + + public void AddStrategy(IExportStrategy strategy) + { + if (!_exportStrategies.TryAdd(strategy.GetType(), strategy)) + throw new InvalidOperationException(); + } + + public void RemoveStrategy(Type type) + { + if (_exportStrategies.TryRemove(type, out IExportStrategy? existing)) + existing?.Dispose(); + } + + public bool HasStrategy() + { + return !_exportStrategies.IsEmpty; + } + + public async Task ImplementStrategyAsync(IReadOnlyList logs) + { + List tasks = new List(_exportStrategies.Count); + + foreach (KeyValuePair strategy in _exportStrategies) + { + tasks.Add(Task.Factory.StartNew(delegate (object? state) + { + return strategy.Value.ExportAsync(logs); + }, null, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Current)); + } + + await Task.WhenAll(tasks); + } + + #endregion + } +} diff --git a/Apps/LogExporterApp/Strategy/FileExportStrategy.cs b/Apps/LogExporterApp/Strategy/FileExportStrategy.cs new file mode 100644 index 00000000..1749d6a3 --- /dev/null +++ b/Apps/LogExporterApp/Strategy/FileExportStrategy.cs @@ -0,0 +1,71 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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 Serilog; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace LogExporter.Strategy +{ + public sealed class FileExportStrategy : IExportStrategy + { + #region variables + + readonly Serilog.Core.Logger _sender; + + bool _disposed; + + #endregion + + #region constructor + + public FileExportStrategy(string filePath) + { + _sender = new LoggerConfiguration().WriteTo.File(filePath, outputTemplate: "{Message:lj}{NewLine}{Exception}").CreateLogger(); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (!_disposed) + { + _sender.Dispose(); + + _disposed = true; + } + } + + #endregion + + #region public + + public Task ExportAsync(IReadOnlyList logs) + { + foreach (LogEntry logEntry in logs) + _sender.Information(logEntry.ToString()); + + return Task.CompletedTask; + } + + #endregion + } +} diff --git a/Apps/LogExporterApp/Strategy/HttpExportStrategy.cs b/Apps/LogExporterApp/Strategy/HttpExportStrategy.cs new file mode 100644 index 00000000..7441b992 --- /dev/null +++ b/Apps/LogExporterApp/Strategy/HttpExportStrategy.cs @@ -0,0 +1,119 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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 Microsoft.Extensions.Configuration; +using Serilog; +using Serilog.Sinks.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace LogExporter.Strategy +{ + public sealed class HttpExportStrategy : IExportStrategy + { + #region variables + + readonly Serilog.Core.Logger _sender; + + bool _disposed; + + #endregion + + #region constructor + + public HttpExportStrategy(string endpoint, Dictionary? headers = null) + { + IConfigurationRoot? configuration = null; + if (headers != null) + { + configuration = new ConfigurationBuilder() + .AddInMemoryCollection(headers) + .Build(); + } + + _sender = new LoggerConfiguration().WriteTo.Http(endpoint, null, httpClient: new CustomHttpClient(), configuration: configuration).Enrich.FromLogContext().CreateLogger(); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (!_disposed) + { + _sender.Dispose(); + + _disposed = true; + } + } + + #endregion + + #region public + + public Task ExportAsync(IReadOnlyList logs) + { + foreach (LogEntry logEntry in logs) + _sender.Information(logEntry.ToString()); + + return Task.CompletedTask; + } + + #endregion + + public class CustomHttpClient : IHttpClient + { + readonly HttpClient _httpClient; + + public CustomHttpClient() + { + _httpClient = new HttpClient(); + } + + public void Configure(IConfiguration configuration) + { + foreach (IConfigurationSection pair in configuration.GetChildren()) + { + _httpClient.DefaultRequestHeaders.Add(pair.Key, pair.Value); + } + } + + public void Dispose() + { + _httpClient?.Dispose(); + GC.SuppressFinalize(this); + } + + public async Task PostAsync(string requestUri, Stream contentStream, CancellationToken cancellationToken) + { + StreamContent content = new StreamContent(contentStream); + content.Headers.Add("Content-Type", "application/json"); + + return await _httpClient + .PostAsync(requestUri, content, cancellationToken) + .ConfigureAwait(false); + } + } + } +} diff --git a/Apps/LogExporterApp/Strategy/IExportStrategy.cs b/Apps/LogExporterApp/Strategy/IExportStrategy.cs new file mode 100644 index 00000000..d322330e --- /dev/null +++ b/Apps/LogExporterApp/Strategy/IExportStrategy.cs @@ -0,0 +1,33 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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.Tasks; + +namespace LogExporter.Strategy +{ + /// + /// Strategy interface to decide the sinks for exporting the logs. + /// + public interface IExportStrategy: IDisposable + { + Task ExportAsync(IReadOnlyList logs); + } +} diff --git a/Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs b/Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs new file mode 100644 index 00000000..2b8034c5 --- /dev/null +++ b/Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs @@ -0,0 +1,171 @@ +/* +Technitium DNS Server +Copyright (C) 2025 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 Serilog; +using Serilog.Events; +using Serilog.Parsing; +using Serilog.Sinks.Syslog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace LogExporter.Strategy +{ + public sealed class SyslogExportStrategy : IExportStrategy + { + #region variables + + const string _appName = "Technitium DNS Server"; + const string _sdId = "meta"; + const string DEFAUL_PROTOCOL = "udp"; + const int DEFAULT_PORT = 514; + + readonly Facility _facility = Facility.Local6; + + readonly Rfc5424Formatter _formatter; + readonly Serilog.Core.Logger _sender; + + bool _disposed; + + #endregion + + #region constructor + + public SyslogExportStrategy(string address, int? port, string? protocol) + { + port ??= DEFAULT_PORT; + protocol ??= DEFAUL_PROTOCOL; + + LoggerConfiguration conf = new LoggerConfiguration(); + + _sender = protocol.ToLowerInvariant() switch + { + "tls" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: true).Enrich.FromLogContext().CreateLogger(), + "tcp" => conf.WriteTo.TcpSyslog(address, port.Value, _appName, FramingType.OCTET_COUNTING, SyslogFormat.RFC5424, _facility, useTls: false).Enrich.FromLogContext().CreateLogger(), + "udp" => conf.WriteTo.UdpSyslog(address, port.Value, _appName, SyslogFormat.RFC5424, _facility).Enrich.FromLogContext().CreateLogger(), + "local" => conf.WriteTo.LocalSyslog(_appName, _facility).Enrich.FromLogContext().CreateLogger(), + _ => throw new NotSupportedException("Syslog protocol is not supported: " + protocol), + }; + + _formatter = new Rfc5424Formatter(_facility, _appName, null, _sdId, Environment.MachineName); + } + + #endregion + + #region IDisposable + + public void Dispose() + { + if (!_disposed) + { + _sender.Dispose(); + + _disposed = true; + } + } + + #endregion + + #region public + + public Task ExportAsync(IReadOnlyList logs) + { + foreach (LogEntry log in logs) + _sender.Information(_formatter.FormatMessage((LogEvent?)Convert(log))); + + return Task.CompletedTask; + } + + #endregion + + #region private + + private static LogEvent Convert(LogEntry log) + { + // Initialize properties with base log details + List properties = new List + { + new LogEventProperty("timestamp", new ScalarValue(log.Timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"))), + new LogEventProperty("clientIp", new ScalarValue(log.ClientIp)), + new LogEventProperty("protocol", new ScalarValue(log.Protocol.ToString())), + new LogEventProperty("responseType", new ScalarValue(log.ResponseType.ToString())), + new LogEventProperty("responseRtt", new ScalarValue(log.ResponseRtt?.ToString())), + new LogEventProperty("rCode", new ScalarValue(log.ResponseCode.ToString())) + }; + + // Add each question as properties + if (log.Question != null) + { + LogEntry.DnsQuestion question = log.Question; + properties.Add(new LogEventProperty("qName", new ScalarValue(question.QuestionName))); + properties.Add(new LogEventProperty("qType", new ScalarValue(question.QuestionType.ToString()))); + properties.Add(new LogEventProperty("qClass", new ScalarValue(question.QuestionClass.ToString()))); + + string questionSummary = $"QNAME: {question.QuestionName}, QTYPE: {question.QuestionType.ToString()}, QCLASS: {question.QuestionClass.ToString()}"; + properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(questionSummary))); + } + else + { + properties.Add(new LogEventProperty("questionsSummary", new ScalarValue(string.Empty))); + } + + // Add each answer as properties + if (log.Answers.Count > 0) + { + for (int i = 0; i < log.Answers.Count; i++) + { + LogEntry.DnsResourceRecord answer = log.Answers[i]; + + properties.Add(new LogEventProperty($"aName_{i}", new ScalarValue(answer.Name))); + properties.Add(new LogEventProperty($"aType_{i}", new ScalarValue(answer.RecordType.ToString()))); + properties.Add(new LogEventProperty($"aClass_{i}", new ScalarValue(answer.RecordClass.ToString()))); + properties.Add(new LogEventProperty($"aTtl_{i}", new ScalarValue(answer.RecordTtl.ToString()))); + properties.Add(new LogEventProperty($"aRData_{i}", new ScalarValue(answer.RecordData))); + properties.Add(new LogEventProperty($"aDnssecStatus_{i}", new ScalarValue(answer.DnssecStatus.ToString()))); + } + + // Generate answers summary + string answerSummary = string.Join(", ", log.Answers.Select(a => a.RecordData)); + properties.Add(new LogEventProperty("answersSummary", new ScalarValue(answerSummary))); + } + else + { + properties.Add(new LogEventProperty("answersSummary", new ScalarValue(string.Empty))); + } + + // Define the message template to match the original summary format + const string templateText = "{questionsSummary}; RCODE: {rCode}; ANSWER: [{answersSummary}]"; + + // Parse the template + MessageTemplate template = new MessageTemplateParser().Parse(templateText); + + // Create the LogEvent and return it + return new LogEvent( + timestamp: log.Timestamp, + level: LogEventLevel.Information, + exception: null, + messageTemplate: template, + properties: properties + ); + } + + #endregion + } +} diff --git a/Apps/LogExporterApp/dnsApp.config b/Apps/LogExporterApp/dnsApp.config new file mode 100644 index 00000000..ce6c31ea --- /dev/null +++ b/Apps/LogExporterApp/dnsApp.config @@ -0,0 +1,20 @@ +{ + "maxQueueSize": 1000000, + "file": { + "path": "./dns_logs.json", + "enabled": false + }, + "http": { + "endpoint": "http://localhost:5000/logs", + "headers": { + "Authorization": "Bearer abc123" + }, + "enabled": false + }, + "syslog": { + "address": "127.0.0.1", + "port": 514, + "protocol": "UDP", + "enabled": false + } +} \ No newline at end of file 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}