diff --git a/src/System.Net.IPNetwork/IPNetwork2Members.cs b/src/System.Net.IPNetwork/IPNetwork2Members.cs index c5116d95..ca19686f 100644 --- a/src/System.Net.IPNetwork/IPNetwork2Members.cs +++ b/src/System.Net.IPNetwork/IPNetwork2Members.cs @@ -96,6 +96,7 @@ public IPAddress Broadcast /// /// Gets first usable IPAddress in Network. + /// Per RFC 3021, /31 networks have no reserved network address. /// public IPAddress FirstUsable { @@ -103,16 +104,18 @@ public IPAddress FirstUsable { BigInteger first = this.InternalNetwork; if (this.family == AddressFamily.InterNetwork - && this.Usable > 1) + && this.cidr < 31) { - first+= 1; + first += 1; } + return ToIPAddress(first, this.family); } } /// /// Gets last usable IPAddress in Network. + /// Per RFC 3021, /31 networks have no reserved broadcast address. /// public IPAddress LastUsable { @@ -120,10 +123,11 @@ public IPAddress LastUsable { BigInteger last = this.InternalBroadcast; if (this.family == AddressFamily.InterNetwork - && this.Usable > 1) + && this.cidr < 31) { last -= 1; } + return ToIPAddress(last, this.family); } } diff --git a/src/TestProject/IPNetworkTest/IPNetworkSlash31Tests.cs b/src/TestProject/IPNetworkTest/IPNetworkSlash31Tests.cs new file mode 100644 index 00000000..ef036b2e --- /dev/null +++ b/src/TestProject/IPNetworkTest/IPNetworkSlash31Tests.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) IPNetwork. All rights reserved. +// + +namespace TestProject.IPNetworkTest; + +/// +/// Tests for /31 networks (point-to-point links) as per RFC 3021. +/// See GitHub issue #369: https://github.com/lduchosal/ipnetwork/issues/369 +/// +[TestClass] +public class IPNetworkSlash31Tests +{ + /// + /// Tests that FirstUsable and LastUsable are correct for /31 networks. + /// According to RFC 3021, /31 networks have 2 usable addresses with no + /// network or broadcast addresses reserved. + /// + /// Bug: In version 2.6, FirstUsable and LastUsable are inverted for /31 networks. + /// Example: 167.92.212.82/31 + /// Current (Wrong): FirstUsable = 167.92.212.83, LastUsable = 167.92.212.82 + /// Expected: FirstUsable = 167.92.212.82, LastUsable = 167.92.212.83 + /// + [TestMethod] + [TestCategory("Parse")] + [TestCategory("RFC3021")] + public void TestSlash31_FirstUsable_LastUsable_Issue369() + { + // Arrange - using the exact example from issue #369 + string ipaddress = "167.92.212.82/31"; + + // Expected values per RFC 3021 + string expectedNetwork = "167.92.212.82"; + string expectedNetmask = "255.255.255.254"; + string expectedBroadcast = "167.92.212.83"; + string expectedFirst = "167.92.212.82"; + string expectedLast = "167.92.212.83"; + string expectedFirstUsable = "167.92.212.82"; + string expectedLastUsable = "167.92.212.83"; + byte expectedCidr = 31; + uint expectedUsable = 2; + + // Act + var ipnetwork = IPNetwork2.Parse(ipaddress); + + // Assert + Assert.AreEqual(expectedNetwork, ipnetwork.Network.ToString(), "Network"); + Assert.AreEqual(expectedNetmask, ipnetwork.Netmask.ToString(), "Netmask"); + Assert.AreEqual(expectedBroadcast, ipnetwork.Broadcast.ToString(), "Broadcast"); + Assert.AreEqual(expectedFirst, ipnetwork.First.ToString(), "First"); + Assert.AreEqual(expectedLast, ipnetwork.Last.ToString(), "Last"); + Assert.AreEqual(expectedCidr, ipnetwork.Cidr, "Cidr"); + Assert.AreEqual(expectedUsable, ipnetwork.Usable, "Usable"); + Assert.AreEqual(expectedFirstUsable, ipnetwork.FirstUsable.ToString(), "FirstUsable"); + Assert.AreEqual(expectedLastUsable, ipnetwork.LastUsable.ToString(), "LastUsable"); + + // Critical assertion: FirstUsable must be <= LastUsable + Assert.IsTrue( + ipnetwork.FirstUsable.ToString().CompareTo(ipnetwork.LastUsable.ToString()) <= 0 || + ipnetwork.FirstUsable.Equals(ipnetwork.LastUsable), + "FirstUsable must be less than or equal to LastUsable"); + } + + /// + /// Tests another /31 network to ensure the fix works for any /31 subnet. + /// + [TestMethod] + [TestCategory("Parse")] + [TestCategory("RFC3021")] + public void TestSlash31_AnotherNetwork() + { + // Arrange + string ipaddress = "10.0.0.0/31"; + + string expectedFirstUsable = "10.0.0.0"; + string expectedLastUsable = "10.0.0.1"; + uint expectedUsable = 2; + + // Act + var ipnetwork = IPNetwork2.Parse(ipaddress); + + // Assert + Assert.AreEqual(expectedUsable, ipnetwork.Usable, "Usable"); + Assert.AreEqual(expectedFirstUsable, ipnetwork.FirstUsable.ToString(), "FirstUsable"); + Assert.AreEqual(expectedLastUsable, ipnetwork.LastUsable.ToString(), "LastUsable"); + } + + /// + /// Tests that /32 networks still work correctly (host route - single address). + /// + [TestMethod] + [TestCategory("Parse")] + public void TestSlash32_StillWorksCorrectly() + { + // Arrange + string ipaddress = "192.168.1.1/32"; + + string expectedFirstUsable = "192.168.1.1"; + string expectedLastUsable = "192.168.1.1"; + uint expectedUsable = 1; + + // Act + var ipnetwork = IPNetwork2.Parse(ipaddress); + + // Assert + Assert.AreEqual(expectedUsable, ipnetwork.Usable, "Usable"); + Assert.AreEqual(expectedFirstUsable, ipnetwork.FirstUsable.ToString(), "FirstUsable"); + Assert.AreEqual(expectedLastUsable, ipnetwork.LastUsable.ToString(), "LastUsable"); + } + + /// + /// Tests that /30 networks still work correctly (traditional smallest subnet with usable hosts). + /// + [TestMethod] + [TestCategory("Parse")] + public void TestSlash30_StillWorksCorrectly() + { + // Arrange + string ipaddress = "192.168.1.0/30"; + + string expectedNetwork = "192.168.1.0"; + string expectedBroadcast = "192.168.1.3"; + string expectedFirstUsable = "192.168.1.1"; + string expectedLastUsable = "192.168.1.2"; + uint expectedUsable = 2; + + // Act + var ipnetwork = IPNetwork2.Parse(ipaddress); + + // Assert + Assert.AreEqual(expectedNetwork, ipnetwork.Network.ToString(), "Network"); + Assert.AreEqual(expectedBroadcast, ipnetwork.Broadcast.ToString(), "Broadcast"); + Assert.AreEqual(expectedUsable, ipnetwork.Usable, "Usable"); + Assert.AreEqual(expectedFirstUsable, ipnetwork.FirstUsable.ToString(), "FirstUsable"); + Assert.AreEqual(expectedLastUsable, ipnetwork.LastUsable.ToString(), "LastUsable"); + } +}