Skip to content

Commit

Permalink
Change behavior of Contains() method to be more intuitive and introdu…
Browse files Browse the repository at this point in the history
…ce Overlaps() method.
  • Loading branch information
RobThree committed Mar 1, 2024
1 parent 7805b39 commit 198f144
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 44 deletions.
38 changes: 34 additions & 4 deletions IPNetworkHelper.Tests/NetworkHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public void SplitThrowsOnUnsplittableNetworkIPv6()


[TestMethod]
[ExpectedException(typeof(AddressFamilyMismatchException))]
[ExpectedException(typeof(IPNetworkNotInIPNetworkException))]
public void ExtractThrowsOnAddressFamilyMismatch()
{
var ipv4 = IPNetwork.Parse("192.168.0.0/24");
Expand Down Expand Up @@ -267,16 +267,46 @@ public void Network_Contains_OtherNetwork()
var network_d = IPNetwork.Parse("192.168.0.64/28"); // 192.168.0.64 - ..79
var network_e = IPNetwork.Parse("192.168.0.0/26"); // 192.168.0.0 - ..63

Assert.IsTrue(network_a.Contains(network_e));
Assert.IsFalse(network_a.Contains(network_e));
Assert.IsTrue(network_e.Contains(network_a));

Assert.IsTrue(network_b.Contains(network_e));
Assert.IsFalse(network_b.Contains(network_e));
Assert.IsTrue(network_e.Contains(network_b));

Assert.IsTrue(network_c.Contains(network_e));
Assert.IsFalse(network_c.Contains(network_e));
Assert.IsTrue(network_e.Contains(network_c));

Assert.IsFalse(network_d.Contains(network_e));
Assert.IsFalse(network_e.Contains(network_d));
}

[TestMethod]
public void Network_Overlaps_OtherNetwork()
{

/* 0 16 32 48 64 80 128 255
* |----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|
* |\_A_/ \__B_/\_C_/\_D_/
* | |
* \________E__________/
*/

var network_a = IPNetwork.Parse("192.168.0.0/28"); // 192.168.0.0 - ..15
var network_b = IPNetwork.Parse("192.168.0.32/28"); // 192.168.0.32 - ..47
var network_c = IPNetwork.Parse("192.168.0.48/28"); // 192.168.0.48 - ..63
var network_d = IPNetwork.Parse("192.168.0.64/28"); // 192.168.0.64 - ..79
var network_e = IPNetwork.Parse("192.168.0.0/26"); // 192.168.0.0 - ..63

Assert.IsTrue(network_a.Overlaps(network_e));
Assert.IsTrue(network_e.Overlaps(network_a));

Assert.IsTrue(network_b.Overlaps(network_e));
Assert.IsTrue(network_e.Overlaps(network_b));

Assert.IsTrue(network_c.Overlaps(network_e));
Assert.IsTrue(network_e.Overlaps(network_c));

Assert.IsFalse(network_d.Overlaps(network_e));
Assert.IsFalse(network_e.Overlaps(network_d));
}
}
112 changes: 76 additions & 36 deletions IPNetworkHelper/NetworkHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,58 @@ namespace IPNetworkHelper;

