Skip to content

Commit a97ef41

Browse files
committed
Add object‑based splitNetworks with samePrefix support and prefix‑length clamp
Signed-off-by: Denis Kudelin <[email protected]>
1 parent 1c8b46f commit a97ef41

File tree

2 files changed

+138
-14
lines changed

2 files changed

+138
-14
lines changed

Apps/SourceFilterApp/App.cs

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ private sealed class Rule
133133
private readonly NetworkSet include;
134134
private readonly string pattern;
135135
private readonly int specificity;
136-
private readonly NetworkSet split;
136+
private readonly SplitNetwork[] split;
137137
private readonly bool wildcard;
138138

139139
public Rule(JsonElement json) : this(
@@ -147,13 +147,11 @@ public Rule(string pattern, JsonElement jsonRule)
147147
{
148148
this.pattern = Normalize(pattern);
149149
this.wildcard = this.pattern == "*" || this.pattern.StartsWith("*.");
150-
this.specificity = this.wildcard
151-
? this.pattern == "*" ? 0 : this.pattern.Length - 2
152-
: this.pattern.Length;
150+
this.specificity = this.wildcard ? this.pattern == "*" ? 0 : this.pattern.Length - 2 : this.pattern.Length;
153151

154152
this.include = new(GetNetworks(jsonRule, true, "includeNetworks", "include"));
155153
this.exclude = new(GetNetworks(jsonRule, false, "excludeNetworks", "exclude"));
156-
this.split = new(GetNetworks(jsonRule, false, "splitNetworks"));
154+
this.split = GetSplitNetworks(jsonRule);
157155
}
158156

159157
private static List<NetworkAddress> GetNetworks(JsonElement json, bool addDefault, params string[] names)
@@ -211,9 +209,73 @@ public bool IsClientAllowed(IPAddress clientIp)
211209
return true;
212210
}
213211

212+
private static SplitNetwork[] GetSplitNetworks(JsonElement json)
213+
{
214+
if (!json.TryGetProperty("splitNetworks", out var value) || value.ValueKind != JsonValueKind.Array)
215+
return [];
216+
217+
var list = new List<SplitNetwork>();
218+
219+
foreach (var elem in value.EnumerateArray())
220+
{
221+
if (elem.ValueKind == JsonValueKind.String)
222+
{
223+
if (NetworkAddress.TryParse(elem.GetString(), out var net))
224+
list.Add(new SplitNetwork(net, null));
225+
}
226+
else if (elem.ValueKind == JsonValueKind.Object)
227+
{
228+
if (!elem.TryGetProperty("network", out var netProp) || netProp.ValueKind != JsonValueKind.String)
229+
continue;
230+
if (!NetworkAddress.TryParse(netProp.GetString(), out var net))
231+
continue;
232+
233+
int? samePrefix = null;
234+
if (elem.TryGetProperty("samePrefix", out var prefProp) && prefProp.ValueKind == JsonValueKind.Number)
235+
samePrefix = prefProp.GetInt32();
236+
237+
list.Add(new SplitNetwork(net, samePrefix));
238+
}
239+
}
240+
241+
return list.Count == 0 ? [] : list.ToArray();
242+
}
243+
244+
private static bool TryParseSplitNetwork(string str, out SplitNetwork split)
245+
{
246+
split = default;
247+
248+
if (string.IsNullOrWhiteSpace(str))
249+
return false;
250+
251+
var parts = str.Split('/');
252+
253+
if (parts.Length == 3)
254+
{
255+
var networkPart = $"{parts[0]}/{parts[1]}";
256+
257+
if (!NetworkAddress.TryParse(networkPart, out var net))
258+
return false;
259+
if (!int.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var samePrefix))
260+
return false;
261+
split = new SplitNetwork(net, samePrefix);
262+
263+
return true;
264+
}
265+
266+
if (NetworkAddress.TryParse(str, out var network))
267+
{
268+
split = new SplitNetwork(network, null);
269+
270+
return true;
271+
}
272+
273+
return false;
274+
}
275+
214276
public bool PassesSplit(IPAddress clientIp, DnsResourceRecord record)
215277
{
216-
if (this.split.IsEmpty)
278+
if (this.split.Length == 0)
217279
return true;
218280

219281
var recordIp = record switch
@@ -226,10 +288,61 @@ public bool PassesSplit(IPAddress clientIp, DnsResourceRecord record)
226288
if (recordIp is null)
227289
return true;
228290

229-
var clientInside = this.split.Contains(clientIp);
230-
var recordInside = this.split.Contains(recordIp);
291+
var clientInsideAny = false;
292+
var recordInsideAny = false;
293+
294+
foreach (var sn in this.split)
295+
{
296+
var clientInside = sn.Network.Contains(clientIp);
297+
var recordInside = sn.Network.Contains(recordIp);
298+
299+
if (clientInside && recordInside && sn.SamePrefix.HasValue && !IpPrefixEqual(clientIp, recordIp, sn.SamePrefix.Value))
300+
return false;
301+
302+
clientInsideAny |= clientInside;
303+
recordInsideAny |= recordInside;
304+
}
305+
306+
return clientInsideAny == recordInsideAny;
307+
}
308+
309+
private static bool IpPrefixEqual(IPAddress a, IPAddress b, int prefixBits)
310+
{
311+
var aBytes = a.GetAddressBytes();
312+
var bBytes = b.GetAddressBytes();
313+
314+
if (aBytes.Length != bBytes.Length || prefixBits < 0)
315+
return false;
316+
317+
var maxBits = aBytes.Length * 8;
318+
if (prefixBits > maxBits)
319+
prefixBits = maxBits;
320+
321+
var bits = prefixBits;
322+
323+
for (var i = 0; i < aBytes.Length && bits > 0; i++)
324+
{
325+
var take = bits >= 8 ? 8 : bits;
326+
var mask = (byte)(0xFF << (8 - take));
327+
328+
if ((aBytes[i] & mask) != (bBytes[i] & mask))
329+
return false;
330+
bits -= take;
331+
}
332+
333+
return true;
334+
}
335+
336+
private readonly struct SplitNetwork
337+
{
338+
public SplitNetwork(NetworkAddress network, int? samePrefix)
339+
{
340+
this.Network = network;
341+
this.SamePrefix = samePrefix;
342+
}
231343

232-
return clientInside == recordInside;
344+
public NetworkAddress Network { get; }
345+
public int? SamePrefix { get; }
233346
}
234347
}
235348

