From 96686f06ff4ae6120866a8bfa2fabb591a77b793 Mon Sep 17 00:00:00 2001 From: Shreyas Zare Date: Sat, 26 Nov 2022 11:47:05 +0530 Subject: [PATCH] SplitHorizon: Implemented AddressTranslation feature. --- Apps/SplitHorizonApp/AddressTranslation.cs | 402 +++++++++++++++++++++ Apps/SplitHorizonApp/SimpleAddress.cs | 54 ++- Apps/SplitHorizonApp/dnsApp.config | 37 +- 3 files changed, 483 insertions(+), 10 deletions(-) create mode 100644 Apps/SplitHorizonApp/AddressTranslation.cs diff --git a/Apps/SplitHorizonApp/AddressTranslation.cs b/Apps/SplitHorizonApp/AddressTranslation.cs new file mode 100644 index 00000000..3491e1fb --- /dev/null +++ b/Apps/SplitHorizonApp/AddressTranslation.cs @@ -0,0 +1,402 @@ +/* +Technitium DNS Server +Copyright (C) 2022 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 Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using TechnitiumLibrary.Net; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace SplitHorizon +{ + public class AddressTranslation : IDnsApplication, IDnsPostProcessor, IDnsAuthoritativeRequestHandler + { + #region variables + + bool _enableAddressTranslation; + IReadOnlyDictionary _networkGroupMap; + IReadOnlyDictionary _groups; + + #endregion + + #region IDisposable + + public void Dispose() + { + //do nothing + } + + #endregion + + #region public + + public async Task InitializeAsync(IDnsServer dnsServer, string config) + { + if (string.IsNullOrEmpty(config) || config.StartsWith('#')) + { + //replace old config with default config + config = """ +{ + "networks": { + "custom-networks": [ + "172.16.1.0/24", + "172.16.10.0/24", + "172.16.2.1" + ] + }, + "enableAddressTranslation": false, + "networkGroupMap": { + "10.0.0.0/8": "local1", + "172.16.0.0/12": "local2", + "192.168.0.0/16": "local3" + }, + "groups": [ + { + "name": "local1", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "10.0.0.4", + "5.6.7.8": "10.0.0.5" + } + }, + { + "name": "local2", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "172.16.0.4", + "5.6.7.8": "172.16.0.5" + } + }, + { + "name": "local3", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "192.168.0.4", + "5.6.7.8": "192.168.0.5" + } + } + ] +} +"""; + + await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); + } + + dynamic jsonConfig = JsonConvert.DeserializeObject(config); + + if (jsonConfig.enableAddressTranslation is null) + { + //update old config with default config + config = config.TrimEnd(' ', '\t', '\r', '\n'); + config = config.Substring(0, config.Length - 1); + config = config.TrimEnd(' ', '\t', '\r', '\n'); + config += """ +, + "enableAddressTranslation": false, + "networkGroupMap": { + "10.0.0.0/8": "local1", + "172.16.0.0/12": "local2", + "192.168.0.0/16": "local3" + }, + "groups": [ + { + "name": "local1", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "10.0.0.4", + "5.6.7.8": "10.0.0.5" + } + }, + { + "name": "local2", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "172.16.0.4", + "5.6.7.8": "172.16.0.5" + } + }, + { + "name": "local3", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "192.168.0.4", + "5.6.7.8": "192.168.0.5" + } + } + ] +} +"""; + await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); + + jsonConfig = JsonConvert.DeserializeObject(config); + } + + _enableAddressTranslation = jsonConfig.enableAddressTranslation.Value; + + { + Dictionary networkGroupMap = new Dictionary(); + + foreach (dynamic jsonProperty in jsonConfig.networkGroupMap) + { + string network = jsonProperty.Name; + string group = jsonProperty.Value; + + if (!NetworkAddress.TryParse(network, out NetworkAddress networkAddress)) + throw new InvalidOperationException("Network group map contains an invalid network address: " + network); + + networkGroupMap.Add(networkAddress, group); + } + + _networkGroupMap = networkGroupMap; + } + + { + Dictionary groups = new Dictionary(); + + foreach (dynamic jsonGroup in jsonConfig.groups) + { + Group group = new Group(jsonGroup); + groups.Add(group.Name, group); + } + + _groups = groups; + } + + return; + } + + public Task PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response) + { + if (!_enableAddressTranslation) + return Task.FromResult(response); + + if (request.DnssecOk) + return Task.FromResult(response); + + if (response.RCODE != DnsResponseCode.NoError) + return Task.FromResult(response); + + DnsQuestionRecord question = request.Question[0]; + + switch (question.Type) + { + case DnsResourceRecordType.A: + case DnsResourceRecordType.AAAA: + break; + + default: + return Task.FromResult(response); + } + + if (response.Answer.Count == 0) + return Task.FromResult(response); + + IPAddress remoteIP = remoteEP.Address; + NetworkAddress network = null; + string groupName = null; + + foreach (KeyValuePair entry in _networkGroupMap) + { + if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) + { + network = entry.Key; + groupName = entry.Value; + } + } + + if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled) + return Task.FromResult(response); + + List newAnswer = new List(response.Answer.Count); + + foreach (DnsResourceRecord answer in response.Answer) + { + switch (answer.Type) + { + case DnsResourceRecordType.A: + { + IPAddress externalIp = (answer.RDATA as DnsARecordData).Address; + + if (group.ExternalToInternalTranslation.TryGetValue(externalIp, out IPAddress internalIp)) + newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TtlValue, new DnsARecordData(internalIp))); + else + newAnswer.Add(answer); + } + break; + + case DnsResourceRecordType.AAAA: + { + IPAddress externalIp = (answer.RDATA as DnsAAAARecordData).Address; + + if (group.ExternalToInternalTranslation.TryGetValue(externalIp, out IPAddress internalIp)) + newAnswer.Add(new DnsResourceRecord(answer.Name, answer.Type, answer.Class, answer.TtlValue, new DnsAAAARecordData(internalIp))); + else + newAnswer.Add(answer); + } + break; + + default: + newAnswer.Add(answer); + break; + } + } + + return Task.FromResult(response.Clone(newAnswer)); + } + + public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed) + { + if (!_enableAddressTranslation) + return Task.FromResult(null); + + if (request.DnssecOk) + return Task.FromResult(null); + + DnsQuestionRecord question = request.Question[0]; + if (question.Type != DnsResourceRecordType.PTR) + return Task.FromResult(null); + + IPAddress remoteIP = remoteEP.Address; + NetworkAddress network = null; + string groupName = null; + + foreach (KeyValuePair entry in _networkGroupMap) + { + if (entry.Key.Contains(remoteIP) && ((network is null) || (entry.Key.PrefixLength > network.PrefixLength))) + { + network = entry.Key; + groupName = entry.Value; + } + } + + if ((groupName is null) || !_groups.TryGetValue(groupName, out Group group) || !group.Enabled || !group.TranslateReverseLookups) + return Task.FromResult(null); + + IPAddress ptrIpAddress = IPAddressExtension.ParseReverseDomain(question.Name); + + if (!group.InternalToExternalTranslation.TryGetValue(ptrIpAddress, out IPAddress externalIp)) + return Task.FromResult(null); + + IReadOnlyList answer = new DnsResourceRecord[] { new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, question.Class, 600, new DnsCNAMERecordData(externalIp.GetReverseDomain())) }; + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answer)); + } + + #endregion + + #region properties + + public string Description + { get { return "Translates IP addresses in DNS response for A & AAAA type request based on the client's network address and the configured 1:1 translation. Also supports reverse (PTR) queries for translated addresses."; } } + + #endregion + + class Group + { + #region variables + + readonly string _name; + readonly bool _enabled; + readonly bool _translateReverseLookups; + readonly IReadOnlyDictionary _externalToInternalTranslation; + readonly IReadOnlyDictionary _internalToExternalTranslation; + + #endregion + + #region constructor + + public Group(dynamic jsonGroup) + { + _name = jsonGroup.name.Value; + _enabled = jsonGroup.enabled.Value; + _translateReverseLookups = jsonGroup.translateReverseLookups.Value; + + if (_translateReverseLookups) + { + Dictionary externalToInternalTranslation = new Dictionary(); + Dictionary internalToExternalTranslation = new Dictionary(); + + foreach (dynamic jsonProperty in jsonGroup.externalToInternalTranslation) + { + string strExternalIp = jsonProperty.Name; + string strInternalIp = jsonProperty.Value; + + IPAddress externalIp = IPAddress.Parse(strExternalIp); + IPAddress internalIp = IPAddress.Parse(strInternalIp); + + externalToInternalTranslation.TryAdd(externalIp, internalIp); + internalToExternalTranslation.TryAdd(internalIp, externalIp); + } + + _externalToInternalTranslation = externalToInternalTranslation; + _internalToExternalTranslation = internalToExternalTranslation; + } + else + { + Dictionary externalToInternalTranslation = new Dictionary(); + + foreach (dynamic jsonProperty in jsonGroup.externalToInternalTranslation) + { + string strExternalIp = jsonProperty.Name; + string strInternalIp = jsonProperty.Value; + + IPAddress externalIp = IPAddress.Parse(strExternalIp); + IPAddress internalIp = IPAddress.Parse(strInternalIp); + + externalToInternalTranslation.TryAdd(externalIp, internalIp); + } + + _externalToInternalTranslation = externalToInternalTranslation; + } + } + + #endregion + + #region properties + + public string Name + { get { return _name; } } + + public bool Enabled + { get { return _enabled; } } + + public bool TranslateReverseLookups + { get { return _translateReverseLookups; } } + + public IReadOnlyDictionary ExternalToInternalTranslation + { get { return _externalToInternalTranslation; } } + + public IReadOnlyDictionary InternalToExternalTranslation + { get { return _internalToExternalTranslation; } } + + #endregion + } + } +} diff --git a/Apps/SplitHorizonApp/SimpleAddress.cs b/Apps/SplitHorizonApp/SimpleAddress.cs index 447b6793..be1e5c62 100644 --- a/Apps/SplitHorizonApp/SimpleAddress.cs +++ b/Apps/SplitHorizonApp/SimpleAddress.cs @@ -52,19 +52,55 @@ namespace SplitHorizon public async Task InitializeAsync(IDnsServer dnsServer, string config) { - if (config.StartsWith('#')) + if (string.IsNullOrEmpty(config) || config.StartsWith('#')) { //replace old config with default config - config = @"{ - ""networks"": { - ""custom-networks"": [ - ""172.16.1.0/24"", - ""172.16.10.0/24"", - ""172.16.2.1"" + config = """ +{ + "networks": { + "custom-networks": [ + "172.16.1.0/24", + "172.16.10.0/24", + "172.16.2.1" ] - } + }, + "enableAddressTranslation": false, + "networkGroupMap": { + "10.0.0.0/8": "local1", + "172.16.0.0/12": "local2", + "192.168.0.0/16": "local3" + }, + "groups": [ + { + "name": "local1", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "10.0.0.4", + "5.6.7.8": "10.0.0.5" + } + }, + { + "name": "local2", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "172.16.0.4", + "5.6.7.8": "172.16.0.5" + } + }, + { + "name": "local3", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "192.168.0.4", + "5.6.7.8": "192.168.0.5" + } + } + ] } -"; +"""; await File.WriteAllTextAsync(Path.Combine(dnsServer.ApplicationFolder, "dnsApp.config"), config); } diff --git a/Apps/SplitHorizonApp/dnsApp.config b/Apps/SplitHorizonApp/dnsApp.config index e9fb633a..0c9c1889 100644 --- a/Apps/SplitHorizonApp/dnsApp.config +++ b/Apps/SplitHorizonApp/dnsApp.config @@ -5,5 +5,40 @@ "172.16.10.0/24", "172.16.2.1" ] - } + }, + "enableAddressTranslation": false, + "networkGroupMap": { + "10.0.0.0/8": "local1", + "172.16.0.0/12": "local2", + "192.168.0.0/16": "local3" + }, + "groups": [ + { + "name": "local1", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "10.0.0.4", + "5.6.7.8": "10.0.0.5" + } + }, + { + "name": "local2", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "172.16.0.4", + "5.6.7.8": "172.16.0.5" + } + }, + { + "name": "local3", + "enabled": true, + "translateReverseLookups": true, + "externalToInternalTranslation": { + "1.2.3.4": "192.168.0.4", + "5.6.7.8": "192.168.0.5" + } + } + ] }