public static class NetworkHelper
{
/// <summary>
/// Checks if this network contains the given network entirely.
/// </summary>
/// <param name="thisNetwork">The network to check if it contains the other network entirely.</param>
/// <param name="otherNetwork">The network to be checked if it is contained in this network.</param>
/// <returns>True when this network contains the other network entirely.</returns>
public static bool Contains(this IPNetwork thisNetwork, IPNetwork otherNetwork)
=> thisNetwork.Contains(otherNetwork.BaseAddress)
|| otherNetwork.Contains(thisNetwork.BaseAddress);
=> thisNetwork.Contains(otherNetwork.GetFirstIP())
&& thisNetwork.Contains(otherNetwork.GetLastIP());

/// <summary>
/// Checks if this network overlaps with the given network.
/// </summary>
/// <param name="thisNetwork">The network to check if it overlaps with the other network.</param>
/// <param name="otherNetwork">The network to be checked if it overlaps with this network.</param>
/// <returns>True when this network overlaps with the other network.</returns>
public static bool Overlaps(this IPNetwork thisNetwork, IPNetwork otherNetwork)
=> thisNetwork.Contains(otherNetwork.GetFirstIP())
|| thisNetwork.Contains(otherNetwork.GetLastIP())
|| otherNetwork.Contains(thisNetwork.GetFirstIP())
|| otherNetwork.Contains(thisNetwork.GetLastIP());

/// <summary>
/// Gets the first IP address of the given network (which is the <see cref="IPNetwork.BaseAddress"/>.
/// </summary>
/// <param name="network">The network to get the first IP address from.</param>
/// <returns>Returns the first IP address of the given network.</returns>

public static IPAddress GetFirstIP(this IPNetwork network)
=> new(CalculateFirstBytes(network.BaseAddress.GetAddressBytes(), network.PrefixLength));

private static byte[] CalculateFirstBytes(byte[] prefixBytes, int prefixLength)
{
var result = new byte[prefixBytes.Length];
var mask = CreateMask(prefixBytes, prefixLength);
for (var i = 0; i < prefixBytes.Length; i++)
{
result[i] = (byte)(prefixBytes[i] & mask[i]);
}

return result;
}
=> network.BaseAddress;

/// <summary>
/// Gets the last IP address of the given network.
/// </summary>
/// <param name="network">The network to get the last IP address from.</param>
/// <returns>Returns the last IP address of the given network.</returns>
public static IPAddress GetLastIP(this IPNetwork network)
=> new(CalculateLastBytes(network.BaseAddress.GetAddressBytes(), network.PrefixLength));

internal static byte[] CalculateLastBytes(byte[] prefixBytes, int prefixLength)
{
var result = new byte[prefixBytes.Length];
var mask = CreateMask(prefixBytes, prefixLength);
for (var i = 0; i < prefixBytes.Length; i++)
var addressbytes = network.BaseAddress.GetAddressBytes();
var result = new byte[addressbytes.Length];
var mask = CreateMask(addressbytes, network.PrefixLength);
for (var i = 0; i < addressbytes.Length; i++)
{
result[i] = (byte)(prefixBytes[i] | ~mask[i]);
result[i] = (byte)(addressbytes[i] | ~mask[i]);
}

return result;
return new(result);
}

private static byte[] CreateMask(byte[] prefixBytes, int prefixLength)
private static byte[] CreateMask(byte[] addressBytes, int prefixLength)
{
var mask = new byte[prefixBytes.Length];
var mask = new byte[addressBytes.Length];
var remainingbits = prefixLength;
var i = 0;
while (remainingbits >= 8)
Expand All @@ -63,27 +78,40 @@ private static byte[] CreateMask(byte[] prefixBytes, int prefixLength)
return mask;
}

/// <summary>
/// Splits the given network into two halves.
/// </summary>
/// <param name="network">The network to split.</param>
/// <returns>Returns the left and right half of the given network.</returns>
/// <exception cref="UnableToSplitIPNetworkException">Thrown when the network is already at its maximum prefixlength.</exception>
public static (IPNetwork left, IPNetwork right) Split(this IPNetwork network)
{
var prefixbytes = CalculateFirstBytes(network.BaseAddress.GetAddressBytes(), network.PrefixLength);
var maxprefix = prefixbytes.Length * 8;
var addressbytes = network.BaseAddress.GetAddressBytes();
var maxprefix = addressbytes.Length * 8;
if (network.PrefixLength >= maxprefix)
{
throw new UnableToSplitIPNetworkException(network);
}

// Left part of split is simply first half of network
var left = new IPNetwork(new(prefixbytes), network.PrefixLength + 1);
var left = new IPNetwork(new(addressbytes), network.PrefixLength + 1);

// Right part of split is second half of network
// We need to set the "network MSB" for the second half
var byteindex = network.PrefixLength / 8;
var bitinbyteindex = 7 - (network.PrefixLength % 8);
prefixbytes[byteindex] |= (byte)(1 << bitinbyteindex);
addressbytes[byteindex] |= (byte)(1 << bitinbyteindex);

return (left, new IPNetwork(new(prefixbytes), network.PrefixLength + 1));
return (left, new IPNetwork(new(addressbytes), network.PrefixLength + 1));
}

/// <summary>
/// Takes a random subnet with the given prefix from the network and returns all subnets after taking the desired subnet, including the desired subnet.
/// </summary>
/// <param name="network">The network to extract the subnet from.</param>
/// <param name="prefixLength">The prefixlength of the subnect to extract from the network.</param>
/// <returns>Returns all subnets after taking the desired subnet, including the desired subnet.</returns>
/// <exception cref="NotSupportedException">Thrown for all non-IPv4/6 networks.</exception>
public static IEnumerable<IPNetwork> Extract(this IPNetwork network, int prefixLength)
=> Extract(network, network.BaseAddress.AddressFamily switch
{
Expand All @@ -92,9 +120,22 @@ public static IEnumerable<IPNetwork> Extract(this IPNetwork network, int prefixL
_ => throw new NotSupportedException($"Network addressfamily '{network.BaseAddress.AddressFamily}' not supported")
});

/// <summary>
/// Extracts the given subnet from the network and returns all subnets after taking the desired subnet, including the desired subnet.
/// </summary>
/// <param name="network">The network to extract the desired subnet from.</param>
/// <param name="desiredNetwork">The subnet to extract from the network.</param>
/// <returns>Returns all subnets after taking the desired subnet, including the desired subnet.</returns>
public static IEnumerable<IPNetwork> Extract(this IPNetwork network, IPNetwork desiredNetwork)
=> ExtractImpl(network, desiredNetwork).OrderBy(i => i, IPNetworkComparer.Default);

=> Extract(network, [desiredNetwork]);

/// <summary>
/// Extracts the given subnets from the network and returns all subnets after taking the desired subnet, including the desired subnets.
/// </summary>
/// <param name="network">The network to extract the desired subnets from.</param>
/// <param name="desiredNetworks">The subnets to extract from the network.</param>
/// <returns>Returns all subnets after taking the desired subnet, including the desired subnets.</returns>
/// <exception cref="IPNetworkNotInIPNetworkException">Thrown when any of the desired subnets is not in the network.</exception>
public static IEnumerable<IPNetwork> Extract(this IPNetwork network, IEnumerable<IPNetwork> desiredNetworks)
{
// We start with a single network
Expand All @@ -109,12 +150,11 @@ public static IEnumerable<IPNetwork> Extract(this IPNetwork network, IEnumerable
networks.Remove(target);

// Extract the network from the target and add the results to our networks list
networks.AddRange(target.Extract(d));
networks.AddRange(ExtractImpl(target, d));
}
return networks.OrderBy(i => i, IPNetworkComparer.Default);
}

private static readonly Random _rng = new();
private static IEnumerable<IPNetwork> ExtractImpl(IPNetwork network, IPNetwork desiredNetwork)
{
if (desiredNetwork.BaseAddress.AddressFamily != network.BaseAddress.AddressFamily)
Expand All @@ -137,8 +177,8 @@ private static IEnumerable<IPNetwork> ExtractImpl(IPNetwork network, IPNetwork d
{
var (left, right) = network.Split(); // Split the given network into two halves
var goleft = pickatrandom // If "pick at random"
? _rng.Next(0, 2) == 0 // ... use a 50/50 chance to pick a half
: left.Contains(desiredNetwork.BaseAddress);// ... else: is the desired prefix in the left half?
? Random.Shared.Next(0, 2) == 0 // ... use a 50/50 chance to pick a half
: left.Contains(desiredNetwork.BaseAddress);// ... else: is the desired address in the left half?
yield return goleft ? right : left; // Return half that DOESN'T contain desired network
network = goleft ? left : right; // This is the part containing our desired network
}
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# ![logo](https://raw.githubusercontent.com/RobThree/IPNetworkHelper/master/logo_24x24.png) IPNetworkHelper

Provides helper (extension)methods for working with (IPv4 and/or IPv6) [IPNetworks](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.httpoverrides.ipnetwork). These include parsing, splitting and extracting networks from larger networks. Available as [NuGet package](https://www.nuget.org/packages/IPNetworkHelper/).
Provides helper (extension)methods for working with (IPv4 and/or IPv6) [IPNetworks](https://learn.microsoft.com/en-us/dotnet/api/system.net.ipnetwork). These include splitting and extracting networks from larger networks. Available as [NuGet package](https://www.nuget.org/packages/IPNetworkHelper/).

Note that since version 2.0 we use the [`System.Net.IPNetwork`](https://learn.microsoft.com/en-us/dotnet/api/system.net.ipnetwork) struct, which, unfortunately, is only available in .NET 8.0 and later. If you need support for earlier versions of .NET, use version 1.0 of this library.

Version 3.0 has a breaking change where `Contains()` now does the more intuitive thing and checks if the network is entirely contained within another network. If you want to check if two networks overlap, use the `Overlaps()` method.

## Quickstart

All of the below examples use IPv4 but IPv6 works just as well.
Expand All @@ -18,22 +20,22 @@ if (IPNetwork.TryParse("192.168.0.0/16", out var othernetwork))
// ...
}

// Get first/last IP from network
// Get last IP from network
var first = network.GetFirstIP(); // Network (192.168.0.0)
var last = network.GetLastIP(); // Broadcast (192.168.255.255)
// Splits a network into two halves
var (left, right) = network.Split(); // Returns 192.168.0.0/17 and 192.168.128.0/17
// Remove a subnet from a network
// Extract a subnet from a network
var desired = IPNetwork.Parse("192.168.10.16/28");
var result = network.Extract(desired);

// Result:
// 192.168.0.0/21
// 192.168.8.0/23
// 192.168.10.0/28
// 192.168.10.16/28
// 192.168.10.16/28 <- desired
// 192.168.10.32/27
// 192.168.10.64/26
// 192.168.10.128/25
Expand All @@ -45,6 +47,8 @@ var result = network.Extract(desired);
// 192.168.128.0/17
```

The `Contains(IPNetwork)` method can be used to check if a network is contained (entirely) within another network and the `Overlaps(IPNetwork)` method can be used to check if two networks overlap.

This library also includes an `IPAddressComparer` and `IPNetworkComparer` to be used when sorting IPAddresses or networks:

```c#
Expand Down

0 comments on commit 198f144

Please sign in to comment.