diff --git a/src/changes/changes.xml b/src/changes/changes.xml index da1552f31..87a84774a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -63,6 +63,7 @@ The type attribute can be add,update,fix,remove. + Preserve IPv4-mapped IPv6 addresses in SubnetUtils6 parsing and formatting. Reject CR/LF in SimpleSMTPHeader and SimpleNNTPHeader fields #397. Reject CR/LF in IMAP quoteMailboxName (#399). Reject CR/LF in FTP, POP3, NNTP and SMTP command senders (#400). diff --git a/src/main/java/org/apache/commons/net/util/SubnetUtils6.java b/src/main/java/org/apache/commons/net/util/SubnetUtils6.java index 7670f0845..e8010e931 100644 --- a/src/main/java/org/apache/commons/net/util/SubnetUtils6.java +++ b/src/main/java/org/apache/commons/net/util/SubnetUtils6.java @@ -17,6 +17,7 @@ package org.apache.commons.net.util; import java.math.BigInteger; +import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; @@ -191,7 +192,8 @@ public String toString() { private static String format(final BigInteger addr) { final byte[] bytes = toByteArray16(addr); try { - return InetAddress.getByAddress(bytes).getHostAddress(); + // Inet6Address.getByAddress keeps IPv4-mapped addresses (::ffff:0:0/96) as IPv6; InetAddress.getByAddress would convert them to IPv4. + return Inet6Address.getByAddress(null, bytes, -1).getHostAddress(); } catch (final UnknownHostException e) { // Should never happen with a valid 16-byte array throw new IllegalStateException("Unexpected error formatting IPv6 address", e); @@ -233,6 +235,14 @@ private static byte[] toBytes(final String address) { if (inetAddr instanceof Inet6Address) { return inetAddr.getAddress(); } + if (inetAddr instanceof Inet4Address && address.indexOf(':') >= 0) { + // The JDK parses IPv4-mapped IPv6 literals such as "::ffff:192.0.2.1" to an Inet4Address. + final byte[] mapped = new byte[16]; + mapped[10] = (byte) 0xff; + mapped[11] = (byte) 0xff; + System.arraycopy(inetAddr.getAddress(), 0, mapped, 12, 4); + return mapped; + } throw new IllegalArgumentException(String.format(PARSE_FAIL, address) + " - not an IPv6 address"); } catch (final UnknownHostException e) { throw new IllegalArgumentException(String.format(PARSE_FAIL, address), e); diff --git a/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java b/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java index 066b8a68b..492018c98 100644 --- a/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java +++ b/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java @@ -171,6 +171,23 @@ void testInvalidIPv4Address() { assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("192.168.1.1/24")); } + @Test + void testIPv4MappedAddresses() { + final SubnetInfo info = new SubnetUtils6("::ffff:0:0/96").getInfo(); + assertEquals("0:0:0:0:0:ffff:0:0", info.getNetworkAddress()); + assertEquals("0:0:0:0:0:ffff:ffff:ffff", info.getHighAddress()); + assertTrue(info.isInRange("::ffff:192.0.2.1")); + assertTrue(info.isInRange("::ffff:c000:201")); + assertFalse(info.isInRange("::192.0.2.1")); + assertEquals("0:0:0:0:0:ffff:c000:201/128", new SubnetUtils6("::ffff:192.0.2.1/128").getInfo().getCidrSignature()); + // A high address that falls in the IPv4-mapped range must be formatted as IPv6 and round-trip. + final SubnetInfo low = new SubnetUtils6("::/80").getInfo(); + assertEquals("0:0:0:0:0:ffff:ffff:ffff", low.getHighAddress()); + assertTrue(low.isInRange(low.getHighAddress())); + // Plain IPv4 text is still rejected. + assertThrows(IllegalArgumentException.class, () -> info.isInRange("192.0.2.1")); + } + @Test void testInvalidTwoArgConstructor() { assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1", 129));