App.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. /*
  2. Technitium DNS Server
  3. Copyright (C) 2023 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.IO;
  21. using System.Net;
  22. using System.Text.Json;
  23. using System.Threading;
  24. using System.Threading.Tasks;
  25. using TechnitiumLibrary.Net.Dns;
  26. using TechnitiumLibrary.Net.Dns.ResourceRecords;
  27. namespace QueryLogsSqlite
  28. {
  29. public class App : IDnsApplication, IDnsQueryLogger
  30. {
  31. #region variables
  32. IDnsServer _dnsServer;
  33. bool _enableLogging;
  34. int _maxLogDays;
  35. string _connectionString;
  36. readonly ConcurrentQueue<LogEntry> _queuedLogs = new ConcurrentQueue<LogEntry>();
  37. Timer _queueTimer;
  38. const int QUEUE_TIMER_INTERVAL = 10000;
  39. const int BULK_INSERT_COUNT = 1000;
  40. Timer _cleanupTimer;
  41. const int CLEAN_UP_TIMER_INITIAL_INTERVAL = 5 * 1000;
  42. const int CLEAN_UP_TIMER_PERIODIC_INTERVAL = 15 * 60 * 1000;
  43. #endregion
  44. #region IDisposable
  45. public void Dispose()
  46. {
  47. _enableLogging = false; //turn off logging
  48. if (_queueTimer is not null)
  49. {
  50. _queueTimer.Dispose();
  51. _queueTimer = null;
  52. }
  53. if (_cleanupTimer is not null)
  54. {
  55. _cleanupTimer.Dispose();
  56. _cleanupTimer = null;
  57. }
  58. BulkInsertLogs(); //flush any pending logs
  59. SqliteConnection.ClearAllPools(); //close db file
  60. }
  61. #endregion
  62. #region private
  63. private void BulkInsertLogs()
  64. {
  65. try
  66. {
  67. List<LogEntry> logs = new List<LogEntry>(BULK_INSERT_COUNT);
  68. while (true)
  69. {
  70. while ((logs.Count < BULK_INSERT_COUNT) && _queuedLogs.TryDequeue(out LogEntry log))
  71. {
  72. logs.Add(log);
  73. }
  74. if (logs.Count < 1)
  75. break;
  76. using (SqliteConnection connection = new SqliteConnection(_connectionString))
  77. {
  78. connection.Open();
  79. using (SqliteTransaction transaction = connection.BeginTransaction())
  80. {
  81. using (SqliteCommand command = connection.CreateCommand())
  82. {
  83. 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);";
  84. SqliteParameter paramTimestamp = command.Parameters.Add("@timestamp", SqliteType.Text);
  85. SqliteParameter paramClientIp = command.Parameters.Add("@client_ip", SqliteType.Text);
  86. SqliteParameter paramProtocol = command.Parameters.Add("@protocol", SqliteType.Integer);
  87. SqliteParameter paramResponseType = command.Parameters.Add("@response_type", SqliteType.Integer);
  88. SqliteParameter paramRcode = command.Parameters.Add("@rcode", SqliteType.Integer);
  89. SqliteParameter paramQname = command.Parameters.Add("@qname", SqliteType.Text);
  90. SqliteParameter paramQtype = command.Parameters.Add("@qtype", SqliteType.Integer);
  91. SqliteParameter paramQclass = command.Parameters.Add("@qclass", SqliteType.Integer);
  92. SqliteParameter paramAnswer = command.Parameters.Add("@answer", SqliteType.Text);
  93. foreach (LogEntry log in logs)
  94. {
  95. paramTimestamp.Value = log.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.FFFFFFF");
  96. paramClientIp.Value = log.RemoteEP.Address.ToString();
  97. paramProtocol.Value = (int)log.Protocol;
  98. if (log.Response.Tag == null)
  99. paramResponseType.Value = (int)DnsServerResponseType.Recursive;
  100. else
  101. paramResponseType.Value = (int)(DnsServerResponseType)log.Response.Tag;
  102. paramRcode.Value = (int)log.Response.RCODE;
  103. if (log.Request.Question.Count > 0)
  104. {
  105. DnsQuestionRecord query = log.Request.Question[0];
  106. paramQname.Value = query.Name.ToLower();
  107. paramQtype.Value = (int)query.Type;
  108. paramQclass.Value = (int)query.Class;
  109. }
  110. else
  111. {
  112. paramQname.Value = DBNull.Value;
  113. paramQtype.Value = DBNull.Value;
  114. paramQclass.Value = DBNull.Value;
  115. }
  116. if (log.Response.Answer.Count == 0)
  117. {
  118. paramAnswer.Value = DBNull.Value;
  119. }
  120. else if ((log.Response.Answer.Count > 2) && log.Response.IsZoneTransfer)
  121. {
  122. paramAnswer.Value = "[ZONE TRANSFER]";
  123. }
  124. else
  125. {
  126. string answer = null;
  127. for (int i = 0; i < log.Response.Answer.Count; i++)
  128. {
  129. if (answer is null)
  130. answer = log.Response.Answer[i].RDATA.ToString();
  131. else
  132. answer += ", " + log.Response.Answer[i].RDATA.ToString();
  133. }
  134. paramAnswer.Value = answer;
  135. }
  136. command.ExecuteNonQuery();
  137. }
  138. transaction.Commit();
  139. }
  140. }
  141. }
  142. logs.Clear();
  143. }
  144. }
  145. catch (Exception ex)
  146. {
  147. if (_dnsServer is not null)
  148. _dnsServer.WriteLog(ex);
  149. }
  150. }
  151. #endregion
  152. #region public
  153. public Task InitializeAsync(IDnsServer dnsServer, string config)
  154. {
  155. _dnsServer = dnsServer;
  156. using JsonDocument jsonDocument = JsonDocument.Parse(config);
  157. JsonElement jsonConfig = jsonDocument.RootElement;
  158. _enableLogging = jsonConfig.GetProperty("enableLogging").GetBoolean();
  159. _maxLogDays = jsonConfig.GetProperty("maxLogDays").GetInt32();
  160. string sqliteDbPath = jsonConfig.GetProperty("sqliteDbPath").GetString();
  161. string connectionString = jsonConfig.GetProperty("connectionString").GetString();
  162. if (!Path.IsPathRooted(sqliteDbPath))
  163. sqliteDbPath = Path.Combine(_dnsServer.ApplicationFolder, sqliteDbPath);
  164. _connectionString = connectionString.Replace("{sqliteDbPath}", sqliteDbPath);
  165. using (SqliteConnection connection = new SqliteConnection(_connectionString))
  166. {
  167. connection.Open();
  168. using (SqliteCommand command = connection.CreateCommand())
  169. {
  170. command.CommandText = @"
  171. CREATE TABLE IF NOT EXISTS dns_logs
  172. (
  173. dlid INTEGER PRIMARY KEY,
  174. timestamp DATETIME NOT NULL,
  175. client_ip VARCHAR(39) NOT NULL,
  176. protocol TINYINT NOT NULL,
  177. response_type TINYINT NOT NULL,
  178. rcode TINYINT NOT NULL,
  179. qname VARCHAR(255),
  180. qtype SMALLINT,
  181. qclass SMALLINT,
  182. answer TEXT
  183. );
  184. ";
  185. command.ExecuteNonQuery();
  186. }
  187. using (SqliteCommand command = connection.CreateCommand())
  188. {
  189. command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp ON dns_logs (timestamp);";
  190. command.ExecuteNonQuery();
  191. }
  192. using (SqliteCommand command = connection.CreateCommand())
  193. {
  194. command.CommandText = "CREATE INDEX IF NOT EXISTS index_client_ip ON dns_logs (client_ip);";
  195. command.ExecuteNonQuery();
  196. }
  197. using (SqliteCommand command = connection.CreateCommand())
  198. {
  199. command.CommandText = "CREATE INDEX IF NOT EXISTS index_protocol ON dns_logs (protocol);";
  200. command.ExecuteNonQuery();
  201. }
  202. using (SqliteCommand command = connection.CreateCommand())
  203. {
  204. command.CommandText = "CREATE INDEX IF NOT EXISTS index_response_type ON dns_logs (response_type);";
  205. command.ExecuteNonQuery();
  206. }
  207. using (SqliteCommand command = connection.CreateCommand())
  208. {
  209. command.CommandText = "CREATE INDEX IF NOT EXISTS index_rcode ON dns_logs (rcode);";
  210. command.ExecuteNonQuery();
  211. }
  212. using (SqliteCommand command = connection.CreateCommand())
  213. {
  214. command.CommandText = "CREATE INDEX IF NOT EXISTS index_qname ON dns_logs (qname);";
  215. command.ExecuteNonQuery();
  216. }
  217. using (SqliteCommand command = connection.CreateCommand())
  218. {
  219. command.CommandText = "CREATE INDEX IF NOT EXISTS index_qtype ON dns_logs (qtype);";
  220. command.ExecuteNonQuery();
  221. }
  222. using (SqliteCommand command = connection.CreateCommand())
  223. {
  224. command.CommandText = "CREATE INDEX IF NOT EXISTS index_qclass ON dns_logs (qclass);";
  225. command.ExecuteNonQuery();
  226. }
  227. using (SqliteCommand command = connection.CreateCommand())
  228. {
  229. command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp_client_ip ON dns_logs (timestamp, client_ip);";
  230. command.ExecuteNonQuery();
  231. }
  232. using (SqliteCommand command = connection.CreateCommand())
  233. {
  234. command.CommandText = "CREATE INDEX IF NOT EXISTS index_timestamp_qname ON dns_logs (timestamp, qname);";
  235. command.ExecuteNonQuery();
  236. }
  237. using (SqliteCommand command = connection.CreateCommand())
  238. {
  239. command.CommandText = "CREATE INDEX IF NOT EXISTS index_client_qname ON dns_logs (client_ip, qname);";
  240. command.ExecuteNonQuery();
  241. }
  242. using (SqliteCommand command = connection.CreateCommand())
  243. {
  244. command.CommandText = "CREATE INDEX IF NOT EXISTS index_query ON dns_logs (qname, qtype);";
  245. command.ExecuteNonQuery();
  246. }
  247. using (SqliteCommand command = connection.CreateCommand())
  248. {
  249. command.CommandText = "CREATE INDEX IF NOT EXISTS index_all ON dns_logs (timestamp, client_ip, protocol, response_type, rcode, qname, qtype, qclass);";
  250. command.ExecuteNonQuery();
  251. }
  252. }
  253. if (_enableLogging)
  254. {
  255. _queueTimer = new Timer(delegate (object state)
  256. {
  257. try
  258. {
  259. BulkInsertLogs();
  260. }
  261. catch (Exception ex)
  262. {
  263. _dnsServer.WriteLog(ex);
  264. }
  265. finally
  266. {
  267. if (_queueTimer is not null)
  268. _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
  269. }
  270. });
  271. _queueTimer.Change(QUEUE_TIMER_INTERVAL, Timeout.Infinite);
  272. }
  273. else
  274. {
  275. if (_queueTimer is not null)
  276. {
  277. _queueTimer.Dispose();
  278. _queueTimer = null;
  279. }
  280. }
  281. if (_maxLogDays < 1)
  282. {
  283. if (_cleanupTimer is not null)
  284. {
  285. _cleanupTimer.Dispose();
  286. _cleanupTimer = null;
  287. }
  288. }
  289. else
  290. {
  291. _cleanupTimer = new Timer(delegate (object state)
  292. {
  293. try
  294. {
  295. using (SqliteConnection connection = new SqliteConnection(_connectionString))
  296. {
  297. connection.Open();
  298. using (SqliteCommand command = connection.CreateCommand())
  299. {
  300. command.CommandText = "DELETE FROM dns_logs WHERE timestamp < @timestamp;";
  301. command.Parameters.AddWithValue("@timestamp", DateTime.UtcNow.AddDays(_maxLogDays * -1));
  302. command.ExecuteNonQuery();
  303. }
  304. }
  305. }
  306. catch (Exception ex)
  307. {
  308. _dnsServer.WriteLog(ex);
  309. }
  310. finally
  311. {
  312. if (_cleanupTimer is not null)
  313. _cleanupTimer.Change(CLEAN_UP_TIMER_PERIODIC_INTERVAL, Timeout.Infinite);
  314. }
  315. });
  316. _cleanupTimer.Change(CLEAN_UP_TIMER_INITIAL_INTERVAL, Timeout.Infinite);
  317. }
  318. return Task.CompletedTask;
  319. }
  320. public Task InsertLogAsync(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
  321. {
  322. if (_enableLogging)
  323. _queuedLogs.Enqueue(new LogEntry(timestamp, request, remoteEP, protocol, response));
  324. return Task.CompletedTask;
  325. }
  326. public 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)
  327. {
  328. if (pageNumber == 0)
  329. pageNumber = 1;
  330. if (qname is not null)
  331. qname = qname.ToLower();
  332. string whereClause = string.Empty;
  333. if (start is not null)
  334. whereClause += "timestamp >= @start AND ";
  335. if (end is not null)
  336. whereClause += "timestamp <= @end AND ";
  337. if (clientIpAddress is not null)
  338. whereClause += "client_ip = @client_ip AND ";
  339. if (protocol is not null)
  340. whereClause += "protocol = @protocol AND ";
  341. if (responseType is not null)
  342. whereClause += "response_type = @response_type AND ";
  343. if (rcode is not null)
  344. whereClause += "rcode = @rcode AND ";
  345. if (qname is not null)
  346. {
  347. if (qname.Contains('*'))
  348. {
  349. whereClause += "qname like @qname AND ";
  350. qname = qname.Replace("*", "%");
  351. }
  352. else
  353. {
  354. whereClause += "qname = @qname AND ";
  355. }
  356. }
  357. if (qtype is not null)
  358. whereClause += "qtype = @qtype AND ";
  359. if (qclass is not null)
  360. whereClause += "qclass = @qclass AND ";
  361. if (!string.IsNullOrEmpty(whereClause))
  362. whereClause = whereClause.Substring(0, whereClause.Length - 5);
  363. using (SqliteConnection connection = new SqliteConnection(_connectionString))
  364. {
  365. connection.Open();
  366. //find total entries
  367. long totalEntries;
  368. using (SqliteCommand command = connection.CreateCommand())
  369. {
  370. command.CommandText = "SELECT Count(*) FROM dns_logs" + (string.IsNullOrEmpty(whereClause) ? ";" : " WHERE " + whereClause + ";");
  371. if (start is not null)
  372. command.Parameters.AddWithValue("@start", start);
  373. if (end is not null)
  374. command.Parameters.AddWithValue("@end", end);
  375. if (clientIpAddress is not null)
  376. command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString());
  377. if (protocol is not null)
  378. command.Parameters.AddWithValue("@protocol", (byte)protocol);
  379. if (responseType is not null)
  380. command.Parameters.AddWithValue("@response_type", (byte)responseType);
  381. if (rcode is not null)
  382. command.Parameters.AddWithValue("@rcode", (byte)rcode);
  383. if (qname is not null)
  384. command.Parameters.AddWithValue("@qname", qname);
  385. if (qtype is not null)
  386. command.Parameters.AddWithValue("@qtype", (ushort)qtype);
  387. if (qclass is not null)
  388. command.Parameters.AddWithValue("@qclass", (ushort)qclass);
  389. totalEntries = (long)command.ExecuteScalar();
  390. }
  391. long totalPages = (totalEntries / entriesPerPage) + (totalEntries % entriesPerPage > 0 ? 1 : 0);
  392. if ((pageNumber > totalPages) || (pageNumber < 0))
  393. pageNumber = totalPages;
  394. long endRowNum;
  395. long startRowNum;
  396. if (descendingOrder)
  397. {
  398. endRowNum = totalEntries - ((pageNumber - 1) * entriesPerPage);
  399. startRowNum = endRowNum - entriesPerPage;
  400. }
  401. else
  402. {
  403. endRowNum = pageNumber * entriesPerPage;
  404. startRowNum = endRowNum - entriesPerPage;
  405. }
  406. List<DnsLogEntry> entries = new List<DnsLogEntry>(entriesPerPage);
  407. using (SqliteCommand command = connection.CreateCommand())
  408. {
  409. command.CommandText = @"
  410. SELECT * FROM (
  411. SELECT
  412. ROW_NUMBER() OVER (
  413. ORDER BY dlid
  414. ) row_num,
  415. timestamp,
  416. client_ip,
  417. protocol,
  418. response_type,
  419. rcode,
  420. qname,
  421. qtype,
  422. qclass,
  423. answer
  424. FROM
  425. dns_logs
  426. " + (string.IsNullOrEmpty(whereClause) ? "" : "WHERE " + whereClause) + @"
  427. ) t
  428. WHERE
  429. row_num > @start_row_num AND row_num <= @end_row_num
  430. ORDER BY row_num" + (descendingOrder ? " DESC" : "");
  431. command.Parameters.AddWithValue("@start_row_num", startRowNum);
  432. command.Parameters.AddWithValue("@end_row_num", endRowNum);
  433. if (start is not null)
  434. command.Parameters.AddWithValue("@start", start);
  435. if (end is not null)
  436. command.Parameters.AddWithValue("@end", end);
  437. if (clientIpAddress is not null)
  438. command.Parameters.AddWithValue("@client_ip", clientIpAddress.ToString());
  439. if (protocol is not null)
  440. command.Parameters.AddWithValue("@protocol", (byte)protocol);
  441. if (responseType is not null)
  442. command.Parameters.AddWithValue("@response_type", (byte)responseType);
  443. if (rcode is not null)
  444. command.Parameters.AddWithValue("@rcode", (byte)rcode);
  445. if (qname is not null)
  446. command.Parameters.AddWithValue("@qname", qname);
  447. if (qtype is not null)
  448. command.Parameters.AddWithValue("@qtype", (ushort)qtype);
  449. if (qclass is not null)
  450. command.Parameters.AddWithValue("@qclass", (ushort)qclass);
  451. using (SqliteDataReader reader = command.ExecuteReader())
  452. {
  453. while (reader.Read())
  454. {
  455. DnsQuestionRecord question;
  456. if (reader.IsDBNull(6))
  457. question = null;
  458. else
  459. question = new DnsQuestionRecord(reader.GetString(6), (DnsResourceRecordType)reader.GetInt32(7), (DnsClass)reader.GetInt32(8), false);
  460. string answer;
  461. if (reader.IsDBNull(9))
  462. answer = null;
  463. else
  464. answer = reader.GetString(9);
  465. 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));
  466. }
  467. }
  468. }
  469. return Task.FromResult(new DnsLogPage(pageNumber, totalPages, totalEntries, entries));
  470. }
  471. }
  472. #endregion
  473. #region properties
  474. public string Description
  475. { 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)."; } }
  476. #endregion
  477. class LogEntry
  478. {
  479. #region variables
  480. public readonly DateTime Timestamp;
  481. public readonly DnsDatagram Request;
  482. public readonly IPEndPoint RemoteEP;
  483. public readonly DnsTransportProtocol Protocol;
  484. public readonly DnsDatagram Response;
  485. #endregion
  486. #region constructor
  487. public LogEntry(DateTime timestamp, DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
  488. {
  489. Timestamp = timestamp;
  490. Request = request;
  491. RemoteEP = remoteEP;
  492. Protocol = protocol;
  493. Response = response;
  494. }
  495. #endregion
  496. }
  497. }
  498. }