Apps/SourceFilterApp/dnsApp.config

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,39 @@
55
"pattern": "example.com",
66
"includeNetworks": ["0.0.0.0/0", "::/0"],
77
"excludeNetworks": ["10.0.0.0/8"],
8-
"splitNetworks": ["10.0.0.0/8"]
8+
"splitNetworks": [
9+
{ "network": "10.0.0.0/8", "samePrefix": 16 }
10+
]
911
},
1012
{
1113
"pattern": "*.example.com",
1214
"includeNetworks": ["0.0.0.0/0", "::/0"],
1315
"excludeNetworks": ["10.0.0.0/8"],
14-
"splitNetworks": ["10.0.0.0/8"]
16+
"splitNetworks": [
17+
{ "network": "10.0.0.0/8", "samePrefix": 16 }
18+
]
1519
},
1620
{
1721
"pattern": "internal.example.com",
1822
"includeNetworks": ["10.0.0.0/8"],
19-
"splitNetworks": ["10.0.0.0/8"]
23+
"splitNetworks": [
24+
{ "network": "10.0.0.0/8", "samePrefix": 16 }
25+
]
2026
},
2127
{
2228
"pattern": "dmz.example.com",
2329
"includeNetworks": ["192.168.0.0/16", "10.0.0.0/8"],
2430
"excludeNetworks": ["192.168.50.0/24"],
25-
"splitNetworks": ["192.168.0.0/16"]
31+
"splitNetworks": [
32+
{ "network": "192.168.0.0/16", "samePrefix": 24 }
33+
]
2634
},
2735
{
2836
"pattern": "*",
29-
"splitNetworks": ["10.0.0.0/8", "192.168.0.0/16"]
37+
"splitNetworks": [
38+
{ "network": "10.0.0.0/8", "samePrefix": 16 },
39+
{ "network": "192.168.0.0/16", "samePrefix": 24 }
40+
]
3041
}
3142
]
3243
}

0 commit comments

Comments
 (0)