Skip to content

Commit df0a5f1

Browse files
committed
Add SourceFilterApp
Signed-off-by: Denis Kudelin <[email protected]>
1 parent 767f764 commit df0a5f1

File tree

3 files changed

+316
-0
lines changed

3 files changed

+316
-0
lines changed

Apps/SourceFilterApp/App.cs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Text.Json;
6+
using System.Threading.Tasks;
7+
using DnsServerCore.ApplicationCommon;
8+
using TechnitiumLibrary.Net;
9+
using TechnitiumLibrary.Net.Dns;
10+
using TechnitiumLibrary.Net.Dns.ResourceRecords;
11+
12+
namespace SourceFilterApp;
13+
14+
public sealed class App : IDnsApplication, IDnsPostProcessor
15+
{
16+
#region IDisposable
17+
18+
public void Dispose() { }
19+
20+
#endregion
21+
22+
#region properties
23+
24+
public string Description => "Filters answer records by client network according to include/exclude rules and optional splitNetworks.";
25+
26+
#endregion
27+
28+
#region private
29+
30+
private Rule GetRule(string name)
31+
{
32+
Rule best = null;
33+
var bestScore = -1;
34+
35+
foreach (var rule in this.rules)
36+
{
37+
var score = rule.Match(name);
38+
39+
if (score <= bestScore)
40+
continue;
41+
bestScore = score;
42+
best = rule;
43+
}
44+
45+
return best;
46+
}
47+
48+
#endregion
49+
50+
#region variables
51+
52+
private bool enabled;
53+
private List<Rule> rules;
54+
55+
#endregion
56+
57+
#region public
58+
59+
public Task InitializeAsync(IDnsServer dnsServer, string config)
60+
{
61+
this.rules = [];
62+
63+
if (string.IsNullOrEmpty(config))
64+
{
65+
this.enabled = false;
66+
return Task.CompletedTask;
67+
}
68+
69+
using (var json = JsonDocument.Parse(config))
70+
{
71+
var root = json.RootElement;
72+
this.enabled = !root.TryGetProperty("enabled", out var jsonEnabled) || jsonEnabled.GetBoolean();
73+
74+
if (root.TryGetProperty("rules", out var jsonRules) && jsonRules.ValueKind == JsonValueKind.Array)
75+
foreach (var jsonRule in jsonRules.EnumerateArray())
76+
this.rules.Add(new(jsonRule));
77+
else
78+
foreach (var prop in root.EnumerateObject().Where(prop => !prop.NameEquals("enabled")))
79+
this.rules.Add(new(prop.Name, prop.Value));
80+
}
81+
82+
return Task.CompletedTask;
83+
}
84+
85+
public Task<DnsDatagram> PostProcessAsync(DnsDatagram request, IPEndPoint remoteEP, DnsTransportProtocol protocol, DnsDatagram response)
86+
{
87+
if (!this.enabled)
88+
return Task.FromResult(response);
89+
90+
if (response.Answer.Count == 0)
91+
return Task.FromResult(response);
92+
93+
var clientIp = remoteEP.Address;
94+
var answer = new List<DnsResourceRecord>(response.Answer.Count);
95+
96+
foreach (var record in response.Answer)
97+
{
98+
var rule = this.GetRule(record.Name);
99+
if (rule is null)
100+
{
101+
answer.Add(record);
102+
continue;
103+
}
104+
105+
if (!rule.IsClientAllowed(clientIp))
106+
continue;
107+
108+
if (rule.PassesSplit(clientIp, record))
109+
answer.Add(record);
110+
}
111+
112+
if (answer.Count == response.Answer.Count)
113+
return Task.FromResult(response);
114+
115+
if (answer.Count == 0)
116+
return Task.FromResult(response.Clone([]));
117+
118+
return Task.FromResult(response.Clone(answer));
119+
}
120+
121+
#endregion
122+
123+
#region inner
124+
125+
private sealed class Rule
126+
{
127+
private readonly NetworkSet include;
128+
private readonly NetworkSet exclude;
129+
private readonly NetworkSet split;
130+
private readonly string pattern;
131+
private readonly int specificity;
132+
private readonly bool wildcard;
133+
134+
public Rule(JsonElement json) : this(
135+
(json.TryGetProperty("pattern", out var jsonPattern) ? jsonPattern.GetString() : null) ?? "*",
136+
json) { }
137+
138+
public Rule(string pattern, JsonElement jsonRule)
139+
{
140+
this.pattern = pattern.ToLowerInvariant();
141+
this.wildcard = this.pattern == "*" || this.pattern.StartsWith("*.");
142+
this.specificity = this.wildcard
143+
? this.pattern == "*" ? 0 : this.pattern.Length - 2
144+
: this.pattern.Length;
145+
146+
this.include = new(GetNetworks(jsonRule, true, "includeNetworks", "include"));
147+
this.exclude = new(GetNetworks(jsonRule, false, "excludeNetworks", "exclude"));
148+
this.split = new(GetNetworks(jsonRule, false, "splitNetworks"));
149+
}
150+
151+
private static List<NetworkAddress> GetNetworks(JsonElement json, bool addDefault, params string[] names)
152+
{
153+
var list = new List<NetworkAddress>();
154+
155+
foreach (var n in names)
156+
{
157+
if (!json.TryGetProperty(n, out var value) || value.ValueKind != JsonValueKind.Array)
158+
continue;
159+
160+
foreach (var str in value.EnumerateArray().Select(x => x.GetString()))
161+
if (NetworkAddress.TryParse(str, out var addr))
162+
list.Add(addr);
163+
}
164+
165+
if (addDefault && list.Count == 0)
166+
{
167+
list.Add(NetworkAddress.Parse("0.0.0.0/0"));
168+
list.Add(NetworkAddress.Parse("::/0"));
169+
}
170+
171+
return list;
172+
}
173+
174+
public int Match(string name)
175+
{
176+
name = name.ToLowerInvariant();
177+
178+
if (this.pattern == "*")
179+
return 0;
180+
181+
if (this.wildcard)
182+
{
183+
if (!name.EndsWith(this.pattern[1..], StringComparison.OrdinalIgnoreCase))
184+
return -1;
185+
if (name.Length == this.specificity)
186+
return -1;
187+
188+
return this.specificity;
189+
}
190+
191+
return name.Equals(this.pattern, StringComparison.OrdinalIgnoreCase)
192+
? this.specificity
193+
: -1;
194+
}
195+
196+
public bool IsClientAllowed(IPAddress clientIp)
197+
{
198+
if (!this.include.Contains(clientIp))
199+
return false;
200+
if (this.exclude.Contains(clientIp))
201+
return false;
202+
return true;
203+
}
204+
205+
public bool PassesSplit(IPAddress clientIp, DnsResourceRecord record)
206+
{
207+
if (this.split.IsEmpty)
208+
return true;
209+
210+
IPAddress recordIp = record.Type switch
211+
{
212+
DnsResourceRecordType.A => (record.RDATA as DnsARecordData).Address,
213+
DnsResourceRecordType.AAAA => (record.RDATA as DnsAAAARecordData).Address,
214+
_ => null
215+
};
216+
217+
if (recordIp is null)
218+
return true;
219+
220+
var clientInside = this.split.Contains(clientIp);
221+
var recordInside = this.split.Contains(recordIp);
222+
223+
return clientInside == recordInside;
224+
}
225+
}
226+
227+
private sealed class NetworkSet
228+
{
229+
private readonly NetworkAddress[] nets;
230+
231+
public bool IsEmpty => this.nets.Length == 0;
232+
233+
public NetworkSet(IReadOnlyList<NetworkAddress> nets) => this.nets = nets.Count == 0 ? [] : nets.ToArray();
234+
235+
public bool Contains(IPAddress ip)
236+
{
237+
foreach (var net in this.nets)
238+
if (net.Contains(ip))
239+
return true;
240+
return false;
241+
}
242+
}
243+
244+
#endregion
245+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
6+
<Version>1.0</Version>
7+
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
8+
<Company>Itexoft</Company>
9+
<Product>Technitium DNS Server</Product>
10+
<Authors>Denis Kudelin</Authors>
11+
<AssemblyName>SourceFilterApp</AssemblyName>
12+
<RootNamespace>SourceFilterApp</RootNamespace>
13+
<PackageProjectUrl>https://technitium.com/dns/</PackageProjectUrl>
14+
<RepositoryUrl>https://github.com/TechnitiumSoftware/DnsServer</RepositoryUrl>
15+
<Description>Filters DNS response records based on client network include and exclude rules.</Description>
16+
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
17+
<OutputType>Library</OutputType>
18+
</PropertyGroup>
19+
20+
<ItemGroup>
21+
<ProjectReference Include="..\..\DnsServerCore.ApplicationCommon\DnsServerCore.ApplicationCommon.csproj">
22+
<Private>false</Private>
23+
</ProjectReference>
24+
</ItemGroup>
25+
26+
<ItemGroup>
27+
<Reference Include="TechnitiumLibrary.Net">
28+
<HintPath>..\..\..\TechnitiumLibrary\bin\TechnitiumLibrary.Net.dll</HintPath>
29+
<Private>false</Private>
30+
</Reference>
31+
</ItemGroup>
32+
33+
<ItemGroup>
34+
<None Update="dnsApp.config">
35+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
36+
</None>
37+
</ItemGroup>
38+
39+
</Project>

Apps/SourceFilterApp/dnsApp.config

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"enabled": true,
3+
"rules": [
4+
{
5+
"pattern": "example.com",
6+
"includeNetworks": ["0.0.0.0/0", "::/0"],
7+
"excludeNetworks": ["10.0.0.0/8"],
8+
"splitNetworks": ["10.0.0.0/8"]
9+
},
10+
{
11+
"pattern": "*.example.com",
12+
"includeNetworks": ["0.0.0.0/0", "::/0"],
13+
"excludeNetworks": ["10.0.0.0/8"],
14+
"splitNetworks": ["10.0.0.0/8"]
15+
},
16+
{
17+
"pattern": "internal.example.com",
18+
"includeNetworks": ["10.0.0.0/8"],
19+
"splitNetworks": ["10.0.0.0/8"]
20+
},
21+
{
22+
"pattern": "dmz.example.com",
23+
"includeNetworks": ["192.168.0.0/16", "10.0.0.0/8"],
24+
"excludeNetworks": ["192.168.50.0/24"],
25+
"splitNetworks": ["192.168.0.0/16"]
26+
},
27+
{
28+
"pattern": "*",
29+
"splitNetworks": ["10.0.0.0/8", "192.168.0.0/16"]
30+
}
31+
]
32+
}

0 commit comments

Comments
 (0)