/*
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.ApplicationCommon;
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace ZoneAlias
{
public sealed class App : IDnsApplication, IDnsAuthoritativeRequestHandler
{
#region variables
IDnsServer _dnsServer;
bool _enableAliasing;
Dictionary _aliases;
#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 IsZoneAlias(string domain, out string zone, out string alias)
{
domain = domain.ToLowerInvariant();
do
{
if (_aliases.TryGetValue(domain, out zone))
{
//found alias
alias = domain;
return true;
}
domain = GetParentZone(domain);
}
while (domain is not null);
alias = null;
return false;
}
private static IReadOnlyList ConvertRecords(IReadOnlyList records, string zone, string alias)
{
if (records.Count == 0)
return records;
DnsResourceRecord[] newRecords = new DnsResourceRecord[records.Count];
int j;
for (int i = 0; i < records.Count; i++)
{
DnsResourceRecord record = records[i];
j = record.Name.LastIndexOf(zone, StringComparison.OrdinalIgnoreCase);
if (j == 0)
newRecords[i] = new DnsResourceRecord(alias, record.Type, record.Class, record.TTL, record.RDATA);
else if (j > 0)
newRecords[i] = new DnsResourceRecord(string.Concat(record.Name.AsSpan(0, j), alias), record.Type, record.Class, record.TTL, record.RDATA);
else
newRecords[i] = record;
}
return newRecords;
}
#endregion
#region public
public Task InitializeAsync(IDnsServer dnsServer, string config)
{
_dnsServer = dnsServer;
using JsonDocument jsonDocument = JsonDocument.Parse(config);
JsonElement jsonConfig = jsonDocument.RootElement;
_enableAliasing = jsonConfig.GetPropertyValue("enableAliasing", true);
if (jsonConfig.TryGetProperty("zoneAliases", out JsonElement jsonZoneAliases))
{
Dictionary aliases = new Dictionary();
foreach (JsonProperty jsonZoneAlias in jsonZoneAliases.EnumerateObject())
{
string zone = jsonZoneAlias.Name.ToLowerInvariant();
foreach (JsonElement jsonAlias in jsonZoneAlias.Value.EnumerateArray())
aliases.Add(jsonAlias.GetString().ToLowerInvariant(), zone);
}
aliases.TrimExcess();
_aliases = aliases;
}
else
{
_aliases = null;
}
return Task.CompletedTask;
}
public async Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed)
{
if (!_enableAliasing || (_aliases is null))
return null;
DnsQuestionRecord question = request.Question[0];
string qname = question.Name;
if (!IsZoneAlias(qname, out string zone, out string alias))
return null;
string newQname;
int i = qname.LastIndexOf(alias, StringComparison.OrdinalIgnoreCase);
if (i == 0)
newQname = zone;
else if (i > 0)
newQname = string.Concat(qname.AsSpan(0, i), zone);
else
return null;
DnsQuestionRecord newQuestion = new DnsQuestionRecord(newQname, question.Type, question.Class);
try
{
DnsDatagram response = await _dnsServer.DirectQueryAsync(newQuestion);
IReadOnlyList newAnswer = ConvertRecords(response.Answer, zone, alias);
IReadOnlyList newAuthority = ConvertRecords(response.Authority, zone, alias);
IReadOnlyList newAdditional = ConvertRecords(response.Additional, zone, alias);
return new DnsDatagram(request.Identifier, true, request.OPCODE, response.AuthoritativeAnswer, response.Truncation, request.RecursionDesired, isRecursionAllowed, false, false, response.RCODE, request.Question, newAnswer, newAuthority, newAdditional) { Tag = response.Tag };
}
catch (TimeoutException)
{ }
catch (Exception ex)
{
_dnsServer.WriteLog(ex);
}
return new DnsDatagram(request.Identifier, true, request.OPCODE, false, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.ServerFailure, request.Question);
}
#endregion
#region properties
public string Description
{ get { return "Allows configuring aliases for any zone (internal or external) such that they all return the same set of records."; } }
#endregion
}
}