1+ // <copyright file="UniqueLocalAddress.cs" company="IPNetwork">
2+ // Copyright (c) IPNetwork. All rights reserved.
3+ // </copyright>
4+
5+ using System . Net . Sockets ;
6+ using System . Security . Cryptography ;
7+ using System . Text ;
8+
9+ namespace System . Net ;
10+
11+ /// <summary>
12+ /// Utility class for IPv6 Unique Local Address (ULA) generation and validation.
13+ /// Implements RFC 4193 for generating ULA prefixes in the fd00::/8 range.
14+ ///
15+ /// A locally-assigned ULA always looks like this (128 bits total):
16+ /// | 8 bits | 40 bits | 16 bits | 64 bits |
17+ /// +---------+----------------+-------------+-------------------+
18+ /// | fd (8) | Global ID | Subnet ID | Interface ID |
19+ ///
20+ /// • fd = the fixed 8-bit prefix (fd00::/8 for locally assigned).
21+ /// • Global ID = 40 random bits, chosen once to make your ULA unique.
22+ /// • Subnet ID = 16 bits, chosen by you inside your site.
23+ /// • Interface ID = 64 bits, assigned to each host within the subnet (same rule as all IPv6).
24+ ///
25+ /// The /48 prefix is the site prefix (fdXX:XXXX:XXXX::/48).
26+ /// </summary>
27+ public static class UniqueLocalAddress
28+ {
29+ /// <summary>
30+ /// ULA prefix for locally assigned addresses (fd00::/8).
31+ /// </summary>
32+ public static readonly IPNetwork2 UlaLocallyAssigned = IPNetwork2 . Parse ( "fd00::/8" ) ;
33+
34+ /// <summary>
35+ /// ULA prefix for centrally assigned addresses (fc00::/8) - currently undefined.
36+ /// </summary>
37+ public static readonly IPNetwork2 UlaCentrallyAssigned = IPNetwork2 . Parse ( "fc00::/8" ) ;
38+
39+ /// <summary>
40+ /// Full ULA range (fc00::/7).
41+ /// </summary>
42+ public static readonly IPNetwork2 UlaRange = IPNetwork2 . Parse ( "fc00::/7" ) ;
43+
44+ /// <summary>
45+ /// Generates a random ULA /48 prefix using the algorithm from RFC 4193.
46+ /// </summary>
47+ /// <returns>A randomly generated ULA /48 network.</returns>
48+ public static IPNetwork2 GenerateUlaPrefix ( )
49+ {
50+ byte [ ] globalId = GenerateRandomGlobalId ( ) ;
51+ return CreateUlaPrefix ( globalId ) ;
52+ }
53+
54+ /// <summary>
55+ /// Generates a random ULA /48 prefix using a specific MAC address for entropy.
56+ /// </summary>
57+ /// <param name="macAddress">MAC address to use for entropy generation.</param>
58+ /// <returns>A ULA /48 network generated using the provided MAC address.</returns>
59+ public static IPNetwork2 GenerateUlaPrefix ( byte [ ] macAddress )
60+ {
61+ if ( macAddress == null )
62+ {
63+ throw new ArgumentNullException ( nameof ( macAddress ) ) ;
64+ }
65+
66+ if ( macAddress . Length != 6 )
67+ {
68+ throw new ArgumentException ( "MAC address must be 6 bytes long" , nameof ( macAddress ) ) ;
69+ }
70+
71+ byte [ ] globalId = GenerateGlobalIdFromMac ( macAddress ) ;
72+ return CreateUlaPrefix ( globalId ) ;
73+ }
74+
75+ /// <summary>
76+ /// Generates a random ULA /48 prefix using a seed for deterministic generation.
77+ /// </summary>
78+ /// <param name="seed">Seed value for deterministic generation.</param>
79+ /// <returns>A ULA /48 network generated using the provided seed.</returns>
80+ public static IPNetwork2 GenerateUlaPrefix ( string seed )
81+ {
82+ if ( string . IsNullOrEmpty ( seed ) )
83+ {
84+ throw new ArgumentNullException ( nameof ( seed ) , "Seed cannot be null or empty" ) ;
85+ }
86+
87+ byte [ ] globalId = GenerateGlobalIdFromSeed ( seed ) ;
88+ return CreateUlaPrefix ( globalId ) ;
89+ }
90+
91+ /// <summary>
92+ /// Creates a ULA subnet within a ULA /48 prefix.
93+ /// </summary>
94+ /// <param name="ulaPrefix">The ULA /48 prefix.</param>
95+ /// <param name="subnetId">16-bit subnet identifier.</param>
96+ /// <returns>A ULA /64 subnet.</returns>
97+ public static IPNetwork2 CreateUlaSubnet ( IPNetwork2 ulaPrefix , int subnetId )
98+ {
99+ if ( ! IsUlaPrefix ( ulaPrefix ) )
100+ {
101+ throw new ArgumentException ( "Network must be a valid ULA prefix" , nameof ( ulaPrefix ) ) ;
102+ }
103+
104+ if ( ulaPrefix . Cidr != 48 )
105+ {
106+ throw new ArgumentException ( "ULA prefix must be /48" , nameof ( ulaPrefix ) ) ;
107+ }
108+
109+ if ( subnetId < 0 || subnetId > 65535 )
110+ {
111+ throw new ArgumentOutOfRangeException ( nameof ( ulaPrefix ) ) ;
112+ }
113+
114+ byte [ ] networkBytes = ulaPrefix . Network . GetAddressBytes ( ) ;
115+
116+ // Set subnet ID in bytes 6-7 (positions after the /48 prefix)
117+ networkBytes [ 6 ] = ( byte ) ( subnetId >> 8 ) ;
118+ networkBytes [ 7 ] = ( byte ) ( subnetId & 0xFF ) ;
119+
120+ var subnetAddress = new IPAddress ( networkBytes ) ;
121+ return IPNetwork2 . Parse ( $ "{ subnetAddress } /64") ;
122+ }
123+
124+ /// <summary>
125+ /// Validates whether an IP address is within the ULA range.
126+ /// </summary>
127+ /// <param name="address">IP address to validate.</param>
128+ /// <returns>True if the address is a ULA, false otherwise.</returns>
129+ public static bool IsUla ( IPAddress address )
130+ {
131+ return address ? . AddressFamily == AddressFamily . InterNetworkV6 && UlaRange . Contains ( address ) ;
132+ }
133+
134+ /// <summary>
135+ /// Validates whether a network is within the ULA range.
136+ /// </summary>
137+ /// <param name="network">Network to validate.</param>
138+ /// <returns>True if the network is a ULA, false otherwise.</returns>
139+ public static bool IsUlaPrefix ( IPNetwork2 network )
140+ {
141+ if ( network ? . AddressFamily != AddressFamily . InterNetworkV6 )
142+ {
143+ return false ;
144+ }
145+
146+ return UlaRange . Contains ( network . Network ) ;
147+ }
148+
149+ /// <summary>
150+ /// Validates whether a network is a locally assigned ULA (fd00::/8).
151+ /// </summary>
152+ /// <param name="network">Network to validate.</param>
153+ /// <returns>True if the network is locally assigned ULA, false otherwise.</returns>
154+ public static bool IsLocallyAssignedUla ( IPNetwork2 network )
155+ {
156+ if ( network ? . AddressFamily != AddressFamily . InterNetworkV6 )
157+ {
158+ return false ;
159+ }
160+
161+ return UlaLocallyAssigned . Contains ( network . Network ) ;
162+ }
163+
164+ /// <summary>
165+ /// Generates a 40-bit random Global ID according to RFC 4193 algorithm.
166+ /// </summary>
167+ /// <returns>40-bit Global ID as a byte array.</returns>
168+ private static byte [ ] GenerateRandomGlobalId ( )
169+ {
170+ using var rng = RandomNumberGenerator . Create ( ) ;
171+ byte [ ] globalId = new byte [ 5 ] ; // 40 bits = 5 bytes
172+ rng . GetBytes ( globalId ) ;
173+ return globalId ;
174+ }
175+
176+ /// <summary>
177+ /// Generates a Global ID using MAC address and timestamp as suggested in RFC 4193.
178+ /// </summary>
179+ /// <param name="macAddress">6-byte MAC address.</param>
180+ /// <returns>40-bit Global ID as a byte array.</returns>
181+ private static byte [ ] GenerateGlobalIdFromMac ( byte [ ] macAddress )
182+ {
183+ using var sha2 = SHA256 . Create ( ) ;
184+ byte [ ] input = new byte [ macAddress . Length + 8 ] ;
185+ Array . Copy ( macAddress , 0 , input , 0 , macAddress . Length ) ;
186+
187+ // Add current timestamp
188+ byte [ ] timestamp = BitConverter . GetBytes ( DateTimeOffset . UtcNow . ToUnixTimeSeconds ( ) ) ;
189+ Array . Copy ( timestamp , 0 , input , macAddress . Length , timestamp . Length ) ;
190+
191+ byte [ ] hash = sha2 . ComputeHash ( input ) ;
192+ byte [ ] globalId = new byte [ 5 ] ;
193+ Array . Copy ( hash , 0 , globalId , 0 , 5 ) ;
194+ return globalId ;
195+ }
196+
197+ /// <summary>
198+ /// Generates a Global ID from a seed string for deterministic generation.
199+ /// </summary>
200+ /// <param name="seed">Seed string.</param>
201+ /// <returns>40-bit Global ID as a byte array.</returns>
202+ private static byte [ ] GenerateGlobalIdFromSeed ( string seed )
203+ {
204+ using var sha2 = SHA256 . Create ( ) ;
205+ byte [ ] seedBytes = Encoding . UTF8 . GetBytes ( seed ) ;
206+ byte [ ] hash = sha2 . ComputeHash ( seedBytes ) ;
207+ byte [ ] globalId = new byte [ 5 ] ;
208+ Array . Copy ( hash , 0 , globalId , 0 , 5 ) ;
209+ return globalId ;
210+ }
211+
212+ /// <summary>
213+ /// Creates a ULA /48 prefix from a 40-bit Global ID.
214+ /// </summary>
215+ /// <param name="globalId">5-byte Global ID.</param>
216+ /// <returns>ULA /48 network.</returns>
217+ private static IPNetwork2 CreateUlaPrefix ( byte [ ] globalId )
218+ {
219+ byte [ ] addressBytes = new byte [ 16 ] ;
220+
221+ // Set ULA locally assigned prefix (fd)
222+ addressBytes [ 0 ] = 0xfd ;
223+
224+ // Set the 40-bit Global ID (5 bytes)
225+ Array . Copy ( globalId , 0 , addressBytes , 1 , 5 ) ;
226+
227+ // Remaining bytes are zero for /48 prefix
228+
229+ var address = new IPAddress ( addressBytes ) ;
230+ return IPNetwork2 . Parse ( $ "{ address } /48") ;
231+ }
232+ }
0 commit comments