Skip to content

Commit fcc2e3d

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 fcc2e3d

File tree

2 files changed

+135
-14
lines changed

2 files changed

+135
-14
lines changed

Apps/SourceFilterApp/App.cs

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

232-
return clientInside == recordInside;
341+
public NetworkAddress Network { get; }
342+
public int? SamePrefix { get; }
233343
}
234344
}
235345

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)