From d94f7325ab93809bf2de49c2e783a43c9c56384f Mon Sep 17 00:00:00 2001 From: MrSmoke <709976+MrSmoke@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:20:20 +1100 Subject: [PATCH 1/2] Add IPAddress.IsPrivate() extension method --- .../ClickView.Extensions.Primitives.csproj | 2 +- .../src/Extensions/IpAddressExtensions.cs | 64 ++++++++++ .../Extensions/IpAddressExtensionsTests.cs | 113 ++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs create mode 100644 src/Primitives/Primitives/test/Extensions/IpAddressExtensionsTests.cs diff --git a/src/Primitives/Primitives/src/ClickView.Extensions.Primitives.csproj b/src/Primitives/Primitives/src/ClickView.Extensions.Primitives.csproj index 1ea381b..3f39062 100644 --- a/src/Primitives/Primitives/src/ClickView.Extensions.Primitives.csproj +++ b/src/Primitives/Primitives/src/ClickView.Extensions.Primitives.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.0;net6.0 primitives diff --git a/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs b/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs new file mode 100644 index 0000000..0b208ad --- /dev/null +++ b/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs @@ -0,0 +1,64 @@ +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!) + /// + /// Returns true if the IP address is in a private range.
+ /// 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").
+ /// IPv6: Loopback, link local, site local, unique local and private IPv4 mapped to IPv6.
+ ///
+ /// The IP address. + /// True if the IP address was in a private range. + /// Thrown if the ip address is not or + /// bool isPrivate = IPAddress.Parse("127.0.0.1").IsPrivate(); + public static bool IsPrivate(this IPAddress 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 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; + } +} diff --git a/src/Primitives/Primitives/test/Extensions/IpAddressExtensionsTests.cs b/src/Primitives/Primitives/test/Extensions/IpAddressExtensionsTests.cs new file mode 100644 index 0000000..ccc5a81 --- /dev/null +++ b/src/Primitives/Primitives/test/Extensions/IpAddressExtensionsTests.cs @@ -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()); + } +} From d74e5030e2cdfdeae2b4063adbdb5d5364dd08cf Mon Sep 17 00:00:00 2001 From: MrSmoke <709976+MrSmoke@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:24:51 +1100 Subject: [PATCH 2/2] add null check --- .../Primitives/src/Extensions/IpAddressExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs b/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs index 0b208ad..e7e7581 100644 --- a/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs +++ b/src/Primitives/Primitives/src/Extensions/IpAddressExtensions.cs @@ -19,6 +19,9 @@ public static class IpAddressExtensions /// bool isPrivate = IPAddress.Parse("127.0.0.1").IsPrivate(); 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();