diff options
author | Lorenzo Colitti <lorenzo@google.com> | 2020-04-16 05:07:58 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-04-16 05:07:58 +0000 |
commit | 7af2235ef872406687e258138323c25e7f1b0e00 (patch) | |
tree | ab30b77db911b0c27acb0a582dacf7fadb15cc30 | |
parent | cb613dcc863bfeb1d87a5f7b35c21ed49e390b34 (diff) | |
parent | 1b95e2d3eadf417196527eff77c4d20111399dbf (diff) |
Merge "Support parsing ND option messages." into rvc-dev am: 77e026021f am: 1b95e2d3ea
Change-Id: I9c46f4fd523e2d3ae0a0b23b5dd108dc7fd8cb7c
5 files changed, 408 insertions, 8 deletions
diff --git a/common/netlinkclient/src/android/net/netlink/NdOption.java b/common/netlinkclient/src/android/net/netlink/NdOption.java new file mode 100644 index 0000000..db262b9 --- /dev/null +++ b/common/netlinkclient/src/android/net/netlink/NdOption.java @@ -0,0 +1,78 @@ +/* + * 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); +} diff --git a/common/netlinkclient/src/android/net/netlink/NduseroptMessage.java b/common/netlinkclient/src/android/net/netlink/NduseroptMessage.java new file mode 100644 index 0000000..4940f6e --- /dev/null +++ b/common/netlinkclient/src/android/net/netlink/NduseroptMessage.java @@ -0,0 +1,137 @@ +/* + * 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()); + } +} diff --git a/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java b/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java index b730032..dafa66b 100644 --- a/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java +++ b/common/netlinkclient/src/android/net/netlink/NetlinkMessage.java @@ -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, diff --git a/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java b/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java index 6a68df8..5cce3da 100644 --- a/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java +++ b/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java @@ -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); diff --git a/tests/unit/src/android/net/netlink/NduseroptMessageTest.java b/tests/unit/src/android/net/netlink/NduseroptMessageTest.java new file mode 100644 index 0000000..0c27b97 --- /dev/null +++ b/tests/unit/src/android/net/netlink/NduseroptMessageTest.java @@ -0,0 +1,188 @@ +/* + * 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); + } +} |