/*
Technitium DNS Server
Copyright (C) 2024 Shreyas Zare (shreyas@technitium.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
using DnsServerCore.Dns.ResourceRecords;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using TechnitiumLibrary;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsServerCore.Dns.Zones
{
class CatalogZone : ForwarderZone
{
#region variables
readonly Dictionary _membersIndex = new Dictionary();
readonly ReaderWriterLockSlim _membersIndexLock = new ReaderWriterLockSlim();
#endregion
#region constructor
public CatalogZone(DnsServer dnsServer, AuthZoneInfo zoneInfo)
: base(dnsServer, zoneInfo)
{ }
public CatalogZone(DnsServer dnsServer, string name)
: base(dnsServer, name)
{ }
#endregion
#region IDisposable
protected override void Dispose(bool disposing)
{
try
{
_membersIndexLock.Dispose();
}
finally
{
base.Dispose(disposing);
}
}
#endregion
#region internal
internal override void InitZone()
{
//init catalog zone with dummy SOA and NS records
DnsSOARecordData soa = new DnsSOARecordData("invalid", "invalid", 1, 300, 60, 604800, 900);
DnsResourceRecord soaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, soa);
soaRecord.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;
_entries[DnsResourceRecordType.SOA] = [soaRecord];
_entries[DnsResourceRecordType.NS] = [new DnsResourceRecord(_name, DnsResourceRecordType.NS, DnsClass.IN, 0, new DnsNSRecordData("invalid"))];
}
internal void InitZoneProperties()
{
//set catalog zone version record
_dnsServer.AuthZoneManager.SetRecord(_name, new DnsResourceRecord("version." + _name, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData("2")));
//init catalog global properties
QueryAccess = AuthZoneQueryAccess.Allow;
ZoneTransfer = AuthZoneTransfer.Deny;
}
internal void BuildMembersIndex()
{
foreach (KeyValuePair memberEntry in EnumerateCatalogMemberZones(_dnsServer))
_membersIndex.TryAdd(memberEntry.Key.ToLowerInvariant(), memberEntry.Value);
}
#endregion
#region catalog
public void AddMemberZone(string memberZoneName, AuthZoneType zoneType)
{
memberZoneName = memberZoneName.ToLowerInvariant();
_membersIndexLock.EnterWriteLock();
try
{
if (_membersIndex.TryGetValue(memberZoneName, out _))
{
if (_membersIndex.Remove(memberZoneName, out string removedMemberZoneDomain))
{
foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, removedMemberZoneDomain, true))
_dnsServer.AuthZoneManager.DeleteRecord(_name, record);
}
}
string memberZoneDomain = GetDomainWithLabel("zones." + _name);
DateTime utcNow = DateTime.UtcNow;
DnsResourceRecord ptrRecord = new DnsResourceRecord(memberZoneDomain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(memberZoneName));
ptrRecord.GetAuthGenericRecordInfo().LastModified = utcNow;
DnsResourceRecord txtRecord = new DnsResourceRecord("zone-type.ext." + memberZoneDomain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(zoneType.ToString().ToLowerInvariant()));
txtRecord.GetAuthGenericRecordInfo().LastModified = utcNow;
_dnsServer.AuthZoneManager.AddRecord(_name, ptrRecord);
_dnsServer.AuthZoneManager.AddRecord(_name, txtRecord);
_membersIndex[memberZoneName] = memberZoneDomain;
}
finally
{
_membersIndexLock.ExitWriteLock();
}
}
public bool RemoveMemberZone(string memberZoneName)
{
memberZoneName = memberZoneName.ToLowerInvariant();
_membersIndexLock.EnterWriteLock();
try
{
if (_membersIndex.Remove(memberZoneName, out string removedMemberZoneDomain))
{
foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, removedMemberZoneDomain, true))
_dnsServer.AuthZoneManager.DeleteRecord(_name, record);
return true;
}
return false;
}
finally
{
_membersIndexLock.ExitWriteLock();
}
}
public void ChangeMemberZoneOwnership(string memberZoneName, string newCatalogZoneName)
{
string memberZoneDomain = GetMemberZoneDomain(memberZoneName);
string domain = "coo." + memberZoneDomain;
DateTime utcNow = DateTime.UtcNow;
uint soaExpiry = GetZoneSoaExpire();
//add COO record with expiry
DnsResourceRecord cooRecord = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(newCatalogZoneName));
GenericRecordInfo cooRecordInfo = cooRecord.GetAuthGenericRecordInfo();
cooRecordInfo.LastModified = utcNow;
cooRecordInfo.ExpiryTtl = soaExpiry;
_dnsServer.AuthZoneManager.SetRecord(_name, cooRecord);
//set expiry for other member zone records
foreach (DnsResourceRecord record in _dnsServer.AuthZoneManager.EnumerateAllRecords(_name, memberZoneDomain, true))
{
GenericRecordInfo recordInfo = record.GetAuthGenericRecordInfo();
recordInfo.LastModified = utcNow;
recordInfo.ExpiryTtl = soaExpiry;
}
}
public IReadOnlyCollection GetAllMemberZoneNames()
{
_membersIndexLock.EnterReadLock();
try
{
return _membersIndex.Keys.ToArray();
}
finally
{
_membersIndexLock.ExitReadLock();
}
}
public void SetAllowQueryProperty(IReadOnlyCollection acl = null, string memberZoneName = null)
{
string domain = "allow-query.ext." + GetMemberZoneDomain(memberZoneName);
if (acl is null)
{
_dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.APL);
}
else
{
DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.APL, DnsClass.IN, 0, NetworkAccessControl.ConvertToAPLRecordData(acl));
record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;
_dnsServer.AuthZoneManager.SetRecord(_name, record);
}
}
public void SetAllowTransferProperty(IReadOnlyCollection acl = null, string memberZoneName = null)
{
string domain = "allow-transfer.ext." + GetMemberZoneDomain(memberZoneName);
if (acl is null)
{
_dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.APL);
}
else
{
DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.APL, DnsClass.IN, 0, NetworkAccessControl.ConvertToAPLRecordData(acl));
record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;
_dnsServer.AuthZoneManager.SetRecord(_name, record);
}
}
public void SetZoneTransferTsigKeyNamesProperty(IReadOnlyDictionary tsigKeyNames = null, string memberZoneName = null)
{
string domain = "transfer-tsig-key-names.ext." + GetMemberZoneDomain(memberZoneName);
if (tsigKeyNames is null)
{
_dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.PTR);
}
else
{
DnsResourceRecord[] records = new DnsResourceRecord[tsigKeyNames.Count];
int i = 0;
foreach (KeyValuePair entry in tsigKeyNames)
{
DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.PTR, DnsClass.IN, 0, new DnsPTRRecordData(entry.Key));
record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;
records[i++] = record;
}
_dnsServer.AuthZoneManager.SetRecords(_name, records);
}
}
public void SetPrimaryAddressesProperty(IReadOnlyList primaryServerAddresses = null, string memberZoneName = null)
{
string domain = "primary-addresses.ext." + GetMemberZoneDomain(memberZoneName);
if (primaryServerAddresses is null)
{
_dnsServer.AuthZoneManager.DeleteRecords(_name, domain, DnsResourceRecordType.TXT);
}
else
{
IReadOnlyList charStrings = primaryServerAddresses.Convert(delegate (NameServerAddress nameServer)
{
return nameServer.ToString();
});
DnsResourceRecord record = new DnsResourceRecord(domain, DnsResourceRecordType.TXT, DnsClass.IN, 0, new DnsTXTRecordData(charStrings));
record.GetAuthGenericRecordInfo().LastModified = DateTime.UtcNow;
_dnsServer.AuthZoneManager.SetRecord(_name, record);
}
}
private string GetMemberZoneDomain(string memberZoneName = null)
{
if (memberZoneName is null)
{
return _name;
}
else
{
memberZoneName = memberZoneName.ToLowerInvariant();
_membersIndexLock.EnterReadLock();
try
{
if (!_membersIndex.TryGetValue(memberZoneName, out string memberZoneDomain))
throw new DnsServerException("Failed to find '" + memberZoneName + "' member zone entry in '" + ToString() + "' Catalog zone: member zone does not exists.");
return memberZoneDomain;
}
finally
{
_membersIndexLock.ExitReadLock();
}
}
}
private string GetDomainWithLabel(string domain)
{
Span buffer = stackalloc byte[8];
int i = 0;
do
{
RandomNumberGenerator.Fill(buffer);
string label = Base32.ToBase32HexString(buffer, true).ToLowerInvariant();
string domainWithLabel = label + "." + domain;
if (_dnsServer.AuthZoneManager.NameExists(_name, domainWithLabel))
continue;
return domainWithLabel;
}
while (++i < 10);
throw new DnsServerException("Failed to generate unique label for the given domain name '" + domain + "'. Please try again.");
}
#endregion
#region public
public override string GetZoneTypeName()
{
return "Catalog";
}
public override void SetRecords(DnsResourceRecordType type, IReadOnlyList records)
{
switch (type)
{
case DnsResourceRecordType.SOA:
if ((records.Count != 1) || !records[0].Name.Equals(_name, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("Invalid SOA record.");
DnsResourceRecord newSoaRecord = records[0];
DnsSOARecordData newSoa = newSoaRecord.RDATA as DnsSOARecordData;
//reset fixed record values
DnsSOARecordData modifiedSoa = new DnsSOARecordData("invalid", "invalid", newSoa.Serial, newSoa.Refresh, newSoa.Retry, newSoa.Expire, newSoa.Minimum);
DnsResourceRecord modifiedSoaRecord = new DnsResourceRecord(_name, DnsResourceRecordType.SOA, DnsClass.IN, 0, modifiedSoa) { Tag = newSoaRecord.Tag };
base.SetRecords(type, [modifiedSoaRecord]);
break;
default:
throw new InvalidOperationException("Cannot set records in Catalog zone.");
}
}
public override void AddRecord(DnsResourceRecord record)
{
throw new InvalidOperationException("Cannot add record in Catalog zone.");
}
public override bool DeleteRecords(DnsResourceRecordType type)
{
throw new InvalidOperationException("Cannot delete record in Catalog zone.");
}
public override bool DeleteRecord(DnsResourceRecordType type, DnsResourceRecordData record)
{
throw new InvalidOperationException("Cannot delete records in Catalog zone.");
}
public override void UpdateRecord(DnsResourceRecord oldRecord, DnsResourceRecord newRecord)
{
throw new InvalidOperationException("Cannot update record in Catalog zone.");
}
public override IReadOnlyList QueryRecords(DnsResourceRecordType type, bool dnssecOk)
{
return []; //catalog zone is not queriable
}
#endregion
#region properties
public override string CatalogZoneName
{
get { return base.CatalogZoneName; }
set { throw new InvalidOperationException(); }
}
public override bool OverrideCatalogQueryAccess
{
get { return base.OverrideCatalogQueryAccess; }
set { throw new InvalidOperationException(); }
}
public override bool OverrideCatalogZoneTransfer
{
get { return base.OverrideCatalogZoneTransfer; }
set { throw new InvalidOperationException(); }
}
public override bool OverrideCatalogNotify
{
get { return base.OverrideCatalogNotify; }
set { throw new InvalidOperationException(); }
}
public override AuthZoneUpdate Update
{
get { return base.Update; }
set { throw new InvalidOperationException(); }
}
#endregion
}
}