DnsRebindingProtection: updated implementation code for optimization.

This commit is contained in:
Shreyas Zare
2024-02-04 17:01:47 +05:30
parent c540f15c51
commit a457f152d6
5 changed files with 182 additions and 164 deletions

View File

@@ -1,110 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using DnsServerCore.ApplicationCommon;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace DnsRebindBlocking
{
public class App: IDnsApplication, IDnsPostProcessor
{
private AppConfig Config = null!;
private HashSet<NetworkAddress> PrivateNetworks = new();
private IDnsServer DnsServer = null!;
public void Dispose()
{
// Nothing to dispose of.
}
public Task InitializeAsync(IDnsServer dnsServer, string config)
{
DnsServer = dnsServer;
Config = JsonSerializer.Deserialize<AppConfig>(config, new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
})!;
DnsServer.WriteLog($"Initializing. Enabled: {Config.Enabled}");
PrivateNetworks.Clear();
foreach (var privateNetwork in Config.PrivateNetworks)
{
var success = NetworkAddress.TryParse(privateNetwork, out var networkAddress);
if (!success)
DnsServer.WriteLog($"Invalid network address specified: {privateNetwork}");
else
PrivateNetworks.Add(networkAddress);
}
// Add the ServerDomain to the PrivateDomains list so it doesn't block it's own.
Config.PrivateDomains.Add(DnsServer.ServerDomain);
return Task.CompletedTask;
}
public string Description => "Block DNS responses with protected IP ranges to prevent DNS rebinding attacks.";
public Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
{
// Do not filter authoritative responses. Because in this case any rebinding is intentional.
if (!Config.Enabled || response.AuthoritativeAnswer)
return Task.FromResult(response);
var answers = response.Answer.Where(res => !IsFilteredRebind(res)).ToList();
var additional = response.Additional.Where(res => !IsFilteredRebind(res)).ToList();
return Task.FromResult(response.Clone(answers, response.Authority, additional));
}
private bool IsFilteredRebind(DnsResourceRecord record)
{
if (record.Type != DnsResourceRecordType.A && record.Type != DnsResourceRecordType.AAAA)
return false;
IPAddress address;
switch (record.RDATA)
{
case DnsARecordData data:
address = data.Address;
break;
case DnsAAAARecordData data:
address = data.Address;
break;
default:
return false;
}
var isPrivateNetwork = PrivateNetworks.Any(net => net.Contains(address));
var isPrivateDomain = IsZoneFound(Config.PrivateDomains, record.Name, out _);
return isPrivateNetwork && !isPrivateDomain;
}
private static string? GetParentZone(string domain)
{
var i = domain.IndexOf('.');
//dont return root zone
return i > -1 ? domain[(i + 1)..] : null;
}
private static bool IsZoneFound(IReadOnlySet<string> domains, string domain, out string? foundZone)
{
var currentDomain = domain.ToLower();
do
{
if (domains.Contains(currentDomain))
{
foundZone = currentDomain;
return true;
}
currentDomain = GetParentZone(currentDomain);
}
while (currentDomain is not null);
foundZone = null;
return false;
}
}
}

View File

@@ -1,10 +0,0 @@
using System.Collections.Generic;
namespace DnsRebindBlocking;
public class AppConfig
{
public required bool Enabled { get; set; }
public required List<string> PrivateNetworks { get; init; } = new();
public required HashSet<string> PrivateDomains { get; init; } = new();
}

View File

@@ -1,43 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Version>1.0.0</Version>
<Company>Technitium</Company>
<Product>Technitium DNS Server</Product>
<Authors>Shreyas Zare, Rui Fung Yip</Authors>
<RootNamespace>DnsRebindBlocking</RootNamespace>
<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>
<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>
<Description>Blocks DNS rebinding attacks using configured private domains and networks.</Description>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<OutputType>Library</OutputType>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\DnsServerCore.ApplicationCommon\DnsServerCore.ApplicationCommon.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Reference Include="TechnitiumLibrary">
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="TechnitiumLibrary.Net">
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="dnsApp.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,180 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
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<NetworkAddress> _privateNetworks;
HashSet<string> _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<DnsResourceRecord> answer, out List<DnsResourceRecord> 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<DnsResourceRecord>(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<NetworkAddress>(jsonConfig.ReadArray("privateNetworks", NetworkAddress.Parse));
_privateDomains = new HashSet<string>(jsonConfig.ReadArray("privateDomains"));
return Task.CompletedTask;
}
public Task<DnsDatagram> 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<DnsResourceRecord> 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
}
}

View File

@@ -1,7 +1,8 @@
{
"enabled": false,
"enableProtection": true,
"privateNetworks": [
"10.0.0.0/8",
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16",