-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #65 from clickviewapp/IpAddress-IsPrivate
Add IPAddress.IsPrivate() extension method
- Loading branch information
Showing
3 changed files
with
181 additions
and
1 deletion.
There are no files selected for viewing
2 changes: 1 addition & 1 deletion
2
src/Primitives/Primitives/src/ClickView.Extensions.Primitives.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
113
src/Primitives/Primitives/test/Extensions/IpAddressExtensionsTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |