Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NetUtil IPv6 bugs related to IPv4 and compression #6613

Closed
wants to merge 1 commit into from

Conversation

Scottmitch
Copy link
Member

Motivation:
NetUtil#getByName and NetUtil#isValidIpV6Address do not strictly enforce the format of IPv4 addresses that are allowed to be embedded in IPv6 addresses as specified in https://tools.ietf.org/html/rfc4291#section-2.5.5. This may lead to invalid addresses being parsed, or invalid addresses being considered valid.

Modifications:

Result:
NetUtil#getByName and NetUtil#isValidIpV6Address respect the IPv6 RFC which defines the valid formats for embedding IPv4 addresses.

@Scottmitch Scottmitch added this to the 4.0.46.Final milestone Apr 6, 2017
@Scottmitch Scottmitch self-assigned this Apr 6, 2017
@Scottmitch Scottmitch requested a review from trustin April 6, 2017 23:30
@Scottmitch
Copy link
Member Author

@trustin @fenik17 - PTAL. Please suggest additional unit tests if you think of interesting test cases.

* </ul>
* @return byte array representation of the {@code ip} or {@code null} if not a valid IP address.
*/
private static byte[] getIPv6ByName(CharSequence ip, boolean ipv4Mapped) {
Copy link

@netkins netkins Apr 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR The Cyclomatic Complexity of this method "getIPv6ByName" is 93 which is greater than 25 authorized. rule

@fenik17
Copy link
Contributor

fenik17 commented Apr 7, 2017

Can we add support for IPv6 addresses with [] and % into a getIPv6ByName() method? Something like that:

public static byte[] createByteArrayFromIpAddressString(String ipAddressString) {
    if (isValidIpV4Address(ipAddressString)) {
        ....
    }
    return getIPv6ByName(ipAddressString, true);
}

private static byte[] getIPv6ByName(CharSequence ip, boolean ipv4Mapped) {
    ...
    int startIdx; // instead of '0' below
    int endIdx;   // instead of 'ipLength - 1' below
    if (ip.charAt(0) == '[') {
        startIdx = 1;
        endIdx = ip.length() - 2;
    } else {
        startIdx = 0;
        endIdx = ip.length() - 1;
    }
    ...
    loop: for (int i = startIdx; i <= endIdx; i++) {
        ...
        case '%':
            endIdx = i - 1;
            break loop;
        }
        ...
    }
    ...
    if (startIdx == 1 && (ipv6Seperators == 0 || ip.charAt(ip.length() - 1) != ']')) {
        // "[x.x.x.x]" - invalid ipv4
        // "[x:x:x::x" - invalid ipv6
        return null;
    }
    ...
}

This will allow us not to call a isValidIpV6Address() method in the createByteArrayFromIpAddressString(), and make getByName more universal. Also, we will be able to test getByName() on a par with the isValidIpV4Address() and isValidIpV6Address():

    @Test
    public void testIsValidIpV4Address() {
        for (String host : validIpV4Hosts.keySet()) {
            assertTrue(host, NetUtil.isValidIpV4Address(host));
            assertNotNull(host, NetUtil.getByName(host));
        }
        for (String host : invalidIpV4Hosts.keySet()) {
            assertFalse(host, NetUtil.isValidIpV4Address(host));
            assertNull(host, NetUtil.getByName(host));
        }
    }

    @Test
    public void testIsValidIpV6Address() {
        for (String host : validIpV6Hosts.keySet()) {
            assertTrue(host, NetUtil.isValidIpV6Address(host));
            assertNotNull(host, NetUtil.getByName(host));
        }
        for (String host : invalidIpV6Hosts.keySet()) {
            assertFalse(host, NetUtil.isValidIpV6Address(host));
            assertNull(host, NetUtil.getByName(host));
        }
    }

@fenik17
Copy link
Contributor

fenik17 commented Apr 7, 2017

In addition to the previous comment - InetAddress.getByName() from the JDK supports an [] and %.

!isValidIPv4MappedSeparators(bytes[currentIndex - 1], bytes[currentIndex - 2])) {
return false;
}
currentIndex -= 2;
Copy link
Contributor

@fenik17 fenik17 Apr 7, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe -= 2 with an Unsafe and -= 3 without?
With -2 test failed for ip "::ffff:1.2.3.4" whithout an Unsafe.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Netkins doesn't run tests with -Dio.netty.noUnsafe=true?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch ... should be -=3 ... the indexing is off the for unsafe case.

Netkins doesn't run tests with -Dio.netty.noUnsafe=true?

No. It just assumes the default for the platform.

!isValidNumericChar(ip.charAt(i - 3))) ||
tmp == 2 && (!isValidNumericChar(ip.charAt(i - 1)) ||
!isValidNumericChar(ip.charAt(i - 2))) ||
tmp == 1 && !isValidNumericChar(ip.charAt(i - 1)))) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of last part need only for the ipv4Seperators == 1:

|| ( 
    ipv4Seperators == 1 &&
    (
          (!ipv4Mapped || currentIndex != 0 && !isValidIPv4Mapped(bytes, currentIndex))
       || (tmp == 3 && (!isValidNumericChar(ip.charAt(i - 1)) || !isValidNumericChar(ip.charAt(i - 2)) || !isValidNumericChar(ip.charAt(i - 3))) ||
           tmp == 2 && (!isValidNumericChar(ip.charAt(i - 1)) || !isValidNumericChar(ip.charAt(i - 2))) ||
           tmp == 1 && !isValidNumericChar(ip.charAt(i - 1)))
    )
  ) {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The validation after we see the first ipv4Separators is done in the default case below.

@fenik17
Copy link
Contributor

fenik17 commented Apr 7, 2017

@Scottmitch fix the typos ipv6Seperators -> ipv6Separators, ipv4Seperators -> ipv4Separators please :)

@Scottmitch
Copy link
Member Author

Can we add support for IPv6 addresses with [] and % into a getIPv6ByName() method?

Lets do a followup PR for this enhancement.

