From 48c30b5b834b15f7b36ed00cc08da18a1e36cb69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20M=C3=BCller?= Date: Mon, 17 Nov 2025 18:57:21 +0100 Subject: [PATCH] Added some IP address improvements --- CHANGELOG.md | 3 + .../Extensions/IPAddressExtensions.cs | 90 +++++++++++- .../Extensions/IPAddressExtensionsTest.cs | 133 +++++++++++++++++- 3 files changed, 224 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c777b..6e1b4f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/AMWD.Common/Extensions/IPAddressExtensions.cs b/src/AMWD.Common/Extensions/IPAddressExtensions.cs index 71532dd..bb32f29 100644 --- a/src/AMWD.Common/Extensions/IPAddressExtensions.cs +++ b/src/AMWD.Common/Extensions/IPAddressExtensions.cs @@ -1,4 +1,7 @@ -namespace System.Net +using System.Linq; +using System.Net.Sockets; + +namespace System.Net { /// /// Provides extension methods for es. @@ -48,5 +51,90 @@ return new IPAddress(bytes); } + + /// + /// Converts an to a clean string representation. + /// + /// The to convert. + /// A clean string representation of the . + public static string ToCleanString(this IPAddress ipAddress) + { + string address = ipAddress.ToString(); + if (ipAddress.IsIPv4MappedToIPv6) + address = ipAddress.MapToIPv4().ToString(); + + return address; + } + + /// + /// Converts a prefix length to a subnet mask (IPv4 only). + /// + /// The prefix length. + 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); + } + + /// + /// Obfuscates an IP address by masking portions of it to conceal sensitive information. + /// + /// + /// 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 (127.0.0.1 and ::1) are not obfuscated. + /// + /// The IP address to obfuscate. + /// The char (or string) to obfuscate with. + /// + /// A string representation of the obfuscated IP address. For loopback addresses, the original address is returned unmodified. + /// + 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); + } } } diff --git a/test/AMWD.Common.Tests/Extensions/IPAddressExtensionsTest.cs b/test/AMWD.Common.Tests/Extensions/IPAddressExtensionsTest.cs index bc97e03..fc22b1b 100644 --- a/test/AMWD.Common.Tests/Extensions/IPAddressExtensionsTest.cs +++ b/test/AMWD.Common.Tests/Extensions/IPAddressExtensionsTest.cs @@ -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(() => 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); + } } }