/*
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;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using System.Threading.Tasks;
using TechnitiumLibrary;
using TechnitiumLibrary.Net;
using TechnitiumLibrary.Net.Dns;
using TechnitiumLibrary.Net.Dns.ResourceRecords;
namespace SplitHorizon
{
public sealed class SimpleAddress : IDnsApplication, IDnsAppRecordRequestHandler
{
#region variables
static Dictionary> _networks;
#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);
}
using JsonDocument jsonDocument = JsonDocument.Parse(config);
JsonElement jsonConfig = jsonDocument.RootElement;
if (jsonConfig.TryGetProperty("networks", out JsonElement jsonNetworks))
{
Dictionary> networks = new Dictionary>();
foreach (JsonProperty jsonProperty in jsonNetworks.EnumerateObject())
{
string networkName = jsonProperty.Name;
JsonElement jsonNetworkAddresses = jsonProperty.Value;
if (jsonNetworkAddresses.ValueKind == JsonValueKind.Array)
{
List networkAddresses = new List(jsonNetworkAddresses.GetArrayLength());
foreach (JsonElement jsonNetworkAddress in jsonNetworkAddresses.EnumerateArray())
networkAddresses.Add(NetworkAddress.Parse(jsonNetworkAddress.GetString()));
networks.TryAdd(networkName, networkAddresses);
}
}
_networks = networks;
}
else
{
_networks = new Dictionary>(1);
}
}
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) && !appRecordName.StartsWith('*'))
return Task.FromResult(null);
switch (question.Type)
{
case DnsResourceRecordType.A:
case DnsResourceRecordType.AAAA:
using (JsonDocument jsonDocument = JsonDocument.Parse(appRecordData))
{
JsonElement jsonAppRecordData = jsonDocument.RootElement;
JsonElement jsonAddresses = default;
NetworkAddress selectedNetwork = null;
foreach (JsonProperty jsonProperty in jsonAppRecordData.EnumerateObject())
{
string name = jsonProperty.Name;
if ((name == "public") || (name == "private"))
continue;
if (_networks.TryGetValue(name, out List networkAddresses))
{
foreach (NetworkAddress networkAddress in networkAddresses)
{
if (networkAddress.Contains(remoteEP.Address))
{
jsonAddresses = jsonProperty.Value;
break;
}
}
if (jsonAddresses.ValueKind != JsonValueKind.Undefined)
break;
}
else if (NetworkAddress.TryParse(name, out NetworkAddress networkAddress))
{
if (networkAddress.Contains(remoteEP.Address) && ((selectedNetwork is null) || (networkAddress.PrefixLength > selectedNetwork.PrefixLength)))
{
selectedNetwork = networkAddress;
jsonAddresses = jsonProperty.Value;
}
}
}
if (jsonAddresses.ValueKind == JsonValueKind.Undefined)
{
if (NetUtilities.IsPrivateIP(remoteEP.Address))
{
if (!jsonAppRecordData.TryGetProperty("private", out jsonAddresses))
return Task.FromResult(null);
}
else
{
if (!jsonAppRecordData.TryGetProperty("public", out jsonAddresses))
return Task.FromResult(null);
}
}
List answers = new List();
switch (question.Type)
{
case DnsResourceRecordType.A:
foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())
{
if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetwork))
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.A, DnsClass.IN, appRecordTtl, new DnsARecordData(address)));
}
break;
case DnsResourceRecordType.AAAA:
foreach (JsonElement jsonAddress in jsonAddresses.EnumerateArray())
{
if (IPAddress.TryParse(jsonAddress.GetString(), out IPAddress address) && (address.AddressFamily == AddressFamily.InterNetworkV6))
answers.Add(new DnsResourceRecord(question.Name, DnsResourceRecordType.AAAA, DnsClass.IN, appRecordTtl, new DnsAAAARecordData(address)));
}
break;
}
if (answers.Count == 0)
return Task.FromResult(null);
if (answers.Count > 1)
answers.Shuffle();
return Task.FromResult(new DnsDatagram(request.Identifier, true, request.OPCODE, true, false, request.RecursionDesired, isRecursionAllowed, false, false, DnsResponseCode.NoError, request.Question, answers));
}
default:
return Task.FromResult(null);
}
}
#endregion
#region properties
internal static Dictionary> Networks
{ get { return _networks; } }
public string Description
{ get { return "Returns A or AAAA records with different set of IP addresses for clients querying over public, private, or other specified networks."; } }
public string ApplicationRecordDataTemplate
{
get
{
return @"{
""public"": [
""1.1.1.1"",
""2.2.2.2""
],
""private"": [
""192.168.1.1"",
""::1""
],
""custom-networks"": [
""172.16.1.1""
],
""10.0.0.0/8"": [
""10.1.1.1""
]
}";
}
}
#endregion
}
}