|| (ipv6Seperators > 0 && (currentIndex + compressLength < 12))
++ipv4Separators;
tmp = i - begin; // tmp is the length of the current segment.
if (tmp > IPV4_MAX_CHAR_BETWEEN_SEPARATOR
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR Reduce the number of conditional operators (19) used in the expression (maximum allowed 5). rule

@fenik17
Copy link
Contributor

fenik17 commented Apr 7, 2017

convertToBytes() can be removed at now.

Lets do a followup PR for this enhancement.

Ok.

@Scottmitch
Copy link
Member Author

convertToBytes() can be removed at now.

+1

@fenik17
Copy link
Contributor

fenik17 commented Apr 9, 2017

Please suggest additional unit tests if you think of interesting test cases.

0:0:0:0:0:ffff::10.0.0.1

isValidIpV6Address("0:0:0:0:0:ffff::10.0.0.1") => false
getByName("0:0:0:0:0:ffff::10.0.0.1")          => not null

Copy link
Member

@normanmaurer normanmaurer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm...

@normanmaurer
Copy link
Member

Let me merge this...

@fenik17
Copy link
Contributor

fenik17 commented Apr 21, 2017

The getByName() method still does not check IPv4-Mapped addresses, e.g. for "1:0:0:0:0:ffff:10.0.0.1" or "0:0:0:0:0:ffff::10.0.0.1" it return a not null.
But isValidIpV6Address() has such check (return false).

@fenik17
Copy link
Contributor

fenik17 commented Apr 21, 2017

I can fix it in another PR if @Scottmitch have not time..

@Scottmitch
Copy link
Member Author

@fenik17 - Thanks for pointing out this unit test .. let me add this unit test and fix the issue

compressLength = 12;
} else if (ipv6Seperators >= IPV6_MIN_SEPARATORS &&
} else if (ipv6Separators >= IPV6_MIN_SEPARATORS &&
ip.charAt(ipLength - 1) != ':' &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this check - ip.charAt(ipLength - 1) != ':' - seems superfluous or wrong for the ipv4Separators > 0.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed ... this should be caught in the loop above.

@Scottmitch Scottmitch force-pushed the netutil_bugs branch 3 times, most recently from ff2b72d to 9f0c151 Compare April 24, 2017 20:37
@fenik17
Copy link
Contributor

fenik17 commented Apr 24, 2017

One more unit test for invalid address: "1:0:0:0:0:ffff:10.0.0.1".
Problem in the isValidIPv4Mapped().

@fenik17
Copy link
Contributor

fenik17 commented Apr 24, 2017

Maybe better use equals() with zeroed array instead of getLong() / getInt() in the isValidIPv4Mapped()?

ip.charAt(ipLength - 1) != ':' &&
(!isCompressed && (ipv6Seperators == 6 && ip.charAt(0) != ':') ||
isCompressed && (ipv6Seperators + 1 < IPV6_MAX_SEPARATORS &&
} else if (ipv6Separators >= IPV6_MIN_SEPARATORS &&
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAJOR Reduce the number of conditional operators (7) used in the expression (maximum allowed 5). rule

@Scottmitch
Copy link
Member Author

Maybe better use equals() with zeroed array instead of getLong() / getInt() in the isValidIPv4Mapped()?

I considered doing this initially ... I will just add a isZero method to PlatformDependent instead to make this more generally available an independently unit tested. This way we won't need an intermediate allocation.

Motivation:
NetUtil#getByName and NetUtil#isValidIpV6Address do not strictly enforce the format of IPv4 addresses that are allowed to be embedded in IPv6 addresses as specified in https://tools.ietf.org/html/rfc4291#section-2.5.5. This may lead to invalid addresses being parsed, or invalid addresses being considered valid. Compression of a single IPv6 word was also not handled correctly if there are 7 : characters.

Modifications:
- NetUtil#isValidIpV6Address should enforce the IPv4-Compatible and IPv4-Mapped are the only valid formats for including IPv4 addresses as specified in https://tools.ietf.org/html/rfc4291#section-2.5.5
- NetUtil#getByName should more stritcly parse IPv6 addresses which contain IPv4 addresses as specified in https://tools.ietf.org/html/rfc4291#section-2.5.5
- NetUtil should allow compression even if the number of : characters is 7.
- NetUtil#createByteArrayFromIpAddressString should use the same IP string to byte[] translation which is used in NetUtil#getByName

Result:
NetUtil#getByName and NetUtil#isValidIpV6Address respect the IPv6 RFC which defines the valid formats for embedding IPv4 addresses.
@Scottmitch
Copy link
Member Author

4.1 (9cb858f) 4.0 (241cdc6)

@Scottmitch Scottmitch closed this Apr 25, 2017
@Scottmitch Scottmitch deleted the netutil_bugs branch April 25, 2017 22:31
"0:0:0:0:0:0:10.0.0.1", "00000000000000000000ffff0a000001",
"0:0:0:0:0::10.0.0.1", "00000000000000000000ffff0a000001",
"::0:0:0:0:0:10.0.0.1", "00000000000000000000ffff0a000001",
"0:0:0:0:0:0::10.0.0.1", "00000000000000000000ffff0a000001",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Scottmitch oops. Is it really valid?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we allow IPv4 compatible addresses as input. The rational is to be liberal in what we accept but more strict in what we generate (we only generate IPv4 mapped).

// We allow IPv4 Mapped (https://tools.ietf.org/html/rfc4291#section-2.5.5.1)
// and IPv4 compatible (https://tools.ietf.org/html/rfc4291#section-2.5.5.1).
// The IPv4 compatible is deprecated, but it allows parsing of plain IPv4 addressed into IPv6-Mapped addresses.

Copy link
Contributor

@fenik17 fenik17 Apr 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm about of count of zeros and "::" after. There can not be more bits in place of "::".
This is confirmed by examples [1] and some online-validators: [2] and [3].

   3. An alternative form that is sometimes more convenient when dealing
      with a mixed environment of IPv4 and IPv6 nodes is
      x:x:x:x:x:x:d.d.d.d, where the 'x's are the hexadecimal values of
      the six high-order 16-bit pieces of the address, and the 'd's are
      the decimal values of the four low-order 8-bit pieces of the
      address (standard IPv4 representation).  Examples:
         0:0:0:0:0:0:13.1.68.3
         0:0:0:0:0:FFFF:129.144.52.38
      or in compressed form:
         ::13.1.68.3
         ::FFFF:129.144.52.38

[1] https://tools.ietf.org/html/rfc4291#section-2.2
[2] http://formvalidation.io/validators/ip/ ("Try it" in the end)
[3] https://www.helpsystems.com/intermapper/ipv6-test-address-validation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out. I agree there is a bug. Let me come up with a PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #6680

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants