diff --git a/Apps/DnsRebindBlockingApp/App.cs b/Apps/DnsRebindBlockingApp/App.cs deleted file mode 100644 index b981cfc4..00000000 --- a/Apps/DnsRebindBlockingApp/App.cs +++ /dev/null @@ -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 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(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 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 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; - } - } -} \ No newline at end of file diff --git a/Apps/DnsRebindBlockingApp/AppConfig.cs b/Apps/DnsRebindBlockingApp/AppConfig.cs deleted file mode 100644 index fdc0e4b1..00000000 --- a/Apps/DnsRebindBlockingApp/AppConfig.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace DnsRebindBlocking; - -public class AppConfig -{ - public required bool Enabled { get; set; } - public required List PrivateNetworks { get; init; } = new(); - public required HashSet PrivateDomains { get; init; } = new(); -} \ No newline at end of file diff --git a/Apps/DnsRebindBlockingApp/DnsRebindBlockingApp.csproj b/Apps/DnsRebindBlockingApp/DnsRebindBlockingApp.csproj deleted file mode 100644 index 5f3edd9c..00000000 --- a/Apps/DnsRebindBlockingApp/DnsRebindBlockingApp.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - net7.0 - false - true - 1.0.0 - Technitium - Technitium DNS Server - Shreyas Zare, Rui Fung Yip - DnsRebindBlocking - https://technitium.com/dns/ - https://github.com/TechnitiumSoftware/DnsServer - Blocks DNS rebinding attacks using configured private domains and networks. - false - Library - enable - - - - - false - - - - - - ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll - false - - - ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll - false - - - - - - PreserveNewest - - - - diff --git a/Apps/DnsRebindingProtectionApp/App.cs b/Apps/DnsRebindingProtectionApp/App.cs new file mode 100644 index 00000000..f3b938c5 --- /dev/null +++ b/Apps/DnsRebindingProtectionApp/App.cs @@ -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 . + +*/ + +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 + } +} diff --git a/Apps/DnsRebindBlockingApp/dnsApp.config b/Apps/DnsRebindingProtectionApp/dnsApp.config similarity index 80% rename from Apps/DnsRebindBlockingApp/dnsApp.config rename to Apps/DnsRebindingProtectionApp/dnsApp.config index 6e50535e..4f7035e0 100644 --- a/Apps/DnsRebindBlockingApp/dnsApp.config +++ b/Apps/DnsRebindingProtectionApp/dnsApp.config @@ -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",