Skip to content

Commit 2ad90c9

Browse files
authored
feat/networkawarecidrguess (#357)
* Feat: NetworkAware CidrGuess / Impl and test * Feat: NetworkAware CidrGuess / Doc * Chore: NetworkAware CidrGuess / Upgrade version to 3.2
1 parent 84cc98e commit 2ad90c9

File tree

8 files changed

+253
-8
lines changed

8 files changed

+253
-8
lines changed

README.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,16 +318,40 @@ IPV6 : /64
318318
IPV4 : /32
319319
IPV6 : /128
320320

321+
322+
#### NetworkAware
323+
324+
IPV4 :
325+
326+
Rule of thumb
327+
• Ends with .0 → /24
328+
• Ends with .0.0 or .255.255 → /16
329+
• Ends with .0.0.0 or .255.255.255 → /8
330+
• Else → /32
331+
332+
333+
IPV6 :
334+
335+
Rule of thumb
336+
• Ends with :0000 → /112
337+
• Ends with :0000:0000 → /96
338+
• Ends with three trailing :0000 → /80
339+
• …
340+
• Ends with four trailing :0000 → /64
341+
• Else → /128
342+
321343
#### IPv4
322344

323345
```C#
324346
IPNetwork2 defaultParse= IPNetwork2.Parse("192.168.0.0"); // default to ClassFull
325347
IPNetwork2 classFullParse = IPNetwork2.Parse("192.168.0.0", CidrGuess.ClassFull);
326348
IPNetwork2 classLessParse = IPNetwork2.Parse("192.168.0.0", CidrGuess.ClassLess);
349+
IPNetwork2 networkAwareParse = IPNetwork2.Parse("192.168.0.0", CidrGuess.NetworkAware);
327350

328351
Console.WriteLine("IPV4 Default Parse : {0}", defaultStrategy);
329352
Console.WriteLine("IPV4 ClassFull Parse : {0}", classFullParse);
330353
Console.WriteLine("IPV4 ClassLess Parse : {0}", classLessParse);
354+
Console.WriteLine("IPV4 NetworkAware Parse : {0}", networkAwareParse);
331355
```
332356

333357
Output
@@ -336,6 +360,7 @@ Output
336360
IPV4 Default Parse : 192.168.0.0/24
337361
IPV4 ClassFull Parse : 192.168.0.0/24
338362
IPV4 ClassLess Parse : 192.168.0.0/32
363+
IPV4 NetworkAware Parse : 192.168.0.0/16
339364
```
340365

341366
#### IPv6
@@ -344,10 +369,12 @@ IPV4 ClassLess Parse : 192.168.0.0/32
344369
IPNetwork2 defaultParse = IPNetwork2.Parse("::1"); // default to ClassFull
345370
IPNetwork2 classFullParse = IPNetwork2.Parse("::1", CidrGuess.ClassFull);
346371
IPNetwork2 classLessParse = IPNetwork2.Parse("::1", CidrGuess.ClassLess);
372+
IPNetwork2 networkAwareParse = IPNetwork2.Parse("::1", CidrGuess.NetworkAware);
347373

348374
Console.WriteLine("IPV6 Default Parse : {0}", defaultParse);
349375
Console.WriteLine("IPV6 ClassFull Parse : {0}", classFullParse);
350376
Console.WriteLine("IPV6 ClassLess Parse : {0}", classLessParse);
377+
Console.WriteLine("IPV6 NetworkAware Parse : {0}", networkAwareParse);
351378
```
352379

353380
Output
@@ -356,6 +383,7 @@ Output
356383
IPV6 Default Parse : ::/64
357384
IPV6 ClassFull Parse : ::/64
358385
IPV6 ClassLess Parse : ::1/128
386+
IPV6 ClassLess Parse : ::1/128
359387
```
360388

361389
---
@@ -423,7 +451,7 @@ Below some examples :
423451
```JS
424452
Provide at least one ipnetwork
425453
Usage: ipnetwork [-inmcbflu] [-d cidr|-D] [-h|-s cidr|-S|-w|-W|-x|-C network|-o network] networks ..
426-
Version: 3.1.0
454+
Version: 3.2.0
427455

428456
Print options
429457
-i : network

src/ConsoleApplication/ConsoleApplication.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<ImplicitUsings>disable</ImplicitUsings>
66
<SignAssembly>True</SignAssembly>
77
<AssemblyOriginatorKeyFile>..\System.Net.IPNetwork.snk</AssemblyOriginatorKeyFile>
8-
<Version>3.1.0</Version>
8+
<Version>3.2.0</Version>
99
<RootNamespace>System.Net</RootNamespace>
1010
<LangVersion>latestmajor</LangVersion>
1111
<GenerateDocumentationFile>true</GenerateDocumentationFile>

src/System.Net.IPNetwork/CidrGuess.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ namespace System.Net;
1010
public static class CidrGuess
1111
{
1212
/// <summary>
13-
/// Gets classFull guess.
13+
/// Gets classFull guesser.
1414
/// </summary>
1515
public static ICidrGuess ClassFull { get => CidrClassfull.Value; }
1616

1717
/// <summary>
18-
/// Gets classLess guess.
18+
/// Gets classLess guesser.
1919
/// </summary>
2020
public static ICidrGuess ClassLess { get => CidrClassless.Value; }
2121

22+
23+
/// <summary>
24+
/// Gets a NetworkAware guesser.
25+
/// </summary>
26+
public static ICidrGuess NetworkAware { get => CidrNetworkAware.Value; }
27+
2228
private static readonly Lazy<ICidrGuess> CidrClassless = new (() => new CidrClassLess());
2329
private static readonly Lazy<ICidrGuess> CidrClassfull = new (() => new CidrClassFull());
30+
private static readonly Lazy<ICidrGuess> CidrNetworkAware = new (() => new CidrNetworkAware());
2431
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using System.Net.Sockets;
2+
3+
namespace System.Net;
4+
5+
/// <summary>
6+
/// If your CidrGuess is “network-aware” based only on what humans usually encode in the textual address, a good heuristic is:
7+
///
8+
/// IPv4 (dotted-quad)
9+
/// • Treat trailing 0s as “network bits” and trailing 255s as a “wildcard” hint for the same boundary.
10+
/// • Otherwise, fall back to /32 (no safe aggregation from the string alone).
11+
/// • Special case: 0.0.0.0 (or 255.255.255.255) → /0.
12+
///
13+
/// Rule of thumb
14+
/// Ends with .0 → /24
15+
/// Ends with .0.0 or .255.255 → /16
16+
/// Ends with .0.0.0 or .255.255.255 → /8
17+
/// Else → /32
18+
///
19+
/// Matches your examples
20+
/// Parse("192.0.43.8") → /32
21+
/// Parse("192.0.43.0") → /24
22+
/// Parse("192.43.0.0") → /16
23+
/// Parse("192.0.43.255") → /24 (wildcard hint)
24+
/// Parse("192.43.255.255") → /16 (wildcard hint)
25+
///
26+
/// So: in a network-aware context like this, you generally don’t emit /25, /26, etc.,
27+
/// because there’s no reliable visual cue for those in dotted-quad—stick to /32, /24, /16, /8, /0.
28+
///
29+
/// IPv6 (colon-hex)
30+
/// IPv6 is grouped by hextets (16-bit chunks), so mirror the idea at 16-bit boundaries.
31+
/// Use trailing :0000 hextets as “network bits”.
32+
/// Otherwise, fall back to /128.
33+
/// Note: operationally, /64 is the standard host subnet size, but you should still infer from the string, not assumptions.
34+
///
35+
/// Rule of thumb
36+
/// Ends with :0000 → /112
37+
/// Ends with :0000:0000 → /96
38+
/// Ends with three trailing :0000 → /80
39+
/// …
40+
/// Ends with four trailing :0000 → /64
41+
/// Else → /128
42+
///
43+
/// Examples
44+
/// 2001:db8:1:2:3:4:5:6 → /128
45+
/// 2001:db8:1:2:3:4:5:0000 → /112
46+
/// 2001:db8:1:2:3:4:0000:0000 → /96
47+
/// 2001:db8:1:2:3:0000:0000:0000 → /80
48+
/// 2001:db8:1:2:0000:0000:0000:0000 → /64
49+
///
50+
/// TL;DR
51+
/// IPv4: stick to /32, /24, /16, /8, /0 based on trailing .0/.255; otherwise /32.
52+
/// IPv6: infer /128, /112, /96, /80, /64, … based on trailing :0000 groups; otherwise /128.
53+
/// </summary>
54+
public sealed class CidrNetworkAware : ICidrGuess
55+
{
56+
/// <summary>
57+
/// Tries to guess a network-aware CIDR prefix length from a textual IP address.
58+
/// IPv4: honors trailing 0s (network) and trailing 255s (wildcard hint) at octet boundaries.
59+
/// IPv6: honors trailing :0000 at hextet (16-bit) boundaries. Optional trailing :ffff wildcard heuristic is off by default.
60+
/// </summary>
61+
/// <param name="ip">IP address as string (no slash). Example: "192.0.43.0" or "2001:db8::".</param>
62+
/// <param name="cidr">Guessed CIDR (0..32 for IPv4, 0..128 for IPv6).</param>
63+
/// <returns>true if parsed and guessed; false if input is not a valid IP address.</returns>
64+
public bool TryGuessCidr(string ip, out byte cidr)
65+
{
66+
cidr = 0;
67+
if (string.IsNullOrWhiteSpace(ip))
68+
return false;
69+
70+
// Reject if user passed a slash - this API expects a plain address.
71+
// (You can relax this if you want to honor an explicitly supplied prefix.)
72+
if (ip.Contains("/"))
73+
return false;
74+
75+
if (!IPAddress.TryParse(ip.Trim(), out var ipAddress))
76+
return false;
77+
78+
if (ipAddress.AddressFamily == AddressFamily.InterNetwork)
79+
{
80+
cidr = GuessIpv4(ipAddress);
81+
return true;
82+
}
83+
else if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
84+
{
85+
cidr = GuessIpv6(ipAddress);
86+
return true;
87+
}
88+
89+
return false;
90+
}
91+
92+
private static byte GuessIpv4(IPAddress ip)
93+
{
94+
byte[] b = ip.GetAddressBytes(); // length 4
95+
96+
// /0 if all 0s or all 255s
97+
bool allZero = b[0] == 0 && b[1] == 0 && b[2] == 0 && b[3] == 0;
98+
bool allFf = b[0] == 255 && b[1] == 255 && b[2] == 255 && b[3] == 255;
99+
if (allZero || allFf) return 0;
100+
101+
// Network-aware boundaries via trailing zeros (network) OR trailing 255 (wildcard hint)
102+
bool last3Zero = b[1] == 0 && b[2] == 0 && b[3] == 0;
103+
bool last2Zero = b[2] == 0 && b[3] == 0;
104+
bool last1Zero = b[3] == 0;
105+
106+
bool last3Ff = b[1] == 255 && b[2] == 255 && b[3] == 255;
107+
bool last2Ff = b[2] == 255 && b[3] == 255;
108+
bool last1Ff = b[3] == 255;
109+
110+
if (last3Zero || last3Ff) return 8;
111+
if (last2Zero || last2Ff) return 16;
112+
if (last1Zero || last1Ff) return 24;
113+
114+
// Otherwise host address
115+
return 32;
116+
}
117+
118+
private static byte GuessIpv6(IPAddress ip)
119+
{
120+
byte[] b = ip.GetAddressBytes(); // length 16
121+
122+
// Count trailing zero hextets (pairs of bytes == 0x0000)
123+
int trailingZeroHextets = CountTrailingHextets(b, 0x0000);
124+
if (trailingZeroHextets == 8) return 0; // all zero address '::'
125+
if (trailingZeroHextets > 0) return (byte)(128 - 16 * trailingZeroHextets);
126+
127+
// Otherwise host address
128+
return 128;
129+
}
130+
131+
private static int CountTrailingHextets(byte[] bytes, ushort value)
132+
{
133+
// bytes.Length must be 16 for IPv6
134+
int count = 0;
135+
for (int i = bytes.Length - 2; i >= 0; i -= 2)
136+
{
137+
ushort hextet = (ushort)((bytes[i] << 8) | bytes[i + 1]);
138+
if (hextet == value) count++;
139+
else break;
140+
}
141+
return count;
142+
}
143+
}

src/System.Net.IPNetwork/Properties/AssemblyInfo.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@
3939
// You can specify all the values or you can default the Build and Revision Numbers
4040
// by using the '*' as shown below:
4141
// [assembly: AssemblyVersion("1.0.*")]
42-
[assembly: AssemblyVersion("3.1.0.0")]
43-
[assembly: AssemblyFileVersion("3.1.0.0")]
42+
[assembly: AssemblyVersion("3.2.0.0")]
43+
[assembly: AssemblyFileVersion("3.2.0.0")]
4444
[assembly: InternalsVisibleTo("TestProject, PublicKey=00240000048000009400000006020000002400005253413100040000010001004d29ae79cfcf603de0200afc96f4d8304aa857341b78e706fedb3f0ac9c9d613443cea78a1ee687def573ad45b5cdc0abeeb1db304eec7c07331015d8aeeb3fd5e092273a2347e6cb54803a00484807c64bc3092f17619abfc5290133efad358a27747bfe71d1dc23b461d7cf91272844fc7a8390dc63b16236729dadb2c21bc")]

src/System.Net.IPNetwork/System.Net.IPNetwork.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<TargetFrameworks>net9.0;net8.0;netstandard2.0;netstandard2.1</TargetFrameworks>
55
<GenerateDocumentationFile>true</GenerateDocumentationFile>
66
<PackageId>IPNetwork2</PackageId>
7-
<PackageVersion>3.1.0</PackageVersion>
7+
<PackageVersion>3.2.0</PackageVersion>
88
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
99
<PackageProjectUrl>https://github.com/lduchosal/ipnetwork</PackageProjectUrl>
1010
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// <copyright file="CidrNetworkAwareUnitTest.cs" company="IPNetwork">
2+
// Copyright (c) IPNetwork. All rights reserved.
3+
// </copyright>
4+
5+
#nullable enable
6+
namespace TestProject;
7+
8+
/// <summary>
9+
/// Test.
10+
/// </summary>
11+
[TestClass]
12+
public class CidrNetworkAwareUnitTest
13+
{
14+
/// <summary>
15+
/// Test.
16+
/// </summary>
17+
[TestMethod]
18+
[DataRow(null, 0, false)]
19+
[DataRow("null", 0, false)]
20+
[DataRow("invalidcidr", 0, false)]
21+
[DataRow("in.va.lid.ci.dr", 0, false)]
22+
[DataRow("de:ad:be:ef::", 064)]
23+
[DataRow("0::", 0)]
24+
[DataRow("1::", 16)]
25+
[DataRow("::1", 128)]
26+
27+
// IPv4 patterns
28+
[DataRow("10.0.0.0", 8)]
29+
[DataRow("0.0.0.0", 0)]
30+
[DataRow("172.0.0.0", 8)]
31+
[DataRow("192.0.0.0", 8)]
32+
[DataRow("224.0.0.0", 8)]
33+
[DataRow("240.0.0.0", 8)]
34+
[DataRow("192.0.43.8", 32)]
35+
[DataRow("192.0.43.0", 24)]
36+
[DataRow("192.43.0.0", 16)]
37+
38+
// IPv4 wildcard patterns (.255 endings)
39+
[DataRow("192.43.255.255", 16 )]
40+
[DataRow("10.255.255.255", 8 )]
41+
[DataRow("192.0.43.255", 24)]
42+
[DataRow("192.0.255.255", 16)]
43+
[DataRow("192.255.255.255", 8)]
44+
[DataRow("255.255.255.255", 0)]
45+
46+
// IPv6 exact address
47+
[DataRow("2001:db8::1", 128)]
48+
[DataRow("::", 0)]
49+
[DataRow("2001:0db8::", 32)]
50+
51+
// IPv6 with trailing zeros (network boundaries)
52+
[DataRow("2001:db8::", 32)] // common /64 subnet
53+
[DataRow("2001:db8:1:2:3:4:5:0", 112)] // last hextet zero
54+
[DataRow("2001:db8:1:2:3:4:0:0", 96)] // last 2 hextets zero
55+
[DataRow("2001:db8:1:2:3:0:0:0", 80)] // last 3 hextets zero
56+
[DataRow("2001:db8:1:2:0:0:0:0", 64)] // last 4 hextets zero
57+
[DataRow("2001:db8:1:0:0:0:0:0", 48)]
58+
[DataRow("2001:db8:0:0:0:0:0:0", 32)]
59+
public void TestTryGuess(string? message, int expectedCidr, bool expectedParsed = true)
60+
{
61+
var cidrguess = new CidrNetworkAware();
62+
bool parsed = cidrguess.TryGuessCidr(message, out byte cidr);
63+
64+
Assert.AreEqual(expectedParsed, parsed, "parsed");
65+
Assert.AreEqual(expectedCidr, cidr, "cidr");
66+
}
67+
}

src/TestProject/TestProject.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<SignAssembly Condition="'$(OS)' != 'Windows_NT'">true</SignAssembly>
88
<AssemblyOriginatorKeyFile>..\System.Net.IPNetwork.snk</AssemblyOriginatorKeyFile>
99
<EnableNETAnalyzers>true</EnableNETAnalyzers>
10-
<Version>3.1.0</Version>
10+
<Version>3.2.0</Version>
1111
<LangVersion>latestmajor</LangVersion>
1212
<NoWarn>SA1010</NoWarn>
1313
<GenerateDocumentationFile>true</GenerateDocumentationFile>

0 commit comments

Comments
 (0)