mirror of
https://github.com/fergalmoran/DnsServer.git
synced 2026-02-05 15:33:57 +00:00
Initial version of the Log Exporter app
This commit is contained in:
216
Apps/LogExporterApp/App.cs
Normal file
216
Apps/LogExporterApp/App.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
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<LogEntry> _logBuffer;
|
||||
|
||||
private readonly object _queueTimerLock = new object();
|
||||
|
||||
private BufferManagementConfig _config;
|
||||
|
||||
private IDnsServer _dnsServer;
|
||||
|
||||
private Timer _queueTimer;
|
||||
|
||||
private bool disposedValue;
|
||||
|
||||
private readonly IReadOnlyList<DnsLogEntry> _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<LogEntry>(_config.MaxLogEntries.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logBuffer = new BlockingCollection<LogEntry>(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<DnsLogPage> 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<LogEntry>(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
|
||||
}
|
||||
}
|
||||
70
Apps/LogExporterApp/BufferManagementConfig.cs
Normal file
70
Apps/LogExporterApp/BufferManagementConfig.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
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<Target> Targets { get; set; }
|
||||
|
||||
// Load configuration from JSON
|
||||
public static BufferManagementConfig? Deserialize(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<BufferManagementConfig>(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<string,string> Headers { get; set; }
|
||||
|
||||
[JsonPropertyName("address")]
|
||||
public string Address { get; set; }
|
||||
|
||||
[JsonPropertyName("port")]
|
||||
public int? Port { get; set; }
|
||||
|
||||
[JsonPropertyName("protocol")]
|
||||
public string Protocol { get; set; }
|
||||
}
|
||||
}
|
||||
128
Apps/LogExporterApp/LogEntry.cs
Normal file
128
Apps/LogExporterApp/LogEntry.cs
Normal file
@@ -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<Question> Questions { get; set; }
|
||||
public List<Answer> 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<Question>(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<Answer>(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<DateTime>
|
||||
{
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
49
Apps/LogExporterApp/LogExporterApp.csproj
Normal file
49
Apps/LogExporterApp/LogExporterApp.csproj
Normal file
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<Version>1.0</Version>
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
<Company>Technitium</Company>
|
||||
<Product>Technitium DNS Server</Product>
|
||||
<Authors>Zafer Balkan</Authors>
|
||||
<AssemblyName>LogExporterApp</AssemblyName>
|
||||
<RootNamespace>LogExporter</RootNamespace>
|
||||
<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>
|
||||
<Description>The app allows exporting logs to a third party sink using an internal buffer.</Description>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<OutputType>Library</OutputType>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SyslogNet.Client" Version="0.3.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\DnsServerCore.ApplicationCommon\DnsServerCore.ApplicationCommon.csproj">
|
||||
<Private>false</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="TechnitiumLibrary.Net">
|
||||
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
<Reference Include="TechnitiumLibrary">
|
||||
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="dnsApp.config">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
53
Apps/LogExporterApp/Strategy/ExportManager.cs
Normal file
53
Apps/LogExporterApp/Strategy/ExportManager.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
public class ExportManager
|
||||
{
|
||||
private readonly Dictionary<string, IExportStrategy> _exportStrategies;
|
||||
|
||||
public ExportManager()
|
||||
{
|
||||
_exportStrategies = new Dictionary<string, IExportStrategy>();
|
||||
}
|
||||
|
||||
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<LogEntry> logs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
foreach (var strategy in _exportStrategies.Values)
|
||||
{
|
||||
await strategy.ExportLogsAsync(logs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
84
Apps/LogExporterApp/Strategy/FileExportStrategy.cs
Normal file
84
Apps/LogExporterApp/Strategy/FileExportStrategy.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
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<LogEntry> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
91
Apps/LogExporterApp/Strategy/HttpExportStrategy.cs
Normal file
91
Apps/LogExporterApp/Strategy/HttpExportStrategy.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
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<string, string> _headers;
|
||||
private readonly HttpClient _httpClient;
|
||||
private bool disposedValue;
|
||||
|
||||
public HttpExportStrategy(string endpoint, string method, Dictionary<string, string> headers)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
_method = method;
|
||||
_headers = headers;
|
||||
_httpClient = new HttpClient();
|
||||
}
|
||||
|
||||
public async Task ExportLogsAsync(List<LogEntry> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Apps/LogExporterApp/Strategy/IExportStrategy.cs
Normal file
34
Apps/LogExporterApp/Strategy/IExportStrategy.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LogExporter.Strategy
|
||||
{
|
||||
/// <summary>
|
||||
/// Strategu interface to decide the sinks for exporting the logs.
|
||||
/// </summary>
|
||||
public interface IExportStrategy: IDisposable
|
||||
{
|
||||
Task ExportLogsAsync(List<LogEntry> logs, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
168
Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs
Normal file
168
Apps/LogExporterApp/Strategy/SyslogExportStrategy.cs
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
*/
|
||||
|
||||
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<LogEntry> logs, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
var messages = new List<SyslogMessage>(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<string, string>
|
||||
{
|
||||
{ "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);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Apps/LogExporterApp/dnsApp.config
Normal file
26
Apps/LogExporterApp/dnsApp.config
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user