dotnet / dotnet/runtime

[API Proposal]: System.Net.IPNetworks

Open
#121,545 4 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.Net
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Background and motivation

The System.Net.Primitives package currently provides the IPNetwork structure, which verifies if an IP address belongs to a network. However, there is a need for an efficient way to verify that an IP address belongs to a group of networks. Security checks, for example, verify that the connection IP address is from one of the known networks. In some environments, the list of networks to verify may be enormous (hundreds or thousands), and checking each network individually is unacceptable for high-load production services. The proposal is to add the corresponding API directly to .NET to efficiently address this issue.

### API Proposal

```csharp
// package [System.Net.Primitives]
namespace System.Net;

///
/// Represents a collection of .
///
public sealed class IPNetworks
{
///
/// Initializes a new instance of the class with the specified collection of .
///
/// The collection of .
public IPNetworks(IEnumerable ipNetworks);

///
/// Determines whether a given is part of any network.
///
/// The to check.
/// if the is part of any network; otherwise, .
/// The specified is .
public bool Contains(IPAddress ipAddress);
}
```

### API Usage

```csharp
// Initialize IP networks
var ipNetworks = new IPNetworks([
IPNetwork.Parse("15.29.30.56/29"),
IPNetwork.Parse("12.10.2.80/30"),
IPNetwork.Parse("191.233.9.120/29"),
IPNetwork.Parse("1f1f:f1f1:1f1f:ffff::/64")]);

// Prints False
Console.WriteLine(ipNetworks.Contains(IPAddress.Parse("192.160.10.10")));
```

### Alternative Designs

The following is an example of an efficient API implementation:

```csharp
public sealed class IPNetworks
{
private const int MaxNumberOfNetworksForLinearSearch = 5;

private readonly IPNetworkSearch? _ipV4NetworkSearch;
private readonly IPNetworkSearch? _ipV6NetworkSearch;

public IPNetworks(IEnumerable? iPNetworks)
{
var ipV4Networks = (iPNetworks ?? Array.Empty())
.Where(ipNetwork => ipNetwork.BaseAddress.AddressFamily == AddressFamily.InterNetwork)
.ToList();

if (ipV4Networks.Count > 0)
{
if (ipV4Networks.Count <= MaxNumberOfNetworksForLinearSearch)
{
_ipV4NetworkSearch = new IPNetworkLinearSearch(ipV4Networks);
}
else
{
_ipV4NetworkSearch = new IPNetworkTreeSearch(new IpNetworkSearchNode(), true);

foreach (var ipNetwork in ipV4Networks)
{
(_ipV4NetworkSearch as IPNetworkTreeSearch)!.AddNetwork(ipNetwork);
}
}
}

var ipV6Networks = (iPNetworks ?? Array.Empty())
.Where(ipNetwork => ipNetwork.BaseAddress.AddressFamily == AddressFamily.InterNetworkV6)
.ToList();

if (ipV6Networks.Count > 0)
{
if (ipV6Networks.Count <= MaxNumberOfNetworksForLinearSearch)
{
_ipV6NetworkSearch = new IPNetworkLinearSearch(ipV6Networks);
}
else
{
_ipV6NetworkSearch = new IPNetworkTreeSearch(new IpNetworkSearchNode(), false);

foreach (var ipNetwork in ipV6Networks)
{
(_ipV6NetworkSearch as IPNetworkTreeSearch)!.AddNetwork(ipNetwork);
}
}
}
}

public bool Contains(IPAddress ipAddress)
{
if (ipAddress == null)
{
throw new ArgumentNullException(nameof(ipAddress));
}

if (_ipV4NetworkSearch == null && _ipV6NetworkSearch == null)
{
// No networks defined, allow all addresses
return true;
}

var networkSearch = _ipV4NetworkSearch;

if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6)
{
if (ipAddress.IsIPv4MappedToIPv6)
{
ipAddress = ipAddress.MapToIPv4();
}
else
{
networkSearch = _ipV6NetworkSearch;
}
}

if (networkSearch == null)
{
// No networks defined for this address family, deny all addresses
return false;
}

return networkSearch.ContainsAddress(ipAddress);
}

private interface IPNetworkSearch
{
bool ContainsAddress(IPAddress address);
}

private sealed class IPNetworkLinearSearch : IPNetworkSearch
{
private readonly List _ipNetworks;

public IPNetworkLinearSearch(List ipNetworks)
{
_ipNetworks = ipNetworks;
}

public bool ContainsAddress(IPAddress address)
{
foreach (var ipNetwork in _ipNetworks)
{
if (ipNetwork.Contains(address))
{
return true;
}
}

return false;
}
}

private sealed class IPNetworkTreeSearch : IPNetworkSearch
{
private readonly IpNetworkSearchNode _rootNode;
private readonly bool _isIpv4SearchTree;

public IPNetworkTreeSearch(IpNetworkSearchNode rootNode, bool isIpv4SearchTree)
{
_rootNode = rootNode;
_isIpv4SearchTree = isIpv4SearchTree;
}

public bool ContainsAddress(IPAddress address)
{
var addressLengthInBytes = _isIpv4SearchTree ? 4 : 16;

Span addressBytes = stackalloc byte[addressLengthInBytes];
_ = address.TryWriteBytes(addressBytes, out _);

var currentNode = _rootNode;
var bitIndex = 0;

while (currentNode != null && bitIndex < addressBytes.Length * 8)
{
if (currentNode.IsNetwork)
{
// Found a matching network
return true;
}

int byteIndex = bitIndex / 8;
int bitPosition = 7 - (bitIndex % 8);
bool bitValue = (addressBytes[byteIndex] & (1 << bitPosition)) != 0;

currentNode = bitValue ? currentNode.OneSubtree : currentNode.ZeroSubtree;
bitIndex++;
}

return currentNode?.IsNetwork ?? false;
}

public void AddNetwork(IPNetwork ipNetwork)
{
var baseAddressBytes = ipNetwork.BaseAddress.GetAddressBytes();

if (ipNetwork.PrefixLength == 0)
{
// The network covers all addresses
_rootNode.IsNetwork = true;
return;
}

var currentNode = _rootNode;

for (int bitIndex = 0; bitIndex < ipNetwork.PrefixLength; bitIndex++)
{
if (currentNode.IsNetwork)
{
// A broader network already exists in the tree; no need to add more specific networks
return;
}

int byteIndex = bitIndex / 8;
int bitPosition = 7 - (bitIndex % 8);
bool bitValue = (baseAddressBytes[byteIndex] & (1 << bitPosition)) != 0;
if (bitValue)
{
currentNode.OneSubtree ??= new IpNetworkSearchNode();
currentNode = currentNode.OneSubtree;
}
else
{
currentNode.ZeroSubtree ??= new IpNetworkSearchNode();
currentNode = currentNode.ZeroSubtree;
}
}

currentNode.IsNetwork = true;

// Prune any subtrees since this node now represents the parent network which includes all possible subnets
currentNode.OneSubtree = null;
currentNode.ZeroSubtree = null;
}
}

private sealed class IpNetworkSearchNode
{
public bool IsNetwork;
public IpNetworkSearchNode? ZeroSubtree;
public IpNetworkSearchNode? OneSubtree;
}
}
```

### Risks

No risks.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.