diff --git a/Apps/DefaultRecordsApp/App.cs b/Apps/DefaultRecordsApp/App.cs new file mode 100644 index 00000000..8fa1a6d9 --- /dev/null +++ b/Apps/DefaultRecordsApp/App.cs @@ -0,0 +1,274 @@ +/* +Technitium DNS Server +Copyright (C) 2023 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.IO; +using System.Net; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using TechnitiumLibrary; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace DefaultRecords +{ + public class App : IDnsApplication, IDnsPostProcessor + { + #region variables + + IDnsServer _dnsServer; + + bool _enableDefaultRecords; + uint _defaultTtl; + IReadOnlyDictionary _zoneSetMap; + IReadOnlyDictionary _sets; + + #endregion + + #region IDisposable + + public void Dispose() + { + //do nothing + } + + #endregion + + #region private + + private static string GetParentZone(string domain) + { + int i = domain.IndexOf('.'); + if (i > -1) + return domain.Substring(i + 1); + + //dont return root zone + return null; + } + + private bool TryGetMappedSets(string domain, out string zone, out string[] setNames) + { + domain = domain.ToLowerInvariant(); + + string parent; + + do + { + if (_zoneSetMap.TryGetValue(domain, out setNames)) + { + zone = domain; + return true; + } + + parent = GetParentZone(domain); + if (parent is null) + { + if (_zoneSetMap.TryGetValue("*", out setNames)) + { + zone = "*"; + return true; + } + + break; + } + + domain = "*." + parent; + + if (_zoneSetMap.TryGetValue(domain, out setNames)) + { + zone = domain; + return true; + } + + domain = parent; + } + while (true); + + zone = null; + return false; + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + _dnsServer = dnsServer; + + using JsonDocument jsonDocument = JsonDocument.Parse(config); + JsonElement jsonConfig = jsonDocument.RootElement; + + _enableDefaultRecords = jsonConfig.GetProperty("enableDefaultRecords").GetBoolean(); + _defaultTtl = jsonConfig.GetPropertyValue("defaultTtl", 3600u); + + _zoneSetMap = jsonConfig.ReadObjectAsMap("zoneSetMap", delegate (string zone, JsonElement jsonSets) + { + string[] sets = jsonSets.GetArray(); + + return new Tuple(zone.ToLowerInvariant(), sets); + }); + + _sets = jsonConfig.ReadArrayAsMap("sets", delegate (JsonElement jsonSet) + { + Set set = new Set(jsonSet); + + return new Tuple(set.Name, set); + }); + + return Task.CompletedTask; + } + + public async Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) + { + if (!_enableDefaultRecords) + return response; + + if (!response.AuthoritativeAnswer || (response.OPCODE != DnsOpcode.StandardQuery)) + return response; + + switch (response.RCODE) + { + case DnsResponseCode.NoError: + case DnsResponseCode.NxDomain: + break; + + default: + return response; + } + + DnsQuestionRecord question = request.Question[0]; + + if (!TryGetMappedSets(question.Name, out string zone, out string[] setNames)) + return response; + + if (zone.StartsWith('*')) + { + DnsDatagram soaResponse = await _dnsServer.DirectQueryAsync(new DnsQuestionRecord(question.Name, DnsResourceRecordType.SOA, DnsClass.IN)); + if (soaResponse is null) + return response; + + if ((soaResponse.Answer.Count > 0) && (soaResponse.Answer[soaResponse.Answer.Count - 1].Type == DnsResourceRecordType.SOA)) + zone = soaResponse.Answer[soaResponse.Answer.Count - 1].Name; + else if ((soaResponse.Authority.Count > 0) && (soaResponse.Authority[0].Type == DnsResourceRecordType.SOA)) + zone = soaResponse.Authority[0].Name; + else + return response; + } + + StringBuilder sb = new StringBuilder(); + + foreach (string setName in setNames) + { + if (_sets.TryGetValue(setName, out Set set) && set.Enable) + { + foreach (string record in set.Records) + sb.AppendLine(record); + } + } + + if (sb.Length == 0) + return response; + + StringReader sR = new StringReader(sb.ToString()); + IReadOnlyCollection records = ZoneFile.ReadZoneFileFromAsync(sR, zone, _defaultTtl).Sync(); + + List newAnswer = new List(response.Answer.Count + records.Count); + string qname = question.Name; + + if (response.Answer.Count > 0) + { + newAnswer.AddRange(response.Answer); + + DnsResourceRecord lastRR = response.Answer[response.Answer.Count - 1]; + if (lastRR.Type == DnsResourceRecordType.CNAME) + qname = (lastRR.RDATA as DnsCNAMERecordData).Domain; + } + + foreach (DnsResourceRecord record in records) + { + if (record.Class != question.Class) + continue; + + if ((record.Type != question.Type) && (record.Type != DnsResourceRecordType.CNAME)) + continue; + + if (!record.Name.Equals(qname, StringComparison.OrdinalIgnoreCase)) + continue; + + newAnswer.Add(record); + + if (record.Type == DnsResourceRecordType.CNAME) + qname = (record.RDATA as DnsCNAMERecordData).Domain; + } + + if (newAnswer.Count == response.Answer.Count) + return response; + + return new DnsDatagram(response.Identifier, true, response.OPCODE, response.AuthoritativeAnswer, response.Truncation, response.RecursionDesired, response.RecursionAvailable, response.AuthenticData, response.CheckingDisabled, DnsResponseCode.NoError, response.Question, newAnswer) { Tag = response.Tag }; + } + + #endregion + + #region properties + + public string Description + { get { return "Enables default records for configured local zones."; } } + + #endregion + + class Set + { + #region variables + + readonly string _name; + readonly bool _enable; + readonly IReadOnlyCollection _records; + + #endregion + + #region constructor + + public Set(JsonElement jsonSet) + { + _name = jsonSet.GetProperty("name").GetString(); + _enable = jsonSet.GetProperty("enable").GetBoolean(); + _records = jsonSet.ReadArray("records"); + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + public bool Enable + { get { return _enable; } } + + public IReadOnlyCollection Records + { get { return _records; } } + + #endregion + } + } +} diff --git a/Apps/DefaultRecordsApp/dnsApp.config b/Apps/DefaultRecordsApp/dnsApp.config new file mode 100644 index 00000000..27b711c9 --- /dev/null +++ b/Apps/DefaultRecordsApp/dnsApp.config @@ -0,0 +1,27 @@ +{ + "enableDefaultRecords": false, + "defaultTtl": 3600, + "zoneSetMap": { + "*": ["set1"], + "*.net": ["set2"], + "example.org": ["set1", "set2"] + }, + "sets": [ + { + "name": "set1", + "enable": true, + "records": [ + "@ 3600 IN MX 10 mail.example.com.", + "@ 3600 IN TXT \"v=spf1 a mx -all\"" + ] + }, + { + "name": "set2", + "enable": true, + "records": [ + "www 3600 IN CNAME @", + "@ 3600 IN A 1.2.3.4" + ] + } + ] +} \ No newline at end of file