Last active
November 13, 2023 11:44
-
-
Save angularsen/f77b53ee9966fcd914025e25a2b3a085 to your computer and use it in GitHub Desktop.
IsPrivate extension method for IPAddress
This file contains hidden or 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
using System; | |
using System.Net; | |
using System.Net.Sockets; | |
namespace MyNamespace | |
{ | |
/// <summary> | |
/// Extension methods on <see cref="System.Net.IPAddress"/>. | |
/// </summary> | |
public static class IPAddressExtensions | |
{ | |
/// <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> | |
/// <example><code>bool isPrivate = IPAddress.Parse("127.0.0.1").IsPrivate();</code></example> | |
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(byte[] ipv4Bytes) | |
{ | |
// 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; | |
// Class A private range: 10.0.0.0 – 10.255.255.255 (10.0.0.0/8) | |
bool IsClassA() => ipv4Bytes[0] == 10; | |
// 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 C private range: 192.168.0.0 – 192.168.255.255 (192.168.0.0/16) | |
bool IsClassC() => ipv4Bytes[0] == 192 && ipv4Bytes[1] == 168; | |
return IsLinkLocal() || IsClassA() || IsClassC() || IsClassB(); | |
} | |
} | |
} |
This file contains hidden or 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
using System.Diagnostics.CodeAnalysis; | |
using System.Net; | |
using MyNamespace; | |
using FluentAssertions; | |
using Xunit; | |
namespace MyNamespace.Tests | |
{ | |
[SuppressMessage("ReSharper", "InvokeAsExtensionMethod")] | |
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_ReturnsFalse_PublicIPv4(string ip) | |
{ | |
var ipAddress = IPAddress.Parse(ip); | |
IPAddressExtensions.IsPrivate(ipAddress).Should().BeFalse(); | |
} | |
[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_ReturnsFalse_PublicIPv4MappedToIPv6(string ip) | |
{ | |
var ipAddress = IPAddress.Parse(ip); | |
IPAddressExtensions.IsPrivate(ipAddress).Should().BeFalse(); | |
} | |
[Theory] | |
[InlineData("127.0.0.1" )] // Loopback IPv4 127.0.0.1 - 127.255.255.255 (127.0.0.0/8) | |
[InlineData("127.10.20.30" )] | |
[InlineData("127.255.255.255")] | |
[InlineData("10.0.0.0" )] // Class A private IP 10.0.0.0 – 10.255.255.255 (10.0.0.0/8) | |
[InlineData("10.20.30.40" )] | |
[InlineData("10.255.255.255" )] | |
[InlineData("172.16.0.0" )] // Class B private IP 172.16.0.0 – 172.31.255.255 (172.16.0.0/12) | |
[InlineData("172.20.30.40" )] | |
[InlineData("172.31.255.255" )] | |
[InlineData("192.168.0.0" )] // Class C private IP 192.168.0.0 – 192.168.255.255 (192.168.0.0/16) | |
[InlineData("192.168.30.40" )] | |
[InlineData("192.168.255.255")] | |
[InlineData("169.254.0.0" )] // Link local (169.254.x.x) | |
[InlineData("169.254.30.40" )] | |
[InlineData("169.254.255.255")] | |
public void IsPrivate_ReturnsTrue_PrivateIPv4(string ip) | |
{ | |
var ipAddress = IPAddress.Parse(ip); | |
IPAddressExtensions.IsPrivate(ipAddress).Should().BeTrue(); | |
} | |
[Theory] | |
[InlineData("::ffff:127.0.0.1" )] // Loopback IPv4 127.0.0.1 - 127.255.255.254 (127.0.0.0/8) | |
[InlineData("::ffff:127.10.20.30" )] | |
[InlineData("::ffff:127.255.255.254")] | |
[InlineData("::ffff:10.0.0.0" )] // Class A private IP 10.0.0.0 – 10.255.255.255 (10.0.0.0/8) | |
[InlineData("::ffff:10.20.30.40" )] | |
[InlineData("::ffff:10.255.255.255" )] | |
[InlineData("::ffff:172.16.0.0" )] // Class B private IP 172.16.0.0 – 172.31.255.255 (172.16.0.0/12) | |
[InlineData("::ffff:172.20.30.40" )] | |
[InlineData("::ffff:172.31.255.255" )] | |
[InlineData("::ffff:192.168.0.0" )] // Class C private IP 192.168.0.0 – 192.168.255.255 (192.168.0.0/16) | |
[InlineData("::ffff:192.168.30.40" )] | |
[InlineData("::ffff:192.168.255.255")] | |
[InlineData("::ffff:169.254.0.0" )] // Link local (169.254.x.x) | |
[InlineData("::ffff:169.254.30.40" )] | |
[InlineData("::ffff:169.254.255.255")] | |
public void IsPrivate_ReturnsTrue_PrivateIPv4MappedToIPv6(string ip) | |
{ | |
var ipAddress = IPAddress.Parse(ip); | |
IPAddressExtensions.IsPrivate(ipAddress).Should().BeTrue(); | |
} | |
[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_ReturnsTrue_PrivateIPv6(string ip) | |
{ | |
var ipAddress = IPAddress.Parse(ip); | |
IPAddressExtensions.IsPrivate(ipAddress).Should().BeTrue(); | |
} | |
[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_ReturnsFalse_PublicIPv6(string ip) | |
{ | |
var ipAddress = IPAddress.Parse(ip); | |
IPAddressExtensions.IsPrivate(ipAddress).Should().BeFalse(); | |
} | |
} | |
} |
@tskarman Thank you, I updated the gist.
These inline data parameters do not match the assertion in method IsPrivate_ReturnsTrue_PrivateIPv6
[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.
@Zakhar32 I don't follow, all those inline data parameters are different kinds of private IPs are they not?
And the assertion says IsPrivate(ipAddress).Should().BeTrue()
, which matches the method name IsPrivate_ReturnsTrue_PrivateIPv6
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Line 37 should check for
NET6_0_OR_GREATER
instead ofNET6_0
to avoid regressions when targeting newer frameworks.That of course would be caught by your tests under most circumstances but I still wanted to point out the fact/fix.