diff --git a/Apps/WeightedRoundRobinApp/Address.cs b/Apps/WeightedRoundRobinApp/Address.cs new file mode 100644 index 00000000..2fc30853 --- /dev/null +++ b/Apps/WeightedRoundRobinApp/Address.cs @@ -0,0 +1,196 @@ +/* +Technitium DNS Server +Copyright (C) 2023 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.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace WeightedRoundRobin +{ + public class Address : IDnsApplication, IDnsAppRecordRequestHandler + { + #region IDisposable + + public void Dispose() + { + //do nothing + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + return Task.CompletedTask; + } + + public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) + { + DnsQuestionRecord question = request.Question[0]; + + if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase)) + return Task.FromResult(null); + + string jsonPropertyName; + + switch (question.Type) + { + case DnsResourceRecordType.A: + jsonPropertyName = "ipv4Addresses"; + break; + + case DnsResourceRecordType.AAAA: + jsonPropertyName = "ipv6Addresses"; + break; + + default: + return Task.FromResult(null); + } + + List addresses; + int totalWeight = 0; + + using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) + { + JsonElement jsonAppRecordData = jsonDocument.RootElement; + + if (!jsonAppRecordData.TryGetProperty(jsonPropertyName, out JsonElement jsonAddresses) || (jsonAddresses.ValueKind == JsonValueKind.Null)) + return Task.FromResult(null); + + addresses = new List(jsonAddresses.GetArrayLength()); + + foreach (JsonElement jsonAddressEntry in jsonAddresses.EnumerateArray()) + { + if (jsonAddressEntry.TryGetProperty("enabled", out JsonElement jsonEnabled) && (jsonEnabled.ValueKind != JsonValueKind.Null) && !jsonEnabled.GetBoolean()) + continue; + + if (!jsonAddressEntry.TryGetProperty("address", out JsonElement jsonAddress) || (jsonAddress.ValueKind == JsonValueKind.Null) || !IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address)) + continue; + + if (!jsonAddressEntry.TryGetProperty("weight", out JsonElement jsonWeight) || (jsonWeight.ValueKind == JsonValueKind.Null)) + continue; + + int weight = jsonWeight.GetInt32(); + if (weight < 1) + continue; + + addresses.Add(new WeightedAddress() { Address = address, Weight = weight }); + totalWeight += weight; + } + } + + if (addresses.Count == 0) + return Task.FromResult(null); + + int randomSelection = RandomNumberGenerator.GetInt32(1, 101); + int rangeFrom; + int rangeTo = 0; + DnsResourceRecord answer = null; + + for (int i = 0; i < addresses.Count; i++) + { + rangeFrom = rangeTo + 1; + + if (i == addresses.Count - 1) + rangeTo = 100; + else + rangeTo += addresses[i].Weight * 100 / totalWeight; + + if ((rangeFrom <= randomSelection) && (randomSelection <= rangeTo)) + { + switch (question.Type) + { + case DnsResourceRecordType.A: + answer = new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, appRecordTtl, new DnsARecordData(addresses[i].Address)); + break; + + case DnsResourceRecordType.AAAA: + answer = new DnsResourceRecord(question.Name, question.Type, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(addresses[i].Address)); + break; + + default: + throw new InvalidOperationException(); + } + + break; + } + } + + if (answer is null) + throw new InvalidOperationException(); + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); + } + + #endregion + + #region properties + + public string Description + { get { return "Returns an A or AAAA record using weighted round-robin load balancing."; } } + + public string ApplicationRecordDataTemplate + { + get + { + return @"{ + ""ipv4Addresses"": [ + { + ""address"": ""1.1.1.1"", + ""weight"": 5, + ""enabled"": true + }, + { + ""address"": ""2.2.2.2"", + ""weight"": 3, + ""enabled"": true + } + ], + ""ipv6Addresses"": [ + { + ""address"": ""::1"", + ""weight"": 2, + ""enabled"": true + }, + { + ""address"": ""::2"", + ""weight"": 3, + ""enabled"": true + } + ] +}"; + } + } + + #endregion + + struct WeightedAddress + { + public IPAddress Address; + public int Weight; + } + } +} diff --git a/Apps/WeightedRoundRobinApp/CNAME.cs b/Apps/WeightedRoundRobinApp/CNAME.cs new file mode 100644 index 00000000..fbf9f840 --- /dev/null +++ b/Apps/WeightedRoundRobinApp/CNAME.cs @@ -0,0 +1,159 @@ +/* +Technitium DNS Server +Copyright (C) 2023 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.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; +using TechnitiumLibrary.Net.Dns; +using TechnitiumLibrary.Net.Dns.ResourceRecords; + +namespace WeightedRoundRobin +{ + public class CNAME : IDnsApplication, IDnsAppRecordRequestHandler + { + #region IDisposable + + public void Dispose() + { + //do nothing + } + + #endregion + + #region public + + public Task InitializeAsync(IDnsServer dnsServer, string config) + { + return Task.CompletedTask; + } + + public Task ProcessRequestAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, bool isRecursionAllowed, string zoneName, string appRecordName, uint appRecordTtl, string appRecordData) + { + DnsQuestionRecord question = request.Question[0]; + + if (!question.Name.Equals(appRecordName, StringComparison.OrdinalIgnoreCase)) + return Task.FromResult(null); + + List domainNames; + int totalWeight = 0; + + using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData)) + { + JsonElement jsonAppRecordData = jsonDocument.RootElement; + + if (!jsonAppRecordData.TryGetProperty("cnames", out JsonElement jsonCnames) || (jsonCnames.ValueKind == JsonValueKind.Null)) + return Task.FromResult(null); + + domainNames = new List(jsonCnames.GetArrayLength()); + + foreach (JsonElement jsonCnameEntry in jsonCnames.EnumerateArray()) + { + if (jsonCnameEntry.TryGetProperty("enabled", out JsonElement jsonEnabled) && (jsonEnabled.ValueKind != JsonValueKind.Null) && !jsonEnabled.GetBoolean()) + continue; + + if (!jsonCnameEntry.TryGetProperty("domain", out JsonElement jsonDomain) || (jsonDomain.ValueKind == JsonValueKind.Null)) + continue; + + if (!jsonCnameEntry.TryGetProperty("weight", out JsonElement jsonWeight) || (jsonWeight.ValueKind == JsonValueKind.Null)) + continue; + + int weight = jsonWeight.GetInt32(); + if (weight < 1) + continue; + + domainNames.Add(new WeightedDomain() { Domain = jsonDomain.GetString(), Weight = weight }); + totalWeight += weight; + } + } + + if (domainNames.Count == 0) + return Task.FromResult(null); + + int randomSelection = RandomNumberGenerator.GetInt32(1, 101); + int rangeFrom; + int rangeTo = 0; + DnsResourceRecord answer = null; + + for (int i = 0; i < domainNames.Count; i++) + { + rangeFrom = rangeTo + 1; + + if (i == domainNames.Count - 1) + rangeTo = 100; + else + rangeTo += domainNames[i].Weight * 100 / totalWeight; + + if ((rangeFrom <= randomSelection) && (randomSelection <= rangeTo)) + { + if (question.Name.Equals(zoneName, StringComparison.OrdinalIgnoreCase)) //check for zone apex + answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.ANAME, DnsClass.IN, appRecordTtl, new DnsANAMERecordData(domainNames[i].Domain)); //use ANAME + else + answer = new DnsResourceRecord(question.Name, DnsResourceRecordType.CNAME, DnsClass.IN, appRecordTtl, new DnsCNAMERecordData(domainNames[i].Domain)); + + break; + } + } + + if (answer is null) + throw new InvalidOperationException(); + + return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, new DnsResourceRecord[] { answer })); + } + + #endregion + + #region properties + + public string Description + { get { return "Returns a CNAME record using weighted round-robin load balancing."; } } + + public string ApplicationRecordDataTemplate + { + get + { + return @"{ + ""cnames"": [ + { + ""domain"": ""example.com"", + ""weight"": 5, + ""enabled"": true + }, + { + ""domain"": ""example.net"", + ""weight"": 3, + ""enabled"": true + } + ] +}"; + } + } + + #endregion + + struct WeightedDomain + { + public string Domain; + public int Weight; + } + } +} diff --git a/Apps/WeightedRoundRobinApp/WeightedRoundRobinApp.csproj b/Apps/WeightedRoundRobinApp/WeightedRoundRobinApp.csproj new file mode 100644 index 00000000..f5594fa8 --- /dev/null +++ b/Apps/WeightedRoundRobinApp/WeightedRoundRobinApp.csproj @@ -0,0 +1,42 @@ + + + + net7.0 + false + 1.0 + Technitium + Technitium DNS Server + Shreyas Zare + WeightedRoundRobinApp + WeightedRoundRobin + https://technitium.com/dns/ + https://github.com/TechnitiumSoftware/DnsServer + Allows creating APP records in a primary and forwarder zones that can return A or AAAA records, or CNAME record using weighted round-robin load balancing. + false + Library + + + + + false + + + + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.dll + false + + + ..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll + false + + + + + + PreserveNewest + + + + diff --git a/Apps/WeightedRoundRobinApp/dnsApp.config b/Apps/WeightedRoundRobinApp/dnsApp.config new file mode 100644 index 00000000..7a2450c3 --- /dev/null +++ b/Apps/WeightedRoundRobinApp/dnsApp.config @@ -0,0 +1 @@ +#This app requires no config. \ No newline at end of file