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

Commit bb1878a3 authored by Lorenzo Colitti's avatar Lorenzo Colitti
Browse files

Support parsing ND option messages.

Define a new StructNduseroptmsg class that is a rough equivalent
of the "struct nduseroptmsg" used to pass RA options from the
kernel to userspace. Also define a new NdOption class and make
the existing pref64 option subclass it.

Bug: 153694684
Test: new unit tests
Change-Id: I3b71e63ee2cdaa40d095e889188943c5b0cd13af
parent 27420ec1
Loading
Loading
Loading
Loading
+78 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2020 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.netlink;

import java.nio.ByteBuffer;

/**
 * Base class for IPv6 neighbour discovery options.
 */
public class NdOption {
    public static final int STRUCT_SIZE = 2;

    /** The option type. */
    public final byte type;
    /** The length of the option in 8-byte units. Actually an unsigned 8-bit integer */
    public final int length;

    /** Constructs a new NdOption. */
    public NdOption(byte type, int length) {
        this.type = type;
        this.length = length;
    }

    /**
     * Parses a neighbour discovery option.
     *
     * Parses (and consumes) the option if it is of a known type. If the option is of an unknown
     * type, advances the buffer (so the caller can continue parsing if desired) and returns
     * {@link #UNKNOWN}. If the option claims a length of 0, returns null because parsing cannot
     * continue.
     *
     * No checks are performed on the length other than ensuring it is not 0, so if a caller wants
     * to deal with options that might overflow the structure that contains them, it must explicitly
     * set the buffer's limit to the position at which that structure ends.
     *
     * @param buf the buffer to parse.
     * @return a subclass of {@link NdOption}, or {@code null} for an unknown or malformed option.
     */
    public static NdOption parse(ByteBuffer buf) {
        if (buf == null || buf.remaining() < STRUCT_SIZE) return null;

        // Peek the type without advancing the buffer.
        byte type = buf.get(buf.position());
        int length = Byte.toUnsignedInt(buf.get(buf.position() + 1));
        if (length == 0) return null;

        switch (type) {
            case StructNdOptPref64.TYPE:
                return StructNdOptPref64.parse(buf);

            default:
                int newPosition = Math.min(buf.limit(), buf.position() + length * 8);
                buf.position(newPosition);
                return UNKNOWN;
        }
    }

    @Override
    public String toString() {
        return String.format("NdOption(%d, %d)", Byte.toUnsignedInt(type), length);
    }

    public static final NdOption UNKNOWN = new NdOption((byte) 0, 0);
}
+137 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2020 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.netlink;

import static android.system.OsConstants.AF_INET6;

import androidx.annotation.NonNull;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * A NetlinkMessage subclass for RTM_NEWNDUSEROPT messages.
 */
public class NduseroptMessage extends NetlinkMessage {
    public static final int STRUCT_SIZE = 16;

    static final int NDUSEROPT_SRCADDR = 1;

    /** The address family. Presumably always AF_INET6. */
    public final byte family;
    /**
     * The total length in bytes of the options that follow this structure.
     * Actually a 16-bit unsigned integer.
     */
    public final int opts_len;
    /** The interface index on which the options were received. */
    public final int ifindex;
    /** The ICMP type of the packet that contained the options. */
    public final byte icmp_type;
    /** The ICMP code of the packet that contained the options. */
    public final byte icmp_code;

    /**
     * ND option that was in this message.
     * Even though the length field is called "opts_len", the kernel only ever sends one option per
     * message. It is unlikely that this will ever change as it would break existing userspace code.
     * But if it does, we can simply update this code, since userspace is typically newer than the
     * kernel.
     */
    public final NdOption option;

    /** The IP address that sent the packet containing the option. */
    public final InetAddress srcaddr;

    NduseroptMessage(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf)
            throws UnknownHostException {
        super(header);

        // The structure itself.
        buf.order(ByteOrder.nativeOrder());
        family = buf.get();
        buf.get();  // Skip 1 byte of padding.
        opts_len = Short.toUnsignedInt(buf.getShort());
        ifindex = buf.getInt();
        icmp_type = buf.get();
        icmp_code = buf.get();
        buf.order(ByteOrder.BIG_ENDIAN);
        buf.position(buf.position() + 6);  // Skip 6 bytes of padding.

        // The ND option.
        // Ensure we don't read past opts_len even if the option length is invalid.
        // Note that this check is not really necessary since if the option length is not valid,
        // this struct won't be very useful to the caller.
        int oldLimit = buf.limit();
        buf.limit(STRUCT_SIZE + opts_len);
        try {
            option = NdOption.parse(buf);
        } finally {
            buf.limit(oldLimit);
        }

        // The source address.
        int newPosition = STRUCT_SIZE + opts_len;
        if (newPosition >= buf.limit()) {
            throw new IllegalArgumentException("ND options extend past end of buffer");
        }
        buf.position(newPosition);

        StructNlAttr nla = StructNlAttr.parse(buf);
        if (nla == null || nla.nla_type != NDUSEROPT_SRCADDR || nla.nla_value == null) {
            throw new IllegalArgumentException("Invalid source address in ND useropt");
        }
        if (family == AF_INET6) {
            // InetAddress.getByAddress only looks at the ifindex if the address type needs one.
            srcaddr = Inet6Address.getByAddress(null /* hostname */, nla.nla_value, ifindex);
        } else {
            srcaddr = InetAddress.getByAddress(nla.nla_value);
        }
    }

    /**
     * Parses a StructNduseroptmsg from a {@link ByteBuffer}.
     *
     * @param header the netlink message header.
     * @param buf The buffer from which to parse the option. The buffer's byte order must be
     *            {@link java.nio.ByteOrder#BIG_ENDIAN}.
     * @return the parsed option, or {@code null} if the option could not be parsed successfully
     *         (for example, if it was truncated, or if the prefix length code was wrong).
     */
    public static NduseroptMessage parse(@NonNull StructNlMsgHdr header, @NonNull ByteBuffer buf) {
        if (buf == null || buf.remaining() < STRUCT_SIZE) return null;
        try {
            return new NduseroptMessage(header, buf);
        } catch (IllegalArgumentException | UnknownHostException | BufferUnderflowException e) {
            // Not great, but better than throwing an exception that might crash the caller.
            // Convention in this package is that null indicates that the option was truncated, so
            // callers must already handle it.
            return null;
        }
    }

    @Override
    public String toString() {
        return String.format("Nduseroptmsg(%d, %d, %d, %d, %d, %s)",
                family, opts_len, ifindex, Byte.toUnsignedInt(icmp_type),
                Byte.toUnsignedInt(icmp_code), srcaddr.getHostAddress());
    }
}
+2 −0
Original line number Original line Diff line number Diff line
@@ -64,6 +64,8 @@ public class NetlinkMessage {
                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
            case NetlinkConstants.RTM_NEWNDUSEROPT:
                return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
            default:
            default:
                if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
                if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
                    // Netlink control message.  Just parse the header for now,
                    // Netlink control message.  Just parse the header for now,
+3 −8
Original line number Original line Diff line number Diff line
@@ -41,16 +41,12 @@ import java.nio.ByteBuffer;
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 *
 *
 */
 */
public class StructNdOptPref64 {
public class StructNdOptPref64 extends NdOption {
    public static final int STRUCT_SIZE = 16;
    public static final int STRUCT_SIZE = 16;
    public static final int TYPE = 38;
    public static final int TYPE = 38;


    private static final String TAG = StructNdOptPref64.class.getSimpleName();
    private static final String TAG = StructNdOptPref64.class.getSimpleName();


    /** The option type. Always ICMPV6_ND_OPTION_PREF64. */
    public final byte type;
    /** The length of the option in 8-byte units. Actually an unsigned 8-bit integer. */
    public final int length;
    /**
    /**
     * How many seconds the prefix is expected to remain valid.
     * How many seconds the prefix is expected to remain valid.
     * Valid values are from 0 to 65528 in multiples of 8.
     * Valid values are from 0 to 65528 in multiples of 8.
@@ -72,9 +68,8 @@ public class StructNdOptPref64 {
        }
        }
    }
    }


    StructNdOptPref64(@NonNull ByteBuffer buf) {
    public StructNdOptPref64(@NonNull ByteBuffer buf) {
        type = buf.get();
        super(buf.get(), Byte.toUnsignedInt(buf.get()));
        length = buf.get();
        if (type != TYPE) throw new IllegalArgumentException("Invalid type " + type);
        if (type != TYPE) throw new IllegalArgumentException("Invalid type " + type);
        if (length != 2) throw new IllegalArgumentException("Invalid length " + length);
        if (length != 2) throw new IllegalArgumentException("Invalid length " + length);


+188 −0
Original line number Original line Diff line number Diff line
/*
 * Copyright (C) 2020 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.netlink;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import android.net.IpPrefix;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import libcore.util.HexEncoding;

import org.junit.Test;
import org.junit.runner.RunWith;

import java.nio.ByteBuffer;

@RunWith(AndroidJUnit4.class)
@SmallTest
public class NduseroptMessageTest {

    // Pick ifindices that are high enough that they will "never" be an existing interface index,
    // and always be represented numerically in the address. That way, the test will never need to
    // determine the interface names corresponding to these indices. That simplifies the code and
    // makes the test more useful because determining interface names might require permissions.
    private static final int IFINDEX1 = 15715755;
    private static final int IFINDEX2 = 1431655765;

    // IPv6, 0 bytes of options, interface index 15715755, type 134 (RA), code 0, padding.
    private static final String HDR_EMPTY = "0a00" + "0000" + "abcdef00" + "8600000000000000";

    // IPv6, 16 bytes of options, interface index 1431655765, type 134 (RA), code 0, padding.
    private static final String HDR_16BYTE = "0a00" + "1000" + "55555555" + "8600000000000000";

    // IPv6, 32 bytes of options, interface index 1431655765, type 134 (RA), code 0, padding.
    private static final String HDR_32BYTE = "0a00" + "2000" + "55555555" + "8600000000000000";

    // PREF64 option, 2001:db8:3:4:5:6::/96, lifetime=10064
    private static final String OPT_PREF64 = "2602" + "2750" + "20010db80003000400050006";

    // Length 20, NDUSEROPT_SRCADDR, fe80:2:3:4:5:6:7:8
    private static final String NLA_SRCADDR = "1400" + "0100" + "fe800002000300040005000600070008";

    private static final String SRCADDR1 = "fe80:2:3:4:5:6:7:8%" + IFINDEX1;
    private static final String SRCADDR2 = "fe80:2:3:4:5:6:7:8%" + IFINDEX2;

    private static final String MSG_EMPTY = HDR_EMPTY + NLA_SRCADDR;
    private static final String MSG_PREF64 = HDR_16BYTE + OPT_PREF64 + NLA_SRCADDR;

    @Test
    public void testParsing() {
        NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_EMPTY));
        assertMatches((byte) 10, 0, IFINDEX1, (byte) 134, (byte) 0, SRCADDR1, msg);
        assertNull(msg.option);

        msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
        assertMatches((byte) 10, 16, IFINDEX2, (byte) 134, (byte) 0, SRCADDR2, msg);
        assertPref64Option("2001:db8:3:4:5:6::/96", msg.option);
    }

    @Test
    public void testUnknownOption() {
        ByteBuffer buf = toBuffer(MSG_PREF64);
        // Replace the PREF64 option type (38) with an unknown option number.
        final int optionStart = NduseroptMessage.STRUCT_SIZE;
        assertEquals(38, buf.get(optionStart));
        buf.put(optionStart, (byte) 42);

        NduseroptMessage msg = parseNduseroptMessage(buf);
        assertMatches((byte) 10, 16, IFINDEX2, (byte) 134, (byte) 0, SRCADDR2, msg);
        assertEquals(NdOption.UNKNOWN, msg.option);

        buf.flip();
        assertEquals(42, buf.get(optionStart));
        buf.put(optionStart, (byte) 38);

        msg = parseNduseroptMessage(buf);
        assertMatches((byte) 10, 16, IFINDEX2, (byte) 134, (byte) 0, SRCADDR2, msg);
        assertPref64Option("2001:db8:3:4:5:6::/96", msg.option);
    }

    @Test
    public void testZeroLengthOption() {
        // Make sure an unknown option with a 0-byte length is ignored and parsing continues with
        // the address, which comes after it.
        final String hexString = HDR_16BYTE + "00000000000000000000000000000000" + NLA_SRCADDR;
        ByteBuffer buf = toBuffer(hexString);
        assertEquals(52, buf.limit());
        NduseroptMessage msg = parseNduseroptMessage(buf);
        assertMatches((byte) 10, 16, IFINDEX2, (byte) 134, (byte) 0, SRCADDR2, msg);
        assertNull(msg.option);
    }

    @Test
    public void testTooLongOption() {
        // Make sure that if an option's length is too long, it's ignored and parsing continues with
        // the address, which comes after it.
        final String hexString = HDR_16BYTE + "26030000000000000000000000000000" + NLA_SRCADDR;
        ByteBuffer buf = toBuffer(hexString);
        assertEquals(52, buf.limit());
        NduseroptMessage msg = parseNduseroptMessage(buf);
        assertMatches((byte) 10, 16, IFINDEX2, (byte) 134, (byte) 0, SRCADDR2, msg);
        assertNull(msg.option);
    }

    @Test
    public void testOptionsTooLong() {
        // Header claims 32 bytes of options. Buffer ends before options end.
        String hexString = HDR_32BYTE + OPT_PREF64;
        ByteBuffer buf = toBuffer(hexString);
        assertEquals(32, buf.limit());
        assertNull(NduseroptMessage.parse(toBuffer(hexString)));

        // Header claims 32 bytes of options. Buffer ends at end of options with no source address.
        hexString = HDR_32BYTE + OPT_PREF64 + OPT_PREF64;
        buf = toBuffer(hexString);
        assertEquals(48, buf.limit());
        assertNull(NduseroptMessage.parse(toBuffer(hexString)));
    }

    @Test
    public void testTruncation() {
        final int optLen = MSG_PREF64.length() / 2;  // 1 byte = 2 hex chars
        for (int len = 0; len < optLen; len++) {
            ByteBuffer buf = toBuffer(MSG_PREF64.substring(0, len * 2));
            NduseroptMessage msg = parseNduseroptMessage(buf);
            if (len < optLen) {
                assertNull(msg);
            } else {
                assertNotNull(msg);
                assertPref64Option("2001:db8:3:4:5:6::/96", msg.option);
            }
        }
    }

    @Test
    public void testToString() {
        NduseroptMessage msg = parseNduseroptMessage(toBuffer(MSG_PREF64));
        assertNotNull(msg);
        assertEquals("Nduseroptmsg(10, 16, 1431655765, 134, 0, fe80:2:3:4:5:6:7:8%1431655765)",
                msg.toString());
    }

    // Convenience method to parse a NduseroptMessage that's not part of a netlink message.
    private NduseroptMessage parseNduseroptMessage(ByteBuffer buf) {
        return NduseroptMessage.parse(null, buf);
    }

    private ByteBuffer toBuffer(String hexString) {
        return ByteBuffer.wrap(HexEncoding.decode(hexString));
    }

    private void assertMatches(byte family, int optsLen, int ifindex, byte icmpType,
            byte icmpCode, String srcaddr, NduseroptMessage msg) {
        assertNotNull(msg);
        assertEquals(family, msg.family);
        assertEquals(ifindex, msg.ifindex);
        assertEquals(optsLen, msg.opts_len);
        assertEquals(icmpType, msg.icmp_type);
        assertEquals(icmpCode, msg.icmp_code);
        assertEquals(srcaddr, msg.srcaddr.getHostAddress());
    }

    private void assertPref64Option(String prefix, NdOption opt) {
        assertNotNull(opt);
        assertTrue(opt instanceof StructNdOptPref64);
        StructNdOptPref64 pref64Opt = (StructNdOptPref64) opt;
        assertEquals(new IpPrefix(prefix), pref64Opt.prefix);
    }
}