1
0

Added some IP address improvements

This commit is contained in:
2025-11-17 18:57:21 +01:00
parent feed3d5427
commit 48c30b5b83
3 changed files with 224 additions and 2 deletions

View File

@@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `ContainsAny()` and `ContainsAll()` for strings
- `ASPNETCORE_APPL_PROXY` environment variable can be used on proxy configuration
- Support for .NET 10.0 LTS
- `ToCleanString()` for `IPAddresses` mapped IPv4 to IPv6
- `ToSubnetMask()` to generated IPv4 subnet masks from CIDR notation
- `ObfuscateIpAddress()` to mask sensidive parts of an IP address (e.g. for listing in logs)
### Changed

View File

@@ -1,4 +1,7 @@
namespace System.Net
using System.Linq;
using System.Net.Sockets;
namespace System.Net
{
/// <summary>
/// Provides extension methods for <see cref="IPAddress"/>es.
@@ -48,5 +51,90 @@
return new IPAddress(bytes);
}
/// <summary>
/// Converts an <see cref="IPAddress"/> to a clean string representation.
/// </summary>
/// <param name="ipAddress">The <see cref="IPAddress"/> to convert.</param>
/// <returns>A clean string representation of the <see cref="IPAddress"/>.</returns>
public static string ToCleanString(this IPAddress ipAddress)
{
string address = ipAddress.ToString();
if (ipAddress.IsIPv4MappedToIPv6)
address = ipAddress.MapToIPv4().ToString();
return address;
}
/// <summary>
/// Converts a prefix length to a subnet mask (IPv4 only).
/// </summary>
/// <param name="prefixLength">The prefix length.</param>
public static IPAddress ToSubnetMask(this int prefixLength)
{
if (prefixLength < 0 || prefixLength > 32)
throw new ArgumentOutOfRangeException(nameof(prefixLength));
byte[] bytes = new byte[4];
for (int i = 0; i < prefixLength; i++)
{
int byteIndex = i / 8;
int bitIndex = 7 - (i % 8);
bytes[byteIndex] |= (byte)(1 << bitIndex);
}
return new IPAddress(bytes);
}
/// <summary>
/// Obfuscates an IP address by masking portions of it to conceal sensitive information.
/// </summary>
/// <remarks>
/// This method replaces parts of the IP address with a masking character to help prevent exposure of
/// full address details in logs or user interfaces. For IPv4-mapped IPv6 addresses, the address is first converted to
/// its IPv4 equivalent before obfuscation. Loopback addresses (<c>127.0.0.1</c> and <c>::1</c>) are not obfuscated.
/// </remarks>
/// <param name="address">The IP address to obfuscate.</param>
/// <param name="obfuscation">The char (or string) to obfuscate with.</param>
/// <returns>
/// A string representation of the obfuscated IP address. For loopback addresses, the original address is returned unmodified.
/// </returns>
public static string ObfuscateIpAddress(this IPAddress address, string obfuscation = "•")
{
string[] addressParts;
string delimiter;
switch (address.AddressFamily)
{
case AddressFamily.InterNetwork:
if (address.ToString() == "127.0.0.1")
return address.ToString();
delimiter = ".";
addressParts = address.ToString().Split(delimiter.First());
break;
case AddressFamily.InterNetworkV6:
if (address.IsIPv4MappedToIPv6)
return address.MapToIPv4().ObfuscateIpAddress(obfuscation);
if (address.ToString() == "::1")
return address.ToString();
delimiter = ":";
addressParts = address.ToString().Split(delimiter.First());
break;
default:
return address.ToString();
}
for (int i = 0; i < addressParts.Length; i++)
{
if (i % 2 == 0)
addressParts[i] = obfuscation;
}
return string.Join(delimiter, addressParts);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Net;
using System;
using System.Net;
namespace AMWD.Common.Tests.Extensions
{
@@ -82,5 +83,135 @@ namespace AMWD.Common.Tests.Extensions
// assert
Assert.AreEqual("255.255.255.255", decremented.ToString());
}
[TestMethod]
public void ShouldReturnCleanMappedIPAddressString()
{
// arrange
var mapped = IPAddress.Parse("::ffff:192.168.0.1");
// act
string cleaned = mapped.ToCleanString();
// assert
Assert.AreEqual("192.168.0.1", cleaned);
}
[TestMethod]
public void ShouldReturnCleanIPv6String()
{
// arrange
var ipv6 = IPAddress.Parse("2001:db8::1");
// act / assert
Assert.AreEqual("2001:db8::1", ipv6.ToCleanString());
}
[TestMethod]
[DataRow(0, "0.0.0.0")]
[DataRow(32, "255.255.255.255")]
[DataRow(8, "255.0.0.0")]
[DataRow(12, "255.240.0.0")]
[DataRow(24, "255.255.255.0")]
[DataRow(23, "255.255.254.0")]
public void ShouldReturnSubnetMask(int cidr, string expected)
{
// arrange
// act
string mask = cidr.ToSubnetMask().ToString();
// assert
Assert.AreEqual(expected, mask);
}
[TestMethod]
[DataRow(-1)]
[DataRow(33)]
public void ShouldThrowArgumentOutOfRangeExceptionForInvalidSubnetMask(int cidr)
{
// arrange
// act & assert
Assert.ThrowsExactly<ArgumentOutOfRangeException>(() => cidr.ToSubnetMask());
}
[TestMethod]
public void ShouldObfuscateIPv4()
{
// arrange
var ip = IPAddress.Parse("192.168.0.100");
// act
string ob = ip.ObfuscateIpAddress();
// assert
Assert.AreEqual("•.168.•.100", ob);
}
[TestMethod]
public void ShouldObfuscateIpAddressWithCustomString()
{
// arrange
var ip = IPAddress.Parse("10.0.0.5");
// act
string ob = ip.ObfuscateIpAddress("X");
// assert
Assert.AreEqual("X.0.X.5", ob);
}
[TestMethod]
public void ShouldNotObfuscateLocalhostIPv4()
{
// arrange
var ip = IPAddress.Parse("127.0.0.1");
// act
string ob = ip.ObfuscateIpAddress();
// assert
Assert.AreEqual("127.0.0.1", ob);
}
[TestMethod]
public void ShouldObfuscatedMappedIP()
{
// arrange
var ip = IPAddress.Parse("::ffff:10.0.0.5");
// act
string ob = ip.ObfuscateIpAddress("X");
// assert
Assert.AreEqual("X.0.X.5", ob);
}
[TestMethod]
public void ShouldObfuscateIPv6()
{
// arrange
var ip = IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334");
// act
string ob = ip.ObfuscateIpAddress();
// assert
Assert.AreEqual("•:db8:•::•:370:•", ob);
}
[TestMethod]
public void ShouldNotObfuscateLocalhostIPv6()
{
// arrange
var ip = IPAddress.Parse("::1");
// act
string ob = ip.ObfuscateIpAddress();
// assert
Assert.AreEqual("::1", ob);
}
}
}