Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c6c2c3c authored by Lorenzo Colitti's avatar Lorenzo Colitti
Browse files

IpPrefix improvements.

1. Allow IpPrefixes to be created from strings. In order to do
   this, factor out the code from LinkAddress which already does
   this to a small utility class in NetworkUtils.
2. Truncate prefixes on creation, fixing a TODO.
3. Add a toString method.
4. Write a unit test.

While I'm at it, make RouteInfoTest pass again, and convert it
to use IpPrefix instead of LinkAddress.

Change-Id: I5f68f8af8f4aedb25afaee00e05369f01e82a70b
parent 97df96d8
Loading
Loading
Loading
Loading
+58 −12
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.net;

import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;

import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -46,9 +47,18 @@ public final class IpPrefix implements Parcelable {
    private final byte[] address;  // network byte order
    private final int prefixLength;

    private void checkAndMaskAddressAndPrefixLength() {
        if (address.length != 4 && address.length != 16) {
            throw new IllegalArgumentException(
                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
        }
        NetworkUtils.maskRawAddress(address, prefixLength);
    }

    /**
     * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in
     * network byte order and a prefix length.
     * network byte order and a prefix length. Silently truncates the address to the prefix length,
     * so for example {@code 192.0.2.1/24} is silently converted to {@code 192.0.2.0/24}.
     *
     * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long.
     * @param prefixLength the prefix length. Must be >= 0 and <= (32 or 128) (IPv4 or IPv6).
@@ -56,24 +66,46 @@ public final class IpPrefix implements Parcelable {
     * @hide
     */
    public IpPrefix(byte[] address, int prefixLength) {
        if (address.length != 4 && address.length != 16) {
            throw new IllegalArgumentException(
                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
        }
        if (prefixLength < 0 || prefixLength > (address.length * 8)) {
            throw new IllegalArgumentException("IpPrefix with " + address.length +
                    " bytes has invalid prefix length " + prefixLength);
        }
        this.address = address.clone();
        this.prefixLength = prefixLength;
        // TODO: Validate that the non-prefix bits are zero
        checkAndMaskAddressAndPrefixLength();
    }

    /**
     * Constructs a new {@code IpPrefix} from an IPv4 or IPv6 address and a prefix length. Silently
     * truncates the address to the prefix length, so for example {@code 192.0.2.1/24} is silently
     * converted to {@code 192.0.2.0/24}.
     *
     * @param address the IP address. Must be non-null.
     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
     * @hide
     */
    public IpPrefix(InetAddress address, int prefixLength) {
        this(address.getAddress(), prefixLength);
        // We don't reuse the (byte[], int) constructor because it calls clone() on the byte array,
        // which is unnecessary because getAddress() already returns a clone.
        this.address = address.getAddress();
        this.prefixLength = prefixLength;
        checkAndMaskAddressAndPrefixLength();
    }

    /**
     * Constructs a new IpPrefix from a string such as "192.0.2.1/24" or "2001:db8::1/64".
     * Silently truncates the address to the prefix length, so for example {@code 192.0.2.1/24}
     * is silently converted to {@code 192.0.2.0/24}.
     *
     * @param prefix the prefix to parse
     *
     * @hide
     */
    public IpPrefix(String prefix) {
        // We don't reuse the (InetAddress, int) constructor because "error: call to this must be
        // first statement in constructor". We could factor out setting the member variables to an
        // init() method, but if we did, then we'd have to make the members non-final, or "error:
        // cannot assign a value to final variable address". So we just duplicate the code here.
        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(prefix);
        this.address = ipAndMask.first.getAddress();
        this.prefixLength = ipAndMask.second;
        checkAndMaskAddressAndPrefixLength();
    }

    /**
@@ -129,7 +161,7 @@ public final class IpPrefix implements Parcelable {
    }

    /**
     * Returns the prefix length of this {@code IpAddress}.
     * Returns the prefix length of this {@code IpPrefix}.
     *
     * @return the prefix length.
     */
@@ -137,6 +169,20 @@ public final class IpPrefix implements Parcelable {
        return prefixLength;
    }

    /**
     * Returns a string representation of this {@code IpPrefix}.
     *
     * @return a string such as {@code "192.0.2.0/24"} or {@code "2001:db8:1:2::"}.
     */
    public String toString() {
        try {
            return InetAddress.getByAddress(address).getHostAddress() + "/" + prefixLength;
        } catch(UnknownHostException e) {
            // Cosmic rays?
            throw new IllegalStateException("IpPrefix with invalid address! Shouldn't happen.", e);
        }
    }

    /**
     * Implement the Parcelable interface.
     */
+4 −17
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.net;

import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;

import java.net.Inet4Address;
import java.net.InetAddress;
@@ -166,23 +167,9 @@ public class LinkAddress implements Parcelable {
     * @hide
     */
    public LinkAddress(String address, int flags, int scope) {
        InetAddress inetAddress = null;
        int prefixLength = -1;
        try {
            String [] pieces = address.split("/", 2);
            prefixLength = Integer.parseInt(pieces[1]);
            inetAddress = InetAddress.parseNumericAddress(pieces[0]);
        } catch (NullPointerException e) {            // Null string.
        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
        } catch (NumberFormatException e) {           // Non-numeric prefix.
        } catch (IllegalArgumentException e) {        // Invalid IP address.
        }

        if (inetAddress == null || prefixLength == -1) {
            throw new IllegalArgumentException("Bad LinkAddress params " + address);
        }

        init(inetAddress, prefixLength, flags, scope);
        // This may throw an IllegalArgumentException; catching it is the caller's responsibility.
        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
        init(ipAndMask.first, ipAndMask.second, flags, scope);
    }

    /**
+42 −13
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import java.util.Collection;
import java.util.Locale;

import android.util.Log;
import android.util.Pair;


/**
 * Native methods for managing network interfaces.
@@ -218,24 +220,17 @@ public class NetworkUtils {
    }

    /**
     * Get InetAddress masked with prefixLength.  Will never return null.
     * @param IP address which will be masked with specified prefixLength
     * @param prefixLength the prefixLength used to mask the IP
     *  Masks a raw IP address byte array with the specified prefix length.
     */
    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
        if (address == null) {
            throw new RuntimeException("getNetworkPart doesn't accept null address");
        }

        byte[] array = address.getAddress();

    public static void maskRawAddress(byte[] array, int prefixLength) {
        if (prefixLength < 0 || prefixLength > array.length * 8) {
            throw new RuntimeException("getNetworkPart - bad prefixLength");
            throw new RuntimeException("IP address with " + array.length +
                    " bytes has invalid prefix length " + prefixLength);
        }

        int offset = prefixLength / 8;
        int reminder = prefixLength % 8;
        byte mask = (byte)(0xFF << (8 - reminder));
        int remainder = prefixLength % 8;
        byte mask = (byte)(0xFF << (8 - remainder));

        if (offset < array.length) array[offset] = (byte)(array[offset] & mask);

@@ -244,6 +239,16 @@ public class NetworkUtils {
        for (; offset < array.length; offset++) {
            array[offset] = 0;
        }
    }

    /**
     * Get InetAddress masked with prefixLength.  Will never return null.
     * @param address the IP address to mask with
     * @param prefixLength the prefixLength used to mask the IP
     */
    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
        byte[] array = address.getAddress();
        maskRawAddress(array, prefixLength);

        InetAddress netPart = null;
        try {
@@ -254,6 +259,30 @@ public class NetworkUtils {
        return netPart;
    }

    /**
     * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64".
     * @hide
     */
    public static Pair<InetAddress, Integer> parseIpAndMask(String ipAndMaskString) {
        InetAddress address = null;
        int prefixLength = -1;
        try {
            String[] pieces = ipAndMaskString.split("/", 2);
            prefixLength = Integer.parseInt(pieces[1]);
            address = InetAddress.parseNumericAddress(pieces[0]);
        } catch (NullPointerException e) {            // Null string.
        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
        } catch (NumberFormatException e) {           // Non-numeric prefix.
        } catch (IllegalArgumentException e) {        // Invalid IP address.
        }

        if (address == null || prefixLength == -1) {
            throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString);
        }

        return new Pair<InetAddress, Integer>(address, prefixLength);
    }

    /**
     * Check if IP address type is consistent between two InetAddress.
     * @return true if both are the same type.  False otherwise.
+281 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2014 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net;

import android.net.IpPrefix;
import android.os.Parcel;
import static android.test.MoreAsserts.assertNotEqual;
import android.test.suitebuilder.annotation.SmallTest;

import static org.junit.Assert.assertArrayEquals;
import java.net.InetAddress;
import java.util.Random;
import junit.framework.TestCase;


public class IpPrefixTest extends TestCase {

    // Explicitly cast everything to byte because "error: possible loss of precision".
    private static final byte[] IPV4_BYTES = { (byte) 192, (byte) 0, (byte) 2, (byte) 4};
    private static final byte[] IPV6_BYTES = {
        (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8,
        (byte) 0xde, (byte) 0xad, (byte) 0xbe, (byte) 0xef,
        (byte) 0x0f, (byte) 0x00, (byte) 0x00, (byte) 0x00,
        (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0xa0
    };

    @SmallTest
    public void testConstructor() {
        IpPrefix p;
        try {
            p = new IpPrefix((byte[]) null, 9);
            fail("Expected NullPointerException: null byte array");
        } catch(RuntimeException expected) {}

        try {
            p = new IpPrefix((InetAddress) null, 10);
            fail("Expected NullPointerException: null InetAddress");
        } catch(RuntimeException expected) {}

        try {
            p = new IpPrefix((String) null);
            fail("Expected NullPointerException: null String");
        } catch(RuntimeException expected) {}


        try {
            byte[] b2 = {1, 2, 3, 4, 5};
            p = new IpPrefix(b2, 29);
            fail("Expected IllegalArgumentException: invalid array length");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("1.2.3.4");
            fail("Expected IllegalArgumentException: no prefix length");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("1.2.3.4/");
            fail("Expected IllegalArgumentException: empty prefix length");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("foo/32");
            fail("Expected IllegalArgumentException: invalid address");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("1/32");
            fail("Expected IllegalArgumentException: deprecated IPv4 format");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("1.2.3.256/32");
            fail("Expected IllegalArgumentException: invalid IPv4 address");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("foo/32");
            fail("Expected IllegalArgumentException: non-address");
        } catch(IllegalArgumentException expected) {}

        try {
            p = new IpPrefix("f00:::/32");
            fail("Expected IllegalArgumentException: invalid IPv6 address");
        } catch(IllegalArgumentException expected) {}
    }

    public void testTruncation() {
        IpPrefix p;

        p = new IpPrefix(IPV4_BYTES, 32);
        assertEquals("192.0.2.4/32", p.toString());

        p = new IpPrefix(IPV4_BYTES, 29);
        assertEquals("192.0.2.0/29", p.toString());

        p = new IpPrefix(IPV4_BYTES, 8);
        assertEquals("192.0.0.0/8", p.toString());

        p = new IpPrefix(IPV4_BYTES, 0);
        assertEquals("0.0.0.0/0", p.toString());

        try {
            p = new IpPrefix(IPV4_BYTES, 33);
            fail("Expected IllegalArgumentException: invalid prefix length");
        } catch(RuntimeException expected) {}

        try {
            p = new IpPrefix(IPV4_BYTES, 128);
            fail("Expected IllegalArgumentException: invalid prefix length");
        } catch(RuntimeException expected) {}

        try {
            p = new IpPrefix(IPV4_BYTES, -1);
            fail("Expected IllegalArgumentException: negative prefix length");
        } catch(RuntimeException expected) {}

        p = new IpPrefix(IPV6_BYTES, 128);
        assertEquals("2001:db8:dead:beef:f00::a0/128", p.toString());

        p = new IpPrefix(IPV6_BYTES, 122);
        assertEquals("2001:db8:dead:beef:f00::80/122", p.toString());

        p = new IpPrefix(IPV6_BYTES, 64);
        assertEquals("2001:db8:dead:beef::/64", p.toString());

        p = new IpPrefix(IPV6_BYTES, 3);
        assertEquals("2000::/3", p.toString());

        p = new IpPrefix(IPV6_BYTES, 0);
        assertEquals("::/0", p.toString());

        try {
            p = new IpPrefix(IPV6_BYTES, -1);
            fail("Expected IllegalArgumentException: negative prefix length");
        } catch(RuntimeException expected) {}

        try {
            p = new IpPrefix(IPV6_BYTES, 129);
            fail("Expected IllegalArgumentException: negative prefix length");
        } catch(RuntimeException expected) {}

    }

    private void assertAreEqual(Object o1, Object o2) {
        assertTrue(o1.equals(o2));
        assertTrue(o2.equals(o1));
    }

    private void assertAreNotEqual(Object o1, Object o2) {
        assertFalse(o1.equals(o2));
        assertFalse(o2.equals(o1));
    }

    @SmallTest
    public void testEquals() {
        IpPrefix p1, p2;

        p1 = new IpPrefix("192.0.2.251/23");
        p2 = new IpPrefix(new byte[]{(byte) 192, (byte) 0, (byte) 2, (byte) 251}, 23);
        assertAreEqual(p1, p2);

        p1 = new IpPrefix("192.0.2.5/23");
        assertAreEqual(p1, p2);

        p1 = new IpPrefix("192.0.2.5/24");
        assertAreNotEqual(p1, p2);

        p1 = new IpPrefix("192.0.4.5/23");
        assertAreNotEqual(p1, p2);


        p1 = new IpPrefix("2001:db8:dead:beef:f00::80/122");
        p2 = new IpPrefix(IPV6_BYTES, 122);
        assertEquals("2001:db8:dead:beef:f00::80/122", p2.toString());
        assertAreEqual(p1, p2);

        p1 = new IpPrefix("2001:db8:dead:beef:f00::bf/122");
        assertAreEqual(p1, p2);

        p1 = new IpPrefix("2001:db8:dead:beef:f00::8:0/123");
        assertAreNotEqual(p1, p2);

        p1 = new IpPrefix("2001:db8:dead:beef::/122");
        assertAreNotEqual(p1, p2);

        // 192.0.2.4/32 != c000:0204::/32.
        byte[] ipv6bytes = new byte[16];
        System.arraycopy(IPV4_BYTES, 0, ipv6bytes, 0, IPV4_BYTES.length);
        p1 = new IpPrefix(ipv6bytes, 32);
        assertAreEqual(p1, new IpPrefix("c000:0204::/32"));

        p2 = new IpPrefix(IPV4_BYTES, 32);
        assertAreNotEqual(p1, p2);
    }

    @SmallTest
    public void testHashCode() {
        IpPrefix p;
        int oldCode = -1;
        Random random = new Random();
        for (int i = 0; i < 100; i++) {
            if (random.nextBoolean()) {
                // IPv4.
                byte[] b = new byte[4];
                random.nextBytes(b);
                p = new IpPrefix(b, random.nextInt(33));
                assertNotEqual(oldCode, p.hashCode());
                oldCode = p.hashCode();
            } else {
                // IPv6.
                byte[] b = new byte[16];
                random.nextBytes(b);
                p = new IpPrefix(b, random.nextInt(129));
                assertNotEqual(oldCode, p.hashCode());
                oldCode = p.hashCode();
            }
        }
    }

    @SmallTest
    public void testMappedAddressesAreBroken() {
        // 192.0.2.0/24 != ::ffff:c000:0204/120, but because we use InetAddress,
        // we are unable to comprehend that.
        byte[] ipv6bytes = {
            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
            (byte) 0, (byte) 0, (byte) 0, (byte) 0,
            (byte) 0, (byte) 0, (byte) 0xff, (byte) 0xff,
            (byte) 192, (byte) 0, (byte) 2, (byte) 0};
        IpPrefix p = new IpPrefix(ipv6bytes, 120);
        assertEquals(16, p.getRawAddress().length);       // Fine.
        assertArrayEquals(ipv6bytes, p.getRawAddress());  // Fine.

        // Broken.
        assertEquals("192.0.2.0/120", p.toString());
        assertEquals(InetAddress.parseNumericAddress("192.0.2.0"), p.getAddress());
    }

    public IpPrefix passThroughParcel(IpPrefix p) {
        Parcel parcel = Parcel.obtain();
        IpPrefix p2 = null;
        try {
            p.writeToParcel(parcel, 0);
            parcel.setDataPosition(0);
            p2 = IpPrefix.CREATOR.createFromParcel(parcel);
        } finally {
            parcel.recycle();
        }
        assertNotNull(p2);
        return p2;
    }

    public void assertParcelingIsLossless(IpPrefix p) {
      IpPrefix p2 = passThroughParcel(p);
      assertEquals(p, p2);
    }

    public void testParceling() {
        IpPrefix p;

        p = new IpPrefix("2001:4860:db8::/64");
        assertParcelingIsLossless(p);

        p = new IpPrefix("192.0.2.0/25");
        assertParcelingIsLossless(p);
    }
}
+7 −8
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ package android.net;
import java.lang.reflect.Method;
import java.net.InetAddress;

import android.net.LinkAddress;
import android.net.IpPrefix;
import android.net.RouteInfo;
import android.os.Parcel;

@@ -32,9 +32,8 @@ public class RouteInfoTest extends TestCase {
        return InetAddress.parseNumericAddress(addr);
    }

    private LinkAddress Prefix(String prefix) {
        String[] parts = prefix.split("/");
        return new LinkAddress(Address(parts[0]), Integer.parseInt(parts[1]));
    private IpPrefix Prefix(String prefix) {
        return new IpPrefix(prefix);
    }

    @SmallTest
@@ -43,17 +42,17 @@ public class RouteInfoTest extends TestCase {

        // Invalid input.
        try {
            r = new RouteInfo((LinkAddress) null, null, "rmnet0");
            r = new RouteInfo((IpPrefix) null, null, "rmnet0");
            fail("Expected RuntimeException:  destination and gateway null");
        } catch(RuntimeException e) {}

        // Null destination is default route.
        r = new RouteInfo((LinkAddress) null, Address("2001:db8::1"), null);
        r = new RouteInfo((IpPrefix) null, Address("2001:db8::1"), null);
        assertEquals(Prefix("::/0"), r.getDestination());
        assertEquals(Address("2001:db8::1"), r.getGateway());
        assertNull(r.getInterface());

        r = new RouteInfo((LinkAddress) null, Address("192.0.2.1"), "wlan0");
        r = new RouteInfo((IpPrefix) null, Address("192.0.2.1"), "wlan0");
        assertEquals(Prefix("0.0.0.0/0"), r.getDestination());
        assertEquals(Address("192.0.2.1"), r.getGateway());
        assertEquals("wlan0", r.getInterface());
@@ -74,7 +73,7 @@ public class RouteInfoTest extends TestCase {
        class PatchedRouteInfo {
            private final RouteInfo mRouteInfo;

            public PatchedRouteInfo(LinkAddress destination, InetAddress gateway, String iface) {
            public PatchedRouteInfo(IpPrefix destination, InetAddress gateway, String iface) {
                mRouteInfo = new RouteInfo(destination, gateway, iface);
            }