Skip to content

Commit 90baade

Browse files
authored
Feat/ula generation (#362)
* Feat: IPv6 Unique Local Address (ULA)
1 parent 7615240 commit 90baade

File tree

7 files changed

+954
-85
lines changed

7 files changed

+954
-85
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,25 @@ Resut:
481481
```
482482
---
483483

484+
## IPv6 Unique Local Address (ULA)
485+
486+
Unique Local Addresses are IPv6 addresses in the range fc00::/7 that are not routed on the public Internet. They are the IPv6 equivalent of private IPv4 addresses (e.g. 10.0.0.0/8, 192.168.0.0/16).
487+
488+
```C#
489+
// Generate a random ULA prefix
490+
var randomUla = UniqueLocalAddress.GenerateUlaPrefix();
491+
Console.WriteLine($"Random ULA: {randomUla}"); // e.g., fd12:3456:789a::/48
492+
493+
// Create subnets with int subnet IDs (CLS-compliant)
494+
var subnet1 = UniqueLocalAddress.CreateUlaSubnet(randomUla, 1);
495+
var subnet2 = UniqueLocalAddress.CreateUlaSubnet(randomUla, 2);
496+
var maxSubnet = UniqueLocalAddress.CreateUlaSubnet(randomUla, UniqueLocalAddress.MaxSubnetId);
497+
498+
Console.WriteLine($"Subnet 1: {subnet1}"); // e.g., fd12:3456:
499+
```
500+
501+
---
502+
484503
## IPNetwork utility command line
485504

486505
IPNetwork utility command line take care of complex network, ip, netmask,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
// <copyright file="IPNetwork2Extensions.cs" company="IPNetwork">
3+
// Copyright (c) IPNetwork. All rights reserved.
4+
// </copyright>
5+
namespace System.Net;
6+
7+
/// <summary>
8+
/// Extension methods for IPNetwork2 to support ULA operations.
9+
/// </summary>
10+
public static class IPNetwork2UlaExtension
11+
{
12+
/// <summary>
13+
/// Determines if this network is a Unique Local Address (ULA).
14+
/// </summary>
15+
/// <param name="network">The network to check.</param>
16+
/// <returns>True if the network is a ULA, false otherwise.</returns>
17+
public static bool IsUla(this IPNetwork2 network)
18+
{
19+
return UniqueLocalAddress.IsUlaPrefix(network);
20+
}
21+
22+
/// <summary>
23+
/// Determines if this network is a locally assigned ULA (fd00::/8).
24+
/// </summary>
25+
/// <param name="network">The network to check.</param>
26+
/// <returns>True if the network is locally assigned ULA, false otherwise.</returns>
27+
public static bool IsLocallyAssignedUla(this IPNetwork2 network)
28+
{
29+
return UniqueLocalAddress.IsLocallyAssignedUla(network);
30+
}
31+
32+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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+
}

src/TestProject/IPAddressCollectionUnitTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ public void TestIterateIPAddress()
137137
count++;
138138
}
139139

140+
object current = ((IEnumerator)ips).Current;
141+
((IEnumerator)ips).Reset();
142+
143+
Assert.IsNotNull(current, "current is no null");
140144
Assert.IsNotNull(last, "last is null");
141145
Assert.IsNotNull(first, "first is null");
142146
Assert.AreEqual("192.168.1.0", first.ToString(), "first");

0 commit comments

Comments
 (0)