App.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. using DnsServerCore.ApplicationCommon;
  16. using Microsoft.Data.Sqlite;
  17. using System;
  18. using System.Collections.Concurrent;
  19. using System.Collections.Generic;
  20. using System.Data.Common;
  21. using System.IO;
  22. using System.Net;
  23. using System.Text.Json;
  24. using System.Threading;
  25. using System.Threading.Tasks;
  26. using TechnitiumLibrary;
  27. using TechnitiumLibrary.Net.Dns;
  28. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  29. namespace QueryLogsSqlite
  30. {
  31. public sealed class App : IDnsApplication, IDnsQueryLogger
  32. {
  33. #region variables
  34. IDnsServer _dnsServer;
  35. bool _enableLogging;
  36. int _maxLogDays;
  37. int _maxLogRecords;
  38. bool _useInMemoryDb;
  39. string _connectionString;
  40. SqliteConnection _inMemoryConnection;
  41. readonly ConcurrentQueue<LogEntry> _queuedLogs = new ConcurrentQueue<LogEntry>();
  42. readonly Timer _queueTimer;
  43. const int QUEUE_TIMER_INTERVAL = 10000;
  44. const int BULK_INSERT_COUNT = 1000;
  45. readonly Timer _cleanupTimer;
  46. const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000;
  47. const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000;
  48. #endregion
  49. #region constructor
  50. public App()
  51. {
  52. _queueTimer = new Timer(async delegate (object state)
  53. {
  54. try
  55. {
  56. await BulkInsertLogsAsync();
  57. }
  58. catch (Exception ex)
  59. {
  60. _dnsServer.WriteLog(ex);
  61. }
  62. finally
  63. {
  64. try
  65. {
  66. _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
  67. }
  68. catch (ObjectDisposedException)
  69. { }
  70. }
  71. });
  72. _cleanupTimer = new Timer(async delegate (object state)
  73. {
  74. try
  75. {
  76. await using (SqliteConnection connection = new SqliteConnection(_connectionString))
  77. {
  78. await connection.OpenAsync();
  79. if (_maxLogRecords > 0)
  80. {
  81. await using (SqliteCommand command = connection.CreateCommand())
  82. {
  83. command.CommandText = "DELETE FROM dns_logs WHERE ROWID IN (SELECT ROWID FROM dns_logs ORDER BY ROWID DESC LIMIT -1 OFFSET @maxLogRecords);";
  84. command.Parameters.AddWithValue("@maxLogRecords", _maxLogRecords);
  85. await command.ExecuteNonQueryAsync();
  86. }
  87. }
  88. if (_maxLogDays > 0)
  89. {
  90. await using (SqliteCommand command = connection.CreateCommand())
  91. {
  92. command.CommandText = "DELETE FROM dns_logs WHERE timestamp < @timestamp;";
  93. command.Parameters.AddWithValue("@timestamp", DateTime.UtcNow.AddDays(_maxLogDays * -1));
  94. await command.ExecuteNonQueryAsync();
  95. }
  96. }
  97. }
  98. }
  99. catch (Exception ex)
  100. {
  101. _dnsServer.WriteLog(ex);
  102. }
  103. finally
  104. {
  105. try
  106. {
  107. _cleanupTimer.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);
  108. }
  109. catch (ObjectDisposedException)
  110. { }
  111. }
  112. });
  113. }
  114. #endregion
  115. #region IDisposable
  116. public void Dispose()
  117. {
  118. _enableLogging = false; //turn off logging
  119. if (_queueTimer is not null)
  120. _queueTimer.Dispose();
  121. if (_cleanupTimer is not null)
  122. _cleanupTimer.Dispose();
  123. BulkInsertLogsAsync().Sync(); //flush any pending logs
  124. if (_inMemoryConnection is not null)
  125. {
  126. _inMemoryConnection.Dispose();
  127. _inMemoryConnection = null;
  128. }
  129. SqliteConnection.ClearAllPools(); //close db file
  130. }
  131. #endregion
  132. #region private
  133. private async Task BulkInsertLogsAsync()
  134. {
  135. try
  136. {
  137. List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);
  138. while (true)
  139. {
  140. while ((logs.Count < BULK_INSERT_COUNT) && _queuedLogs.TryDequeue(out LogEntry log))
  141. {
  142. logs.Add(log);
  143. }
  144. if (logs.Count < 1)
  145. break;
  146. await using (SqliteConnection connection = new SqliteConnection(_connectionString))
  147. {
  148. await connection.OpenAsync();
  149. await using (DbTransaction transaction = await connection.BeginTransactionAsync())
  150. {
  151. await using (SqliteCommand command = connection.CreateCommand())
  152. {
  153. command.CommandText = "INSERT INTO dns_logs (timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass, answer) VALUES (@timestamp, @client_ip, @protocol, @response_type, @rcode, @qname, @qtype, @qclass, @answer);";
  154. SqliteParameter paramTimestamp = command.Parameters.Add("@timestamp", SqliteType.Text);
  155. SqliteParameter paramClientIp = command.Parameters.Add("@client_ip", SqliteType.Text);
  156. SqliteParameter paramProtocol = command.Parameters.Add("@protocol", SqliteType.Integer);
  157. SqliteParameter paramResponseType = command.Parameters.Add("@response_type", SqliteType.Integer);
  158. SqliteParameter paramRcode = command.Parameters.Add("@rcode", SqliteType.Integer);
  159. SqliteParameter paramQname = command.Parameters.Add("@qname", SqliteType.Text);
  160. SqliteParameter paramQtype = command.Parameters.Add("@qtype", SqliteType.Integer);
  161. SqliteParameter paramQclass = command.Parameters.Add("@qclass", SqliteType.Integer);
  162. SqliteParameter paramAnswer = command.Parameters.Add("@answer", SqliteType.Text);
  163. foreach (LogEntry log in logs)
  164. {
  165. paramTimestamp.Value = log.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFF");
  166. paramClientIp.Value = log.RemoteEP.Address.ToString();
  167. paramProtocol.Value = (int)log.Protocol;
  168. if (log.Response.Tag == null)
  169. paramResponseType.Value = (int)DnsServerResponseType.Recursive;
  170. else
  171. paramResponseType.Value = (int)(DnsServerResponseType)log.Response.Tag;
  172. paramRcode.Value = (int)log.Response.RCODE;
  173. if (log.Request.Question.Count > 0)
  174. {
  175. DnsQuestionRecord query = log.Request.Question[0];
  176. paramQname.Value = query.Name.ToLower();
  177. paramQtype.Value = (int)query.Type;
  178. paramQclass.Value = (int)query.Class;
  179. }
  180. else
  181. {
  182. paramQname.Value = DBNull.Value;
  183. paramQtype.Value = DBNull.Value;
  184. paramQclass.Value = DBNull.Value;
  185. }
  186. if (log.Response.Answer.Count == 0)
  187. {
  188. paramAnswer.Value = DBNull.Value;
  189. }
  190. else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer)
  191. {
  192. paramAnswer.Value = "[ZONE TRANSFER]";
  193. }
  194. else
  195. {
  196. string answer = null;
  197. for (int i = 0; i < log.Response.Answer.Count; i++)
  198. {
  199. if (answer is null)
  200. answer = log.Response.Answer[i].RDATA.ToString();
  201. else
  202. answer += ", " + log.Response.Answer[i].RDATA.ToString();
  203. }
  204. paramAnswer.Value = answer;
  205. }
  206. await command.ExecuteNonQueryAsync();
  207. }
  208. await transaction.CommitAsync();
  209. }
  210. }
  211. }
  212. logs.Clear();
  213. }
  214. }
  215. catch (Exception ex)
  216. {
  217. if (_dnsServer is not null)
  218. _dnsServer.WriteLog(ex);
  219. }
  220. }
  221. #endregion
  222. #region public
  223. public async Task InitializeAsync(IDnsServer dnsServer, string config)
  224. {
  225. _dnsServer = dnsServer;
  226. using JsonDocument jsonDocument = JsonDocument.Parse(config);
  227. JsonElement jsonConfig = jsonDocument.RootElement;
  228. _enableLogging = jsonConfig.GetPropertyValue("enableLogging", true);
  229. _maxLogDays = jsonConfig.GetPropertyValue("maxLogDays", 0);
  230. _maxLogRecords = jsonConfig.GetPropertyValue("maxLogRecords", 0);
  231. _useInMemoryDb = jsonConfig.GetPropertyValue("useInMemoryDb", false);
  232. if (_useInMemoryDb)
  233. {
  234. if (_inMemoryConnection is null)
  235. {
  236. SqliteConnection.ClearAllPools(); //close db file, if any
  237. _connectionString = "Data Source=QueryLogs;Mode=Memory;Cache=Shared";
  238. _inMemoryConnection = new SqliteConnection(_connectionString);
  239. await _inMemoryConnection.OpenAsync();
  240. }
  241. }
  242. else
  243. {
  244. if (_inMemoryConnection is not null)
  245. {
  246. await _inMemoryConnection.DisposeAsync();
  247. _inMemoryConnection = null;
  248. }
  249. string sqliteDbPath = jsonConfig.GetPropertyValue("sqliteDbPath", "querylogs.db");
  250. string connectionString = jsonConfig.GetPropertyValue("connectionString", "Data Source='{sqliteDbPath}'; Cache=Shared;");
  251. if (!Path.IsPathRooted(sqliteDbPath))
  252. sqliteDbPath = Path.Combine(_dnsServer.ApplicationFolder, sqliteDbPath);
  253. connectionString = connectionString.Replace("{sqliteDbPath}", sqliteDbPath);
  254. if ((_connectionString is not null) && !_connectionString.Equals(connectionString))
  255. SqliteConnection.ClearAllPools(); //close previous db file
  256. _connectionString = connectionString;
  257. }
  258. await using (SqliteConnection connection = new SqliteConnection(_connectionString))
  259. {
  260. await connection.OpenAsync();
  261. await using (SqliteCommand command = connection.CreateCommand())
  262. {
  263. command.CommandText = @"
  264. CREATE TABLE IF NOT EXISTS dns_logs
  265. (
  266. dlid INTEGER PRIMARY KEY,
  267. timestamp DATETIME NOT NULL,
  268. client_ip VARCHAR(39) NOT NULL,
  269. protocol TINYINT NOT NULL,
  270. response_type TINYINT NOT NULL,
  271. rcode TINYINT NOT NULL,
  272. qname VARCHAR(255),
  273. qtype SMALLINT,
  274. qclass SMALLINT,
  275. answer TEXT
  276. );
  277. ";
  278. await command.ExecuteNonQueryAsync();
  279. }
  280. await using (SqliteCommand command = connection.CreateCommand())
  281. {
  282. command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp ON dns_logs (timestamp);";
  283. await command.ExecuteNonQueryAsync();
  284. }
  285. await using (SqliteCommand command = connection.CreateCommand())
  286. {
  287. command.CommandText = "CREATE INDEX IF NOT EXISTS index_client_ip ON dns_logs (client_ip);";
  288. await command.ExecuteNonQueryAsync();
  289. }
  290. await using (SqliteCommand command = connection.CreateCommand())
  291. {
  292. command.CommandText = "CREATE INDEX IF NOT EXISTS index_protocol ON dns_logs (protocol);";
  293. await command.ExecuteNonQueryAsync();
  294. }
  295. await using (SqliteCommand command = connection.CreateCommand())
  296. {
  297. command.CommandText = "CREATE INDEX IF NOT EXISTS index_response_type ON dns_logs (response_type);";
  298. await command.ExecuteNonQueryAsync();
  299. }
  300. await using (SqliteCommand command = connection.CreateCommand())
  301. {
  302. command.CommandText = "CREATE INDEX IF NOT EXISTS index_rcode ON dns_logs (rcode);";
  303. await command.ExecuteNonQueryAsync();
  304. }
  305. await using (SqliteCommand command = connection.CreateCommand())
  306. {
  307. command.CommandText = "CREATE INDEX IF NOT EXISTS index_qname ON dns_logs (qname);";
  308. await command.ExecuteNonQueryAsync();
  309. }
  310. await using (SqliteCommand command = connection.CreateCommand())
  311. {
  312. command.CommandText = "CREATE INDEX IF NOT EXISTS index_qtype ON dns_logs (qtype);";
  313. await command.ExecuteNonQueryAsync();
  314. }
  315. await using (SqliteCommand command = connection.CreateCommand())
  316. {
  317. command.CommandText = "CREATE INDEX IF NOT EXISTS index_qclass ON dns_logs (qclass);";
  318. await command.ExecuteNonQueryAsync();
  319. }
  320. await using (SqliteCommand command = connection.CreateCommand())
  321. {
  322. command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp_client_ip ON dns_logs (timestamp, client_ip);";
  323. await command.ExecuteNonQueryAsync();
  324. }
  325. await using (SqliteCommand command = connection.CreateCommand())
  326. {
  327. command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp_qname ON dns_logs (timestamp, qname);";
  328. await command.ExecuteNonQueryAsync();
  329. }
  330. await using (SqliteCommand command = connection.CreateCommand())
  331. {
  332. command.CommandText = "CREATE INDEX IF NOT EXISTS index_client_qname ON dns_logs (client_ip, qname);";
  333. await command.ExecuteNonQueryAsync();
  334. }
  335. await using (SqliteCommand command = connection.CreateCommand())
  336. {
  337. command.CommandText = "CREATE INDEX IF NOT EXISTS index_query ON dns_logs (qname, qtype);";
  338. await command.ExecuteNonQueryAsync();
  339. }
  340. await using (SqliteCommand command = connection.CreateCommand())
  341. {
  342. command.CommandText = "CREATE INDEX IF NOT EXISTS index_all ON dns_logs (timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);";
  343. await command.ExecuteNonQueryAsync();
  344. }
  345. }
  346. if (_enableLogging)
  347. _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
  348. else
  349. _queueTimer.Change(Timeout.Infinite, Timeout.Infinite);
  350. if ((_maxLogDays > 0) || (_maxLogRecords > 0))
  351. _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
  352. else
  353. _cleanupTimer.Change(Timeout.Infinite, Timeout.Infinite);
  354. if (!jsonConfig.TryGetProperty("maxLogRecords", out _))
  355. {
  356. config = config.Replace("\"sqliteDbPath\"", "\"maxLogRecords\": 0,\r\n \"useInMemoryDb\": false,\r\n \"sqliteDbPath\"");
  357. await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config);
  358. }
  359. }
  360. public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
  361. {
  362. if (_enableLogging)
  363. _queuedLogs.Enqueue(new LogEntry(timestamp, request, remoteEP, protocol, response));
  364. return Task.CompletedTask;
  365. }
  366. 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)
  367. {
  368. if (pageNumber == 0)
  369. pageNumber = 1;
  370. if (qname is not null)
  371. qname = qname.ToLower();
  372. string whereClause = string.Empty;
  373. if (start is not null)
  374. whereClause += "timestamp >= @start AND ";
  375. if (end is not null)
  376. whereClause += "timestamp <= @end AND ";
  377. if (clientIpAddress is not null)
  378. whereClause += "client_ip = @client_ip AND ";
  379. if (protocol is not null)
  380. whereClause += "protocol = @protocol AND ";
  381. if (responseType is not null)
  382. whereClause += "response_type = @response_type AND ";
  383. if (rcode is not null)
  384. whereClause += "rcode = @rcode AND ";
  385. if (qname is not null)
  386. {
  387. if (qname.Contains('*'))
  388. {
  389. whereClause += "qname like @qname AND ";
  390. qname = qname.Replace("*", "%");
  391. }
  392. else
  393. {
  394. whereClause += "qname = @qname AND ";
  395. }
  396. }
  397. if (qtype is not null)
  398. whereClause += "qtype = @qtype AND ";
  399. if (qclass is not null)
  400. whereClause += "qclass = @qclass AND ";
  401. if (!string.IsNullOrEmpty(whereClause))
  402. whereClause = whereClause.Substring(0, whereClause.Length - 5);
  403. await using (SqliteConnection connection = new SqliteConnection(_connectionString))
  404. {
  405. await connection.OpenAsync();
  406. //find total entries
  407. long totalEntries;
  408. await using (SqliteCommand command = connection.CreateCommand())
  409. {
  410. command.CommandText = "SELECT Count(*) FROM dns_logs" + (string.IsNullOrEmpty(whereClause) ? ";" : " WHERE " + whereClause + ";");
  411. if (start is not null)
  412. command.Parameters.AddWithValue("@start", start);
  413. if (end is not null)
  414. command.Parameters.AddWithValue("@end", end);
  415. if (clientIpAddress is not null)
  416. command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString());
  417. if (protocol is not null)
  418. command.Parameters.AddWithValue("@protocol", (byte)protocol);
  419. if (responseType is not null)
  420. command.Parameters.AddWithValue("@response_type", (byte)responseType);
  421. if (rcode is not null)
  422. command.Parameters.AddWithValue("@rcode", (byte)rcode);
  423. if (qname is not null)
  424. command.Parameters.AddWithValue("@qname", qname);
  425. if (qtype is not null)
  426. command.Parameters.AddWithValue("@qtype", (ushort)qtype);
  427. if (qclass is not null)
  428. command.Parameters.AddWithValue("@qclass", (ushort)qclass);
  429. totalEntries = (long)await command.ExecuteScalarAsync();
  430. }
  431. long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0);
  432. if ((pageNumber > totalPages) || (pageNumber < 0))
  433. pageNumber = totalPages;
  434. long endRowNum;
  435. long startRowNum;
  436. if (descendingOrder)
  437. {
  438. endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage);
  439. startRowNum = endRowNum - entriesPerPage;
  440. }
  441. else
  442. {
  443. endRowNum = pageNumber * entriesPerPage;
  444. startRowNum = endRowNum - entriesPerPage;
  445. }
  446. List<DnsLogEntry> entries = new List<DnsLogEntry>(entriesPerPage);
  447. await using (SqliteCommand command = connection.CreateCommand())
  448. {
  449. command.CommandText = @"
  450. SELECT * FROM (
  451. SELECT
  452. ROW_NUMBER() OVER (
  453. ORDER BY dlid
  454. ) row_num,
  455. timestamp,
  456. client_ip,
  457. protocol,
  458. response_type,
  459. rcode,
  460. qname,
  461. qtype,
  462. qclass,
  463. answer
  464. FROM
  465. dns_logs
  466. " + (string.IsNullOrEmpty(whereClause) ? "" : "WHERE " + whereClause) + @"
  467. ) t
  468. WHERE
  469. row_num > @start_row_num AND row_num <= @end_row_num
  470. ORDER BY row_num" + (descendingOrder ? " DESC" : "");
  471. command.Parameters.AddWithValue("@start_row_num", startRowNum);
  472. command.Parameters.AddWithValue("@end_row_num", endRowNum);
  473. if (start is not null)
  474. command.Parameters.AddWithValue("@start", start);
  475. if (end is not null)
  476. command.Parameters.AddWithValue("@end", end);
  477. if (clientIpAddress is not null)
  478. command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString());
  479. if (protocol is not null)
  480. command.Parameters.AddWithValue("@protocol", (byte)protocol);
  481. if (responseType is not null)
  482. command.Parameters.AddWithValue("@response_type", (byte)responseType);
  483. if (rcode is not null)
  484. command.Parameters.AddWithValue("@rcode", (byte)rcode);
  485. if (qname is not null)
  486. command.Parameters.AddWithValue("@qname", qname);
  487. if (qtype is not null)
  488. command.Parameters.AddWithValue("@qtype", (ushort)qtype);
  489. if (qclass is not null)
  490. command.Parameters.AddWithValue("@qclass", (ushort)qclass);
  491. await using (SqliteDataReader reader = await command.ExecuteReaderAsync())
  492. {
  493. while (await reader.ReadAsync())
  494. {
  495. DnsQuestionRecord question;
  496. if (reader.IsDBNull(6))
  497. question = null;
  498. else
  499. question = new DnsQuestionRecord(reader.GetString(6), (DnsResourceRecordType)reader.GetInt32(7), (DnsClass)reader.GetInt32(8), false);
  500. string answer;
  501. if (reader.IsDBNull(9))
  502. answer = null;
  503. else
  504. answer = reader.GetString(9);
  505. entries.Add(new DnsLogEntry(reader.GetInt64(0), reader.GetDateTime(1), IPAddress.Parse(reader.GetString(2)), (DnsTransportProtocol)reader.GetByte(3), (DnsServerResponseType)reader.GetByte(4), (DnsResponseCode)reader.GetByte(5), question, answer));
  506. }
  507. }
  508. }
  509. return new DnsLogPage(pageNumber, totalPages, totalEntries, entries);
  510. }
  511. }
  512. #endregion
  513. #region properties
  514. public string Description
  515. { get { return "Logs all incoming DNS requests and their responses in a Sqlite database that can be queried from the DNS Server web console. The query logging throughput is limited by the disk throughput on which the Sqlite db file is stored. This app is not recommended to be used with very high throughput (more than 20,000 requests/second)."; } }
  516. #endregion
  517. class LogEntry
  518. {
  519. #region variables
  520. public readonly DateTime Timestamp;
  521. public readonly DnsDatagram Request;
  522. public readonly IPEndPoint RemoteEP;
  523. public readonly DnsTransportProtocol Protocol;
  524. public readonly DnsDatagram Response;
  525. #endregion
  526. #region constructor
  527. public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
  528. {
  529. Timestamp = timestamp;
  530. Request = request;
  531. RemoteEP = remoteEP;
  532. Protocol = protocol;
  533. Response = response;
  534. }
  535. #endregion
  536. }
  537. }
  538. }