Skip to content

Commit

Permalink
Merge pull request #65 from clickviewapp/IpAddress-IsPrivate
Browse files Browse the repository at this point in the history
Add IPAddress.IsPrivate() extension method
  • Loading branch information
MrSmoke authored Jan 23, 2024
2 parents b0cb935 + d74e503 commit 2317cf1
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net6.0</TargetFrameworks>
<PackageTags>primitives</PackageTags>
</PropertyGroup>

Expand Down
67 changes: 67 additions & 0 deletions src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
namespace ClickView.Extensions.Primitives.Extensions;

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

public static class IpAddressExtensions
{
// Source: https://gist.github.com/angularsen/f77b53ee9966fcd914025e25a2b3a085 (thank you!)
/// <summary>
/// Returns true if the IP address is in a private range.<br/>
/// IPv4: Loopback, link local ("169.254.x.x"), class A ("10.x.x.x"), class B ("172.16.x.x" to "172.31.x.x") and class C ("192.168.x.x").<br/>
/// IPv6: Loopback, link local, site local, unique local and private IPv4 mapped to IPv6.<br/>
/// </summary>
/// <param name="ip">The IP address.</param>
/// <returns>True if the IP address was in a private range.</returns>
/// <exception cref="NotSupportedException">Thrown if the ip address is not <see cref="AddressFamily.InterNetwork"/> or <see cref="AddressFamily.InterNetworkV6"/></exception>
/// <example><code>bool isPrivate = IPAddress.Parse("127.0.0.1").IsPrivate();</code></example>
public static bool IsPrivate(this IPAddress ip)
{
if (ip is null)
throw new ArgumentNullException(nameof(ip));

// Map back to IPv4 if mapped to IPv6, for example "::ffff:1.2.3.4" to "1.2.3.4".
if (ip.IsIPv4MappedToIPv6)
ip = ip.MapToIPv4();

// Checks loopback ranges for both IPv4 and IPv6.
if (IPAddress.IsLoopback(ip))
return true;

// IPv4
if (ip.AddressFamily == AddressFamily.InterNetwork)
return IsPrivateIPv4(ip.GetAddressBytes());

// IPv6
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
return ip.IsIPv6LinkLocal ||
#if NET6_0_OR_GREATER
ip.IsIPv6UniqueLocal ||
#endif
ip.IsIPv6SiteLocal;
}

throw new NotSupportedException(
$"IP address family {ip.AddressFamily} is not supported, expected only IPv4 (InterNetwork) or IPv6 (InterNetworkV6).");
}

private static bool IsPrivateIPv4(IReadOnlyList<byte> ipv4Bytes)
{
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB();

// Class C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168;

// Class B private range: 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
bool IsClassB() => ipv4Bytes[0] == 172 && ipv4Bytes[1] >= 16 && ipv4Bytes[1] <= 31;

// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
bool IsClassA() => ipv4Bytes[0] == 10;

// Link local (no IP assigned by DHCP): 169.254.0.0 to 169.254.255.255 (169.254.0.0/16)
bool IsLinkLocal() => ipv4Bytes[0] == 169 && ipv4Bytes[1] == 254;
}
}
113 changes: 113 additions & 0 deletions src/Primitives/Primitives/test/Extensions/IpAddressExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
namespace ClickView.Extensions.Primitives.Tests.Extensions;

using System.Net;
using Primitives.Extensions;
using Xunit;

