diff --git a/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs b/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs new file mode 100644 index 00000000..35b903b8 --- /dev/null +++ b/DnsServerCore/Dns/ZoneManagers/AuthZoneManager.cs @@ -0,0 +1,962 @@ +/* +Technitium DNS Server +Copyright (C) 2020 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.Dns.ResourceRecords; +using DnsServerCore.Dns.Zones; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace DnsServerCore.Dns.ZoneManagers +{ + public class AuthZoneManager + { + #region variables + + readonly DnsServer _dnsServer; + readonly string _serverDomain; + + readonly ZoneTree _root = new ZoneTree(); + + #endregion + + #region constructor + + public AuthZoneManager(DnsServer dnsServer) + { + _dnsServer = dnsServer; + if (_dnsServer == null) + return; //allowed or blocked zone usage + + _serverDomain = _dnsServer.ServerDomain; + + LoadAllZoneFiles(); + } + + #endregion + + #region private + + private void UpdateServerDomain(string serverDomain) + { + //update authoritative zone SOA and NS records + List zones = ListZones(); + + foreach (AuthZoneInfo zone in zones) + { + if (zone.Type != AuthZoneType.Primary) + continue; + + DnsResourceRecord record = zone.QueryRecords(DnsResourceRecordType.SOA)[0]; + DnsSOARecord soa = record.RDATA as DnsSOARecord; + + if (soa.MasterNameServer.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) + { + string responsiblePerson = soa.ResponsiblePerson; + if (responsiblePerson.EndsWith(_serverDomain)) + responsiblePerson = responsiblePerson.Replace(_serverDomain, serverDomain); + + SetRecords(record.Name, record.Type, record.TtlValue, new DnsResourceRecordData[] { new DnsSOARecord(serverDomain, responsiblePerson, soa.Serial, soa.Refresh, soa.Retry, soa.Expire, soa.Minimum) }); + + //update NS records + IReadOnlyList nsResourceRecords = zone.QueryRecords(DnsResourceRecordType.NS); + + foreach (DnsResourceRecord nsResourceRecord in nsResourceRecords) + { + if ((nsResourceRecord.RDATA as DnsNSRecord).NSDomainName.Equals(_serverDomain, StringComparison.OrdinalIgnoreCase)) + { + UpdateRecord(nsResourceRecord, new DnsResourceRecord(nsResourceRecord.Name, nsResourceRecord.Type, nsResourceRecord.Class, nsResourceRecord.TtlValue, new DnsNSRecord(serverDomain)) { Tag = nsResourceRecord.Tag }); + break; + } + } + + if (zone.Internal) + continue; //dont save internal zones to disk + + try + { + SaveZoneFile(zone.Name); + } + catch (Exception ex) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write(ex); + } + } + } + } + + private AuthZone CreateEmptyZone(AuthZoneInfo zoneInfo) + { + AuthZone zone; + + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + zone = new PrimaryZone(_dnsServer, zoneInfo); + break; + + case AuthZoneType.Secondary: + zone = new SecondaryZone(_dnsServer, zoneInfo); + break; + + case AuthZoneType.Stub: + zone = new StubZone(_dnsServer, zoneInfo); + break; + + case AuthZoneType.Forwarder: + zone = new ForwarderZone(zoneInfo); + break; + + default: + throw new InvalidDataException("DNS zone type not supported."); + } + + if (_root.TryAdd(zone)) + return zone; + + throw new DnsServerException("Zone already exists: " + zoneInfo.Name); + } + + private void LoadRecords(IReadOnlyList records) + { + Dictionary>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(records); + + foreach (KeyValuePair>> groupedByTypeRecords in groupedByDomainRecords) + { + AuthZone zone = GetOrAddZone(groupedByTypeRecords.Key); + + foreach (KeyValuePair> groupedRecords in groupedByTypeRecords.Value) + zone.LoadRecords(groupedRecords.Key, groupedRecords.Value); + + if (zone is SubDomainZone) + (zone as SubDomainZone).AutoUpdateState(); + } + } + + private AuthZone GetAuthoritativeZone(string domain) + { + _ = _root.FindZone(domain, out _, out AuthZone authority, out _); + if (authority == null) + return null; + + return authority; + } + + private AuthZone GetOrAddZone(string domain) + { + return _root.GetOrAdd(domain, delegate (string key) + { + AuthZone authZone = GetAuthoritativeZone(domain); + if (authZone == null) + throw new DnsServerException("Zone not found."); + + if (authZone is PrimaryZone) + return new PrimarySubDomainZone(authZone as PrimaryZone, domain); + else if (authZone is SecondaryZone) + return new SecondarySubDomainZone(domain); + else if (authZone is StubZone) + return new StubSubDomainZone(domain); + else if (authZone is ForwarderZone) + return new ForwarderSubDomainZone(domain); + + throw new DnsServerException("Zone cannot have sub domains."); + }); + } + + private IReadOnlyList GetAdditionalRecords(IReadOnlyList nsRecords) + { + IReadOnlyList glueRecords = nsRecords.GetGlueRecords(); + if (glueRecords.Count > 0) + return glueRecords; + + List additionalRecords = new List(); + + foreach (DnsResourceRecord nsRecord in nsRecords) + { + if (nsRecord.Type != DnsResourceRecordType.NS) + continue; + + AuthZone authZone = _root.FindZone((nsRecord.RDATA as DnsNSRecord).NSDomainName, out _, out _, out _); + if ((authZone != null) && !authZone.Disabled) + { + { + IReadOnlyList records = authZone.QueryRecords(DnsResourceRecordType.A); + if ((records.Count > 0) && (records[0].RDATA is DnsARecord)) + additionalRecords.AddRange(records); + } + + { + IReadOnlyList records = authZone.QueryRecords(DnsResourceRecordType.AAAA); + if ((records.Count > 0) && (records[0].RDATA is DnsAAAARecord)) + additionalRecords.AddRange(records); + } + } + } + + return additionalRecords; + } + + private void LoadAllZoneFiles() + { + string zonesFolder = Path.Combine(_dnsServer.ConfigFolder, "zones"); + if (!Directory.Exists(zonesFolder)) + Directory.CreateDirectory(zonesFolder); + + //move zone files to new folder + { + string[] oldZoneFiles = Directory.GetFiles(_dnsServer.ConfigFolder, "*.zone"); + + foreach (string oldZoneFile in oldZoneFiles) + File.Move(oldZoneFile, Path.Combine(zonesFolder, Path.GetFileName(oldZoneFile))); + } + + //remove old internal zones + { + string[] oldZoneFiles = new string[] { "localhost.zone", "1.0.0.127.in-addr.arpa.zone", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa.zone" }; + + foreach (string oldZoneFile in oldZoneFiles) + { + string filePath = Path.Combine(zonesFolder, oldZoneFile); + + if (File.Exists(filePath)) + { + try + { + File.Delete(filePath); + } + catch + { } + } + } + } + + //load system zones + { + { + CreatePrimaryZone("localhost", _dnsServer.ServerDomain, true); + SetRecords("localhost", DnsResourceRecordType.A, 3600, new DnsResourceRecordData[] { new DnsARecord(IPAddress.Loopback) }); + SetRecords("localhost", DnsResourceRecordType.AAAA, 3600, new DnsResourceRecordData[] { new DnsAAAARecord(IPAddress.IPv6Loopback) }); + } + + { + string prtDomain = "0.in-addr.arpa"; + + CreatePrimaryZone(prtDomain, _dnsServer.ServerDomain, true); + } + + { + string prtDomain = "255.in-addr.arpa"; + + CreatePrimaryZone(prtDomain, _dnsServer.ServerDomain, true); + } + + { + string prtDomain = "127.in-addr.arpa"; + + CreatePrimaryZone(prtDomain, _dnsServer.ServerDomain, true); + SetRecords("1.0.0.127.in-addr.arpa", DnsResourceRecordType.PTR, 3600, new DnsResourceRecordData[] { new DnsPTRRecord("localhost") }); + } + + { + string prtDomain = new DnsQuestionRecord(IPAddress.IPv6Loopback, DnsClass.IN).Name; + + CreatePrimaryZone(prtDomain, _dnsServer.ServerDomain, true); + SetRecords(prtDomain, DnsResourceRecordType.PTR, 3600, new DnsResourceRecordData[] { new DnsPTRRecord("localhost") }); + } + } + + //load zone files + string[] zoneFiles = Directory.GetFiles(zonesFolder, "*.zone"); + + foreach (string zoneFile in zoneFiles) + { + try + { + using (FileStream fS = new FileStream(zoneFile, FileMode.Open, FileAccess.Read)) + { + LoadZoneFrom(fS); + } + + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server successfully loaded zone file: " + zoneFile); + } + catch (Exception ex) + { + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("DNS Server failed to load zone file: " + zoneFile + "\r\n" + ex.ToString()); + } + } + } + + private DnsDatagram GetReferralResponse(DnsDatagram request, AuthZone delegationZone) + { + IReadOnlyList authority = delegationZone.QueryRecords(DnsResourceRecordType.NS); + IReadOnlyList additional = GetAdditionalRecords(authority); + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional); + } + + private DnsDatagram GetForwarderResponse(DnsDatagram request, AuthZone forwarderZone) + { + IReadOnlyList authority = forwarderZone.QueryRecords(DnsResourceRecordType.FWD); + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority); + } + + #endregion + + #region public + + public AuthZoneInfo CreatePrimaryZone(string domain, string masterNameServer, bool @internal) + { + AuthZone authZone = new PrimaryZone(_dnsServer, domain, new DnsSOARecord(masterNameServer, "hostmaster." + masterNameServer, 1, 14400, 3600, 604800, 900), @internal); + + if (_root.TryAdd(authZone)) + return new AuthZoneInfo(authZone); + + return null; + } + + public AuthZoneInfo CreatePrimaryZone(string domain, DnsSOARecord soaRecord, DnsNSRecord ns, bool @internal) + { + AuthZone authZone = new PrimaryZone(_dnsServer, domain, soaRecord, ns, @internal); + + if (_root.TryAdd(authZone)) + return new AuthZoneInfo(authZone); + + return null; + } + + public AuthZoneInfo CreateSecondaryZone(string domain, string masterNameServer) + { + AuthZone authZone = new SecondaryZone(_dnsServer, domain, new DnsSOARecord(masterNameServer, "hostmaster." + masterNameServer, 1, 14400, 3600, 604800, 900)); + + if (_root.TryAdd(authZone)) + { + (authZone as SecondaryZone).RefreshZone(); + return new AuthZoneInfo(authZone); + } + + return null; + } + + public AuthZoneInfo CreateStubZone(string domain, string masterNameServer) + { + AuthZone authZone = new StubZone(_dnsServer, domain, new DnsSOARecord(masterNameServer, "hostmaster." + masterNameServer, 1, 14400, 3600, 604800, 900)); + + if (_root.TryAdd(authZone)) + { + (authZone as StubZone).RefreshZone(); + return new AuthZoneInfo(authZone); + } + + return null; + } + + public AuthZoneInfo CreateForwarderZone(string domain, DnsTransportProtocol forwarderProtocol, string forwarder) + { + AuthZone authZone = new ForwarderZone(domain); + + DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.FWD, DnsClass.IN, 0, new DnsForwarderRecord(forwarderProtocol, forwarder)); + authZone.LoadRecords(DnsResourceRecordType.FWD, new DnsResourceRecord[] { record }); + + if (_root.TryAdd(authZone)) + return new AuthZoneInfo(authZone); + + return null; + } + + public bool DeleteZone(string domain) + { + if (_root.TryRemove(domain, out AuthZone authZone)) + { + authZone.Dispose(); + return true; + } + + return false; + } + + public AuthZoneInfo GetZoneInfo(string domain) + { + _ = _root.FindZone(domain, out _, out AuthZone authority, out _); + if (authority == null) + return null; + + return new AuthZoneInfo(authority); + } + + public List ListAllRecords(string domain) + { + List records = new List(); + + foreach (AuthZone zone in _root.GetZoneWithSubDomainZones(domain)) + records.AddRange(zone.ListAllRecords()); + + return records; + } + + public IReadOnlyList QueryRecords(string domain, DnsResourceRecordType type) + { + if (_root.TryGet(domain, out AuthZone zone)) + return zone.QueryRecords(type); + + return Array.Empty(); + } + + public IReadOnlyList QueryZoneTransferRecords(string domain) + { + List axfrRecords = new List(); + + List zones = _root.GetZoneWithSubDomainZones(domain); + + if ((zones.Count > 0) && (zones[0] is PrimaryZone) && !zones[0].Disabled) + { + //only primary zones support zone transfer + DnsResourceRecord soaRecord = zones[0].QueryRecords(DnsResourceRecordType.SOA)[0]; + + axfrRecords.Add(soaRecord); + + foreach (Zone zone in zones) + { + foreach (DnsResourceRecord record in zone.ListAllRecords()) + { + if (record.IsDisabled()) + continue; + + switch (record.Type) + { + case DnsResourceRecordType.SOA: + //skip + break; + + case DnsResourceRecordType.NS: + axfrRecords.Add(record); + axfrRecords.AddRange(record.GetGlueRecords()); + break; + + default: + axfrRecords.Add(record); + break; + } + } + } + + axfrRecords.Add(soaRecord); + } + + return axfrRecords; + } + + public void SyncRecords(string domain, IReadOnlyList syncRecords, IReadOnlyList additionalRecords = null, bool dontRemove = false) + { + List newRecords = new List(syncRecords.Count); + List glueRecords = new List(); + + if (additionalRecords != null) + glueRecords.AddRange(additionalRecords); + + int i = 0; + + if (syncRecords[0].Type == DnsResourceRecordType.SOA) + i = 1; //skip first SOA in AXFR + + for (; i < syncRecords.Count; i++) + { + DnsResourceRecord record = syncRecords[i]; + + if (record.Name.EndsWith(domain, StringComparison.OrdinalIgnoreCase)) + newRecords.Add(record); + else + glueRecords.Add(record); + } + + if (glueRecords.Count > 0) + { + foreach (DnsResourceRecord record in newRecords) + { + if (record.Type == DnsResourceRecordType.NS) + record.SetGlueRecords(glueRecords); + } + } + + List oldRecords = ListAllRecords(domain); + + Dictionary>> newRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(newRecords); + Dictionary>> oldRecordsGroupedByDomain = DnsResourceRecord.GroupRecords(oldRecords); + + if (!dontRemove) + { + //remove domains that do not exists in new records + foreach (string oldDomain in oldRecordsGroupedByDomain.Keys) + { + if (!newRecordsGroupedByDomain.ContainsKey(oldDomain)) + _root.TryRemove(oldDomain, out _); + } + } + + //sync new records + foreach (KeyValuePair>> newEntries in newRecordsGroupedByDomain) + { + AuthZone zone = GetOrAddZone(newEntries.Key); + zone.SyncRecords(newEntries.Value, dontRemove); + } + } + + public void SetRecords(string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData[] records) + { + DnsResourceRecord[] resourceRecords = new DnsResourceRecord[records.Length]; + + for (int i = 0; i < records.Length; i++) + resourceRecords[i] = new DnsResourceRecord(domain, type, DnsClass.IN, ttl, records[i]); + + AuthZone zone = GetOrAddZone(domain); + + zone.SetRecords(type, resourceRecords); + + if (zone is SubDomainZone) + (zone as SubDomainZone).AutoUpdateState(); + } + + public void AddRecord(string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData record) + { + AuthZone zone = GetOrAddZone(domain); + + zone.AddRecord(new DnsResourceRecord(zone.Name, type, DnsClass.IN, ttl, record)); + + if (zone is SubDomainZone) + (zone as SubDomainZone).AutoUpdateState(); + } + + public void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) + { + if (oldRecord.Type != newRecord.Type) + throw new DnsServerException("Cannot update record: new record must be of same type."); + + if (oldRecord.Type == DnsResourceRecordType.SOA) + throw new DnsServerException("Cannot update record: use SetRecords() for updating SOA record."); + + if (!_root.TryGet(oldRecord.Name, out AuthZone zone)) + throw new DnsServerException("Cannot update record: zone does not exists."); + + switch (oldRecord.Type) + { + case DnsResourceRecordType.CNAME: + case DnsResourceRecordType.PTR: + if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) + { + zone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); + + if (zone is SubDomainZone) + (zone as SubDomainZone).AutoUpdateState(); + } + else + { + zone.DeleteRecords(oldRecord.Type); + + if (zone is SubDomainZone) + { + if (zone.IsEmpty) + _root.TryRemove(oldRecord.Name, out _); //remove empty sub zone + else + (zone as SubDomainZone).AutoUpdateState(); + } + + AuthZone newZone = GetOrAddZone(newRecord.Name); + + newZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); + + if (newZone is SubDomainZone) + (newZone as SubDomainZone).AutoUpdateState(); + } + break; + + default: + if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) + { + zone.DeleteRecord(oldRecord.Type, oldRecord.RDATA); + zone.AddRecord(newRecord); + + if (zone is SubDomainZone) + (zone as SubDomainZone).AutoUpdateState(); + } + else + { + zone.DeleteRecord(oldRecord.Type, oldRecord.RDATA); + + if (zone is SubDomainZone) + { + if (zone.IsEmpty) + _root.TryRemove(oldRecord.Name, out _); //remove empty sub zone + else + (zone as SubDomainZone).AutoUpdateState(); + } + + AuthZone newZone = GetOrAddZone(newRecord.Name); + + newZone.AddRecord(newRecord); + + if (newZone is SubDomainZone) + (newZone as SubDomainZone).AutoUpdateState(); + } + break; + } + } + + public void DeleteRecord(string domain, DnsResourceRecordType type, DnsResourceRecordData record) + { + if (_root.TryGet(domain, out AuthZone zone)) + { + zone.DeleteRecord(type, record); + + if (zone is SubDomainZone) + { + if (zone.IsEmpty) + _root.TryRemove(domain, out _); //remove empty sub zone + else + (zone as SubDomainZone).AutoUpdateState(); + } + } + } + + public void DeleteRecords(string domain, DnsResourceRecordType type) + { + if (_root.TryGet(domain, out AuthZone zone)) + { + zone.DeleteRecords(type); + + if (zone is SubDomainZone) + { + if (zone.IsEmpty) + _root.TryRemove(domain, out _); //remove empty sub zone + else + (zone as SubDomainZone).AutoUpdateState(); + } + } + } + + public List ListZones() + { + List zones = new List(); + + foreach (AuthZone zone in _root) + { + AuthZoneInfo zoneInfo = new AuthZoneInfo(zone); + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + case AuthZoneType.Secondary: + case AuthZoneType.Stub: + case AuthZoneType.Forwarder: + zones.Add(zoneInfo); + break; + } + } + + return zones; + } + + public List ListSubDomains(string domain) + { + return _root.ListSubDomains(domain); + } + + public DnsDatagram Query(DnsDatagram request) + { + AuthZone zone = _root.FindZone(request.Question[0].Name, out AuthZone delegation, out AuthZone authZone, out bool hasSubDomains); + + if ((authZone == null) || authZone.Disabled) //no authority for requested zone + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question); + + if ((delegation != null) && !delegation.Disabled) + return GetReferralResponse(request, delegation); + + if ((zone == null) || zone.Disabled) + { + //zone not found + if (authZone is StubZone) + return GetReferralResponse(request, authZone); + else if (authZone is ForwarderZone) + return GetForwarderResponse(request, authZone); + + DnsResponseCode rCode = hasSubDomains ? DnsResponseCode.NoError : DnsResponseCode.NameError; + IReadOnlyList authority = authZone.QueryRecords(DnsResourceRecordType.SOA); + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, rCode, request.Question, null, authority); + } + else + { + //zone found + IReadOnlyList authority; + IReadOnlyList additional; + + IReadOnlyList answers = zone.QueryRecords(request.Question[0].Type); + if (answers.Count == 0) + { + //record type not found + if (authZone is StubZone) + return GetReferralResponse(request, authZone); + else if (authZone is ForwarderZone) + return GetForwarderResponse(request, authZone); + + authority = authZone.QueryRecords(DnsResourceRecordType.SOA); + additional = null; + } + else + { + //record type found + if (zone.Name.Contains("*")) + { + //wildcard zone; generate new answer records + DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answers.Count]; + + for (int i = 0; i < answers.Count; i++) + wildcardAnswers[i] = new DnsResourceRecord(request.Question[0].Name, answers[i].Type, answers[i].Class, answers[i].TtlValue, answers[i].RDATA) { Tag = answers[i].Tag }; + + answers = wildcardAnswers; + } + + switch (request.Question[0].Type) + { + case DnsResourceRecordType.NS: + authority = null; + additional = GetAdditionalRecords(answers); + break; + + case DnsResourceRecordType.ANY: + authority = null; + additional = null; + break; + + default: + authority = authZone.QueryRecords(DnsResourceRecordType.NS); + additional = GetAdditionalRecords(authority); + break; + } + } + + return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, authority, additional); + } + } + + public void LoadZoneFrom(Stream s) + { + BinaryReader bR = new BinaryReader(s); + + if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DZ") + throw new InvalidDataException("DnsServer zone file format is invalid."); + + switch (bR.ReadByte()) + { + case 2: + { + DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; + if (records.Length > 0) + { + for (int i = 0; i < records.Length; i++) + records[i] = new DnsResourceRecord(s); + + //make zone info + AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, AuthZoneType.Primary, false); + + //create zone + Zone authZone = CreateEmptyZone(zoneInfo); + + //load records + LoadRecords(records); + + //init zone + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + (authZone as PrimaryZone).NotifyNameServers(); + break; + } + } + } + break; + + case 3: + { + bool zoneDisabled = bR.ReadBoolean(); + DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; + if (records.Length > 0) + { + for (int i = 0; i < records.Length; i++) + { + records[i] = new DnsResourceRecord(s); + records[i].Tag = new DnsResourceRecordInfo(bR); + } + + //make zone info + AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, AuthZoneType.Primary, zoneDisabled); + + //create zone + Zone authZone = CreateEmptyZone(zoneInfo); + + //load records + LoadRecords(records); + + //init zone + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + (authZone as PrimaryZone).NotifyNameServers(); + break; + } + } + } + break; + + case 4: + { + //read zone info + AuthZoneInfo zoneInfo = new AuthZoneInfo(bR); + + //create zone + Zone authZone = CreateEmptyZone(zoneInfo); + + //read all zone records + DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; + if (records.Length > 0) + { + for (int i = 0; i < records.Length; i++) + { + records[i] = new DnsResourceRecord(s); + records[i].Tag = new DnsResourceRecordInfo(bR); + } + + //load records + LoadRecords(records); + + //init zone + switch (zoneInfo.Type) + { + case AuthZoneType.Primary: + (authZone as PrimaryZone).NotifyNameServers(); + break; + + case AuthZoneType.Secondary: + (authZone as SecondaryZone).RefreshZone(); + break; + + case AuthZoneType.Stub: + (authZone as StubZone).RefreshZone(); + break; + } + } + } + break; + + default: + throw new InvalidDataException("DNS Zone file version not supported."); + } + } + + public void WriteZoneTo(string domain, Stream s) + { + List zones = _root.GetZoneWithSubDomainZones(domain); + if (zones.Count == 0) + throw new DnsServerException("Zone was not found: " + domain); + + //serialize zone + BinaryWriter bW = new BinaryWriter(s); + + bW.Write(Encoding.ASCII.GetBytes("DZ")); //format + bW.Write((byte)4); //version + + //write zone info + AuthZoneInfo zoneInfo = new AuthZoneInfo(zones[0]); + zoneInfo.WriteTo(bW); + + //write all zone records + List records = new List(); + + foreach (AuthZone zone in zones) + records.AddRange(zone.ListAllRecords()); + + bW.Write(records.Count); + + foreach (DnsResourceRecord record in records) + { + record.WriteTo(s); + + DnsResourceRecordInfo rrInfo = record.Tag as DnsResourceRecordInfo; + if (rrInfo == null) + rrInfo = new DnsResourceRecordInfo(); //default info + + rrInfo.WriteTo(bW); + } + } + + public void SaveZoneFile(string domain) + { + domain = domain.ToLower(); + + using (MemoryStream mS = new MemoryStream()) + { + //serialize zone + WriteZoneTo(domain, mS); + + //write to zone file + mS.Position = 0; + + using (FileStream fS = new FileStream(Path.Combine(_dnsServer.ConfigFolder, "zones", domain + ".zone"), FileMode.Create, FileAccess.Write)) + { + mS.CopyTo(fS); + } + } + + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("Saved zone file for domain: " + domain); + } + + public void DeleteZoneFile(string domain) + { + domain = domain.ToLower(); + + File.Delete(Path.Combine(_dnsServer.ConfigFolder, "zones", domain + ".zone")); + + LogManager log = _dnsServer.LogManager; + if (log != null) + log.Write("Deleted zone file for domain: " + domain); + } + + #endregion + + #region properties + + public string ServerDomain + { + get { return _serverDomain; } + set { UpdateServerDomain(value); } + } + + #endregion + } +} diff --git a/DnsServerCore/Dns/Zones/AuthZoneManager.cs b/DnsServerCore/Dns/Zones/AuthZoneManager.cs deleted file mode 100644 index c51dded6..00000000 --- a/DnsServerCore/Dns/Zones/AuthZoneManager.cs +++ /dev/null @@ -1,602 +0,0 @@ -/* -Technitium DNS Server -Copyright (C) 2020 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.IO; -using System.Net; -using System.Net.Sockets; -using System.Text; -using TechnitiumLibrary.Net.Dns; -using TechnitiumLibrary.Net.Dns.ResourceRecords; - -namespace DnsServerCore.Dns.Zones -{ - public class AuthZoneManager - { - #region variables - - readonly ZoneTree _root = new ZoneTree(); - - #endregion - - #region private - - private void CreateZone(AuthZoneInfo zoneInfo) - { - //create zone - switch (zoneInfo.Type) - { - case AuthZoneType.Primary: - if (!_root.TryAdd(new PrimaryZone(zoneInfo.Name, zoneInfo.Disabled))) - throw new DnsServerException("Zone already exists: " + zoneInfo.Name); - - break; - - case AuthZoneType.Secondary: - if (!_root.TryAdd(new SecondaryZone(zoneInfo.Name, zoneInfo.Disabled))) - throw new DnsServerException("Zone already exists: " + zoneInfo.Name); - - break; - - case AuthZoneType.Stub: - if (!_root.TryAdd(new StubZone(zoneInfo.Name, zoneInfo.Disabled))) - throw new DnsServerException("Zone already exists: " + zoneInfo.Name); - - break; - - default: - throw new InvalidDataException("DNS Zone type not supported."); - } - } - - private IReadOnlyList GetAdditionalRecords(IReadOnlyList nsRecords) - { - IReadOnlyList glueRecords = nsRecords.GetGlueRecords(); - if (glueRecords.Count > 0) - return glueRecords; - - List additionalRecords = new List(); - - foreach (DnsResourceRecord nsRecord in nsRecords) - { - if (nsRecord.Type != DnsResourceRecordType.NS) - continue; - - AuthZone authZone = _root.FindZone((nsRecord.RDATA as DnsNSRecord).NSDomainName, out _, out _, out _); - if ((authZone != null) && !authZone.Disabled) - { - { - IReadOnlyList records = authZone.QueryRecords(DnsResourceRecordType.A); - if ((records.Count > 0) && (records[0].RDATA is DnsARecord)) - additionalRecords.AddRange(records); - } - - { - IReadOnlyList records = authZone.QueryRecords(DnsResourceRecordType.AAAA); - if ((records.Count > 0) && (records[0].RDATA is DnsAAAARecord)) - additionalRecords.AddRange(records); - } - } - } - - return additionalRecords; - } - - #endregion - - #region public - - public bool CreatePrimaryZone(string domain, string masterNameServer, bool @internal) - { - return _root.TryAdd(new PrimaryZone(domain, new DnsSOARecord(masterNameServer, "hostmaster." + masterNameServer, 1, 14400, 3600, 604800, 900), @internal)); - } - - public bool CreatePrimaryZone(string domain, DnsSOARecord soaRecord, DnsNSRecord ns, bool @internal) - { - return _root.TryAdd(new PrimaryZone(domain, soaRecord, ns, @internal)); - } - - public bool CreateSecondaryZone(string domain, string masterNameServer) - { - return _root.TryAdd(new SecondaryZone(domain, new DnsSOARecord(masterNameServer, "hostmaster." + masterNameServer, 1, 14400, 3600, 604800, 900))); - } - - public bool CreateStubZone(string domain, string masterNameServer) - { - return _root.TryAdd(new StubZone(domain, new DnsSOARecord(masterNameServer, "hostmaster." + masterNameServer, 1, 14400, 3600, 604800, 900))); - } - - public bool DeleteZone(string domain) - { - return _root.TryRemove(domain, out _); - } - - public AuthZoneInfo GetZoneInfo(string domain) - { - _ = _root.FindZone(domain, out _, out AuthZone authority, out _); - if (authority == null) - return null; - - return new AuthZoneInfo(authority); - } - - public bool ZoneExistsAndEnabled(string domain) - { - if (_root.TryGet(domain, out AuthZone zone)) - return !zone.Disabled; - - return false; - } - - public void DisableZone(string domain) - { - if (_root.TryGet(domain, out AuthZone zone)) - zone.Disabled = true; - } - - public List ListAllRecords(string domain) - { - List records = new List(); - - foreach (AuthZone zone in _root.GetZoneWithSubDomainZones(domain)) - records.AddRange(zone.ListAllRecords()); - - return records; - } - - public IReadOnlyList QueryRecords(string domain, DnsResourceRecordType type) - { - if (_root.TryGet(domain, out AuthZone zone)) - return zone.QueryRecords(type); - - return Array.Empty(); - } - - public List GetZoneTransferRecords(string domain) - { - List axfrRecords = new List(); - - List zones = _root.GetZoneWithSubDomainZones(domain); - - if ((zones.Count > 0) && (zones[0] is PrimaryZone) && !zones[0].Disabled) - { - //only primary zones support zone transfer - DnsResourceRecord soaRecord = zones[0].QueryRecords(DnsResourceRecordType.SOA)[0]; - - axfrRecords.Add(soaRecord); - - foreach (Zone zone in zones) - { - foreach (DnsResourceRecord record in zone.ListAllRecords()) - { - if (record.Type != DnsResourceRecordType.SOA) - axfrRecords.Add(record); - } - } - - axfrRecords.Add(soaRecord); - } - - return axfrRecords; - } - - public void SetRecords(string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData[] records) - { - AuthZone zone = _root.GetOrAdd(domain, delegate (string key) - { - return new SubDomainZone(domain); - }); - - DnsResourceRecord[] resourceRecords = new DnsResourceRecord[records.Length]; - - for (int i = 0; i < records.Length; i++) - resourceRecords[i] = new DnsResourceRecord(zone.Name, type, DnsClass.IN, ttl, records[i]); - - zone.SetRecords(type, resourceRecords); - - if (zone is SubDomainZone) - zone.Disabled = zone.AreAllRecordsDisabled(); - } - - public void SetRecords(IReadOnlyList resourceRecords) - { - if (resourceRecords.Count == 1) - { - AuthZone zone = _root.GetOrAdd(resourceRecords[0].Name, delegate (string key) - { - return new SubDomainZone(resourceRecords[0].Name); - }); - - zone.SetRecords(resourceRecords[0].Type, resourceRecords); - - if (zone is SubDomainZone) - zone.Disabled = zone.AreAllRecordsDisabled(); - } - else - { - Dictionary>> groupedByDomainRecords = DnsResourceRecord.GroupRecords(resourceRecords); - - //add grouped records - foreach (KeyValuePair>> groupedByTypeRecords in groupedByDomainRecords) - { - AuthZone zone = _root.GetOrAdd(groupedByTypeRecords.Key, delegate (string key) - { - return new SubDomainZone(groupedByTypeRecords.Key); - }); - - foreach (KeyValuePair> groupedRecords in groupedByTypeRecords.Value) - zone.SetRecords(groupedRecords.Key, groupedRecords.Value); - - if (zone is SubDomainZone) - zone.Disabled = zone.AreAllRecordsDisabled(); - } - } - } - - public void AddRecord(string domain, DnsResourceRecordType type, uint ttl, DnsResourceRecordData record) - { - AuthZone zone = _root.GetOrAdd(domain, delegate (string key) - { - return new SubDomainZone(domain); - }); - - zone.AddRecord(new DnsResourceRecord(zone.Name, type, DnsClass.IN, ttl, record)); - - if (zone is SubDomainZone) - zone.Disabled = zone.AreAllRecordsDisabled(); - } - - public void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord) - { - if (oldRecord.Type != newRecord.Type) - throw new DnsServerException("Cannot update record: new record must be of same type."); - - if (oldRecord.Type == DnsResourceRecordType.SOA) - throw new DnsServerException("Cannot update record: use SetRecords() for updating SOA record."); - - if (!_root.TryGet(oldRecord.Name, out AuthZone zone)) - throw new DnsServerException("Cannot update record: zone does not exists."); - - switch (oldRecord.Type) - { - case DnsResourceRecordType.CNAME: - case DnsResourceRecordType.PTR: - if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) - { - zone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); - - if (zone is SubDomainZone) - zone.Disabled = zone.AreAllRecordsDisabled(); - } - else - { - zone.DeleteRecords(oldRecord.Type); - - if (zone is SubDomainZone) - { - if (zone.IsEmpty) - _root.TryRemove(oldRecord.Name, out _); //remove empty sub zone - else - zone.Disabled = zone.AreAllRecordsDisabled(); - } - - AuthZone newZone = _root.GetOrAdd(newRecord.Name, delegate (string key) - { - return new SubDomainZone(newRecord.Name); - }); - - newZone.SetRecords(newRecord.Type, new DnsResourceRecord[] { newRecord }); - - if (newZone is SubDomainZone) - newZone.Disabled = zone.AreAllRecordsDisabled(); - } - break; - - default: - if (oldRecord.Name.Equals(newRecord.Name, StringComparison.OrdinalIgnoreCase)) - { - zone.DeleteRecord(oldRecord.Type, oldRecord.RDATA); - zone.AddRecord(newRecord); - - if (zone is SubDomainZone) - zone.Disabled = zone.AreAllRecordsDisabled(); - } - else - { - zone.DeleteRecord(oldRecord.Type, oldRecord.RDATA); - - if (zone is SubDomainZone) - { - if (zone.IsEmpty) - _root.TryRemove(oldRecord.Name, out _); //remove empty sub zone - else - zone.Disabled = zone.AreAllRecordsDisabled(); - } - - AuthZone newZone = _root.GetOrAdd(newRecord.Name, delegate (string key) - { - return new SubDomainZone(newRecord.Name); - }); - - newZone.AddRecord(newRecord); - - if (newZone is SubDomainZone) - newZone.Disabled = zone.AreAllRecordsDisabled(); - } - break; - } - } - - public void DeleteRecord(string domain, DnsResourceRecordType type, DnsResourceRecordData record) - { - if (_root.TryGet(domain, out AuthZone zone)) - { - zone.DeleteRecord(type, record); - - if (zone is SubDomainZone) - { - if (zone.IsEmpty) - _root.TryRemove(domain, out _); //remove empty sub zone - else - zone.Disabled = zone.AreAllRecordsDisabled(); - } - } - } - - public void DeleteRecords(string domain, DnsResourceRecordType type) - { - if (_root.TryGet(domain, out AuthZone zone)) - { - zone.DeleteRecords(type); - - if (zone is SubDomainZone) - { - if (zone.IsEmpty) - _root.TryRemove(domain, out _); //remove empty sub zone - else - zone.Disabled = zone.AreAllRecordsDisabled(); - } - } - } - - public List ListZones() - { - List zones = new List(); - - foreach (AuthZone zone in _root) - { - AuthZoneInfo zoneInfo = new AuthZoneInfo(zone); - switch (zoneInfo.Type) - { - case AuthZoneType.Primary: - case AuthZoneType.Secondary: - case AuthZoneType.Stub: - zones.Add(zoneInfo); - break; - } - } - - return zones; - } - - public List ListSubDomains(string domain) - { - return _root.ListSubDomains(domain); - } - - public DnsDatagram Query(DnsDatagram request) - { - AuthZone zone = _root.FindZone(request.Question[0].Name, out AuthZone delegation, out AuthZone authZone, out bool hasSubDomains); - - if ((authZone == null) || authZone.Disabled) //no authority for requested zone - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.Refused, request.Question); - - if ((authZone is StubZone) || ((delegation != null) && !delegation.Disabled)) - { - //zone is delegation - IReadOnlyList authority = delegation.QueryRecords(DnsResourceRecordType.NS); - IReadOnlyList additional = GetAdditionalRecords(authority); - - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, null, authority, additional); - } - - if ((zone == null) || zone.Disabled) - { - //zone not found - DnsResponseCode rCode = hasSubDomains ? DnsResponseCode.NoError : DnsResponseCode.NameError; - IReadOnlyList authority = authZone.QueryRecords(DnsResourceRecordType.SOA); - - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, rCode, request.Question, null, authority); - } - - //zone found - if ((authZone is PrimaryZone) || (authZone is SecondaryZone)) - { - IReadOnlyList authority; - IReadOnlyList additional; - - IReadOnlyList answers = zone.QueryRecords(request.Question[0].Type); - if (answers.Count == 0) - { - //record type not found - authority = authZone.QueryRecords(DnsResourceRecordType.SOA); - additional = null; - } - else - { - //record type found - if (zone.Name.Contains("*")) - { - //wildcard zone; generate new answer records - DnsResourceRecord[] wildcardAnswers = new DnsResourceRecord[answers.Count]; - - for (int i = 0; i < answers.Count; i++) - wildcardAnswers[i] = new DnsResourceRecord(request.Question[0].Name, answers[i].Type, answers[i].Class, answers[i].TtlValue, answers[i].RDATA) { Tag = answers[i].Tag }; - - answers = wildcardAnswers; - } - - switch (request.Question[0].Type) - { - case DnsResourceRecordType.NS: - authority = null; - additional = GetAdditionalRecords(answers); - break; - - case DnsResourceRecordType.ANY: - authority = null; - additional = null; - break; - - default: - authority = authZone.QueryRecords(DnsResourceRecordType.NS); - additional = GetAdditionalRecords(authority); - break; - } - } - - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, true, false, request.RecursionDesired, false, false, false, DnsResponseCode.NoError, request.Question, answers, authority, additional); - } - - //unknown zone type encountered - return new DnsDatagram(request.Identifier, true, DnsOpcode.StandardQuery, false, false, request.RecursionDesired, false, false, false, DnsResponseCode.NotImplemented, request.Question); - } - - public void LoadZoneFrom(Stream s) - { - BinaryReader bR = new BinaryReader(s); - - if (Encoding.ASCII.GetString(bR.ReadBytes(2)) != "DZ") - throw new InvalidDataException("DnsServer zone file format is invalid."); - - switch (bR.ReadByte()) - { - case 2: - { - DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; - if (records.Length > 0) - { - for (int i = 0; i < records.Length; i++) - records[i] = new DnsResourceRecord(s); - - //make zone info - AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, AuthZoneType.Primary, false); - - //create zone - CreateZone(zoneInfo); - - //set records - SetRecords(records); - } - } - break; - - case 3: - { - bool zoneDisabled = bR.ReadBoolean(); - DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; - if (records.Length > 0) - { - for (int i = 0; i < records.Length; i++) - { - records[i] = new DnsResourceRecord(s); - records[i].Tag = new DnsResourceRecordInfo(bR); - } - - //make zone info - AuthZoneInfo zoneInfo = new AuthZoneInfo(records[0].Name, AuthZoneType.Primary, zoneDisabled); - - //create zone - CreateZone(zoneInfo); - - //set records - SetRecords(records); - } - } - break; - - case 4: - { - //read zone info - AuthZoneInfo zoneInfo = new AuthZoneInfo(bR); - - //create zone - CreateZone(zoneInfo); - - //read all zone records - DnsResourceRecord[] records = new DnsResourceRecord[bR.ReadInt32()]; - if (records.Length > 0) - { - for (int i = 0; i < records.Length; i++) - { - records[i] = new DnsResourceRecord(s); - records[i].Tag = new DnsResourceRecordInfo(bR); - } - - //set records - SetRecords(records); - } - } - break; - - default: - throw new InvalidDataException("DNS Zone file version not supported."); - } - } - - public void WriteZoneTo(string domain, Stream s) - { - List zones = _root.GetZoneWithSubDomainZones(domain); - if (zones.Count == 0) - throw new DnsServerException("Zone was not found: " + domain); - - //serialize zone - BinaryWriter bW = new BinaryWriter(s); - - bW.Write(Encoding.ASCII.GetBytes("DZ")); //format - bW.Write((byte)4); //version - - //write zone info - AuthZoneInfo zoneInfo = new AuthZoneInfo(zones[0]); - zoneInfo.WriteTo(bW); - - //write all zone records - List records = new List(); - - foreach (AuthZone zone in zones) - records.AddRange(zone.ListAllRecords()); - - bW.Write(records.Count); - - foreach (DnsResourceRecord record in records) - { - record.WriteTo(s); - - DnsResourceRecordInfo rrInfo = record.Tag as DnsResourceRecordInfo; - if (rrInfo == null) - rrInfo = new DnsResourceRecordInfo(); //default info - - rrInfo.WriteTo(bW); - } - } - - #endregion - } -}