/*
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.Collections.Generic;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsRebindingProtection
{
public sealed class App : IDnsApplication, IDnsPostProcessor
{
#region variables
bool _enableProtection;
HashSet _privateNetworks;
HashSet _privateDomains;
#endregion
#region IDisposable
public void Dispose()
{
// Nothing to dispose of.
}
#endregion
#region private
private static string GetParentZone(string domain)
{
int i = domain.IndexOf('.');
if (i > -1)
return domain[(i + 1)..];
//dont return root zone
return null;
}
private bool IsPrivateDomain(string domain)
{
domain = domain.ToLowerInvariant();
do
{
if (_privateDomains.Contains(domain))
return true;
domain = GetParentZone(domain);
}
while (domain is not null);
return false;
}
private bool IsRebindingAttempt(DnsResourceRecord record)
{
IPAddress address;
switch (record.Type)
{
case DnsResourceRecordType.A:
if (IsPrivateDomain(record.Name))
return false;
address = (record.RDATA as DnsARecordData).Address;
break;
case DnsResourceRecordType.AAAA:
if (IsPrivateDomain(record.Name))
return false;
address = (record.RDATA as DnsAAAARecordData).Address;
break;
default:
return false;
}
foreach (NetworkAddress networkAddress in _privateNetworks)
{
if (networkAddress.Contains(address))
return true;
}
return false;
}
private bool TryDetectRebinding(IReadOnlyList answer, out List protectedAnswer)
{
for (int i = 0; i < answer.Count; i++)
{
DnsResourceRecord record = answer[i];
if (IsRebindingAttempt(record))
{
//rebinding attempt detected!
//prepare protected answer
protectedAnswer = new List(answer.Count);
//copy passed records
for (int j = 0; j < i; j++)
protectedAnswer.Add(answer[j]);
//copy remaining records with check
for (int j = i + 1; j < answer.Count; j++)
{
record = answer[j];
if (!IsRebindingAttempt(record))
protectedAnswer.Add(record);
}
return true;
}
}
protectedAnswer = null;
return false;
}
#endregion
#region public
public Task InitializeAsync(IDnsServer dnsServer, string config)
{
using JsonDocument jsonDocument = JsonDocument.Parse(config);
JsonElement jsonConfig = jsonDocument.RootElement;
_enableProtection = jsonConfig.GetPropertyValue("enableProtection", true);
_privateNetworks = new HashSet(jsonConfig.ReadArray("privateNetworks", NetworkAddress.Parse));
_privateDomains = new HashSet(jsonConfig.ReadArray("privateDomains"));
return Task.CompletedTask;
}
public Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
{
// Do not filter authoritative responses. Because in this case any rebinding is intentional.
if (!_enableProtection || response.AuthoritativeAnswer)
return Task.FromResult(response);
if (TryDetectRebinding(response.Answer, out List protectedAnswer))
return Task.FromResult(response.Clone(protectedAnswer));
return Task.FromResult(response);
}
#endregion
#region properties
public string Description
{ get { return "Protects from DNS rebinding attacks using configured private domains and networks."; } }
#endregion
}
}