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+ }
0 commit comments