// Thanks https://gist.github.com/angularsen/f77b53ee9966fcd914025e25a2b3a085
public class IpAddressExtensionsTests
{
[Theory]
[InlineData("1.1.1.1")] // Cloudflare DNS
[InlineData("8.8.8.8")] // Google DNS
[InlineData("20.112.52.29")] // microsoft.com
public void IsPrivate_PublicIPv4_ReturnsFalse(string ip)
{
var ipAddress = IPAddress.Parse(ip);

Assert.False(ipAddress.IsPrivate());
}

[Theory]
[InlineData("::ffff:1.1.1.1")] // Cloudflare DNS
[InlineData("::ffff:8.8.8.8")] // Google DNS
[InlineData("::ffff:20.112.52.29")] // microsoft.com
public void IsPrivate_PublicIPv4MappedToIPv6_ReturnsFalse(string ip)
{
var ipAddress = IPAddress.Parse(ip);

Assert.False(ipAddress.IsPrivate());
}

[Theory]
// Loopback IPv4 127.0.0.1 - 127.255.255.255 (127.0.0.0/8)
[InlineData("127.0.0.1")]
[InlineData("127.10.20.30")]
[InlineData("127.255.255.255")]
// Class A private IP 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
[InlineData("10.0.0.0")]
[InlineData("10.20.30.40")]
[InlineData("10.255.255.255")]
// Class B private IP 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
[InlineData("172.16.0.0")]
[InlineData("172.20.30.40")]
[InlineData("172.31.255.255")]
// Class C private IP 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
[InlineData("192.168.0.0")]
[InlineData("192.168.30.40")]
[InlineData("192.168.255.255")]
// Link local (169.254.x.x)
[InlineData("169.254.0.0")]
[InlineData("169.254.30.40")]
[InlineData("169.254.255.255")]
public void IsPrivate_PrivateIPv4_ReturnsTrue(string ip)
{
var ipAddress = IPAddress.Parse(ip);

Assert.True(ipAddress.IsPrivate());
}

[Theory]
// Loopback IPv4 127.0.0.1 - 127.255.255.254 (127.0.0.0/8)
[InlineData("::ffff:127.0.0.1")]
[InlineData("::ffff:127.10.20.30")]
[InlineData("::ffff:127.255.255.254")]
// Class A private IP 10.0.0.0 – 10.255.255.255 (10.0.0.0/8)
[InlineData("::ffff:10.0.0.0")]
[InlineData("::ffff:10.20.30.40")]
[InlineData("::ffff:10.255.255.255")]
// Class B private IP 172.16.0.0 – 172.31.255.255 (172.16.0.0/12)
[InlineData("::ffff:172.16.0.0")]
[InlineData("::ffff:172.20.30.40")]
[InlineData("::ffff:172.31.255.255")]
// Class C private IP 192.168.0.0 – 192.168.255.255 (192.168.0.0/16)
[InlineData("::ffff:192.168.0.0")]
[InlineData("::ffff:192.168.30.40")]
[InlineData("::ffff:192.168.255.255")]
// Link local (169.254.x.x)
[InlineData("::ffff:169.254.0.0")]
[InlineData("::ffff:169.254.30.40")]
[InlineData("::ffff:169.254.255.255")]
public void IsPrivate_PrivateIPv4MappedToIPv6_ReturnsTrue(string ip)
{
var ipAddress = IPAddress.Parse(ip);

Assert.True(ipAddress.IsPrivate());
}

[Theory]
[InlineData("::1")] // Loopback
[InlineData("fe80::")] // Link local
[InlineData("fe80:1234:5678::1")] // Link local
[InlineData("fc00::")] // Unique local, globally assigned.
[InlineData("fc00:1234:5678::1")] // Unique local, globally assigned.
[InlineData("fd00::")] // Unique local, locally assigned.
[InlineData("fd12:3456:789a::1")] // Unique local, locally assigned.
public void IsPrivate_PrivateIPv6_ReturnsTrue(string ip)
{
var ipAddress = IPAddress.Parse(ip);

Assert.True(ipAddress.IsPrivate());
}

[Theory]
[InlineData("2606:4700:4700::64")] // Cloudflare DNS
[InlineData("2001:4860:4860::8888")] // Google DNS
[InlineData("2001:0db8:85a3:0000:0000:8a2e:0370:7334")] // Commonly used example.
public void IsPrivate_PublicIPv6_ReturnsFalse(string ip)
{
var ipAddress = IPAddress.Parse(ip);

Assert.False(ipAddress.IsPrivate());
}
}

0 comments on commit 2317cf1

Please sign in to comment.