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

Commit 9115abd5 authored by Lorenzo Colitti's avatar Lorenzo Colitti Committed by Gerrit Code Review
Browse files

Merge "Support parsing ND option messages."

parents e29909fe bb1878a3
Loading
Loading
Loading
Loading
+78 −0
Original line number 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 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 Diff line number Diff line
@@ -64,6 +64,8 @@ public class NetlinkMessage {
                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
            case NetlinkConstants.RTM_NEWNDUSEROPT:
                return (NetlinkMessage) NduseroptMessage.parse(nlmsghdr, byteBuffer);
            default:
                if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
                    // Netlink control message.  Just parse the header for now,
+3 −8
Original line number 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 TYPE = 38;

    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.
     * Valid values are from 0 to 65528 in multiples of 8.
@@ -72,9 +68,8 @@ public class StructNdOptPref64 {
        }
    }

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

+188 −0
Original line number 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);
    }
}