Support decoding the new PREF64 RA option.
Bug: 153694684
Test: new unit tests
Change-Id: I94346939cda910b01ffee75cf8b62a23ec5314cc
diff --git a/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java b/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java
new file mode 100644
index 0000000..6a68df8
--- /dev/null
+++ b/common/netlinkclient/src/android/net/netlink/StructNdOptPref64.java
@@ -0,0 +1,121 @@
+/*
+ * 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 android.net.IpPrefix;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+
+/**
+ * The PREF64 router advertisement option. RFC 8781.
+ *
+ * 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Type | Length | Scaled Lifetime | PLC |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | |
+ * + +
+ * | Highest 96 bits of the Prefix |
+ * + +
+ * | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ */
+public class StructNdOptPref64 {
+ 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.
+ */
+ public final int lifetime;
+ /** The NAT64 prefix. */
+ public final IpPrefix prefix;
+
+ int plcToPrefixLength(int plc) {
+ switch (plc) {
+ case 0: return 96;
+ case 1: return 64;
+ case 2: return 56;
+ case 3: return 48;
+ case 4: return 40;
+ case 5: return 32;
+ default:
+ throw new IllegalArgumentException("Invalid prefix length code " + plc);
+ }
+ }
+
+ StructNdOptPref64(@NonNull ByteBuffer buf) {
+ type = buf.get();
+ length = buf.get();
+ if (type != TYPE) throw new IllegalArgumentException("Invalid type " + type);
+ if (length != 2) throw new IllegalArgumentException("Invalid length " + length);
+
+ int scaledLifetimePlc = Short.toUnsignedInt(buf.getShort());
+ lifetime = scaledLifetimePlc & 0xfff8;
+
+ byte[] addressBytes = new byte[16];
+ buf.get(addressBytes, 0, 12);
+ InetAddress addr;
+ try {
+ addr = InetAddress.getByAddress(addressBytes);
+ } catch (UnknownHostException e) {
+ throw new AssertionError("16-byte array not valid InetAddress?");
+ }
+ prefix = new IpPrefix(addr, plcToPrefixLength(scaledLifetimePlc & 7));
+ }
+
+ /**
+ * Parses an option from a {@link ByteBuffer}.
+ *
+ * @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 StructNdOptPref64 parse(@NonNull ByteBuffer buf) {
+ if (buf == null || buf.remaining() < STRUCT_SIZE) return null;
+ try {
+ return new StructNdOptPref64(buf);
+ } catch (IllegalArgumentException 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.
+ Log.d(TAG, "Invalid PREF64 option: " + e);
+ return null;
+ }
+ }
+
+ @Override
+ @NonNull
+ public String toString() {
+ return String.format("NdOptPref64(%s, %d)", prefix, lifetime);
+ }
+}
diff --git a/src/com/android/server/util/NetworkStackConstants.java b/src/com/android/server/util/NetworkStackConstants.java
index 660f0a6..dbba7f3 100644
--- a/src/com/android/server/util/NetworkStackConstants.java
+++ b/src/com/android/server/util/NetworkStackConstants.java
@@ -126,6 +126,7 @@
public static final int ICMPV6_ND_OPTION_PIO = 3;
public static final int ICMPV6_ND_OPTION_MTU = 5;
public static final int ICMPV6_ND_OPTION_RDNSS = 25;
+ public static final int ICMPV6_ND_OPTION_PREF64 = 38;
public static final int ICMPV6_RA_HEADER_LEN = 16;
diff --git a/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java b/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java
new file mode 100644
index 0000000..3d36d9b
--- /dev/null
+++ b/tests/unit/src/android/net/netlink/StructNdOptPref64Test.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2019 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.assertNull;
+
+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.net.InetAddress;
+import java.nio.ByteBuffer;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class StructNdOptPref64Test {
+
+ private static final String PREFIX1 = "64:ff9b::";
+ private static final String PREFIX2 = "2001:db8:1:2:3:64::";
+
+ private static byte[] prefixBytes(String addrString) throws Exception {
+ InetAddress addr = InetAddress.getByName(addrString);
+ byte[] prefixBytes = new byte[12];
+ System.arraycopy(addr.getAddress(), 0, prefixBytes, 0, 12);
+ return prefixBytes;
+ }
+
+ private static IpPrefix prefix(String addrString, int prefixLength) throws Exception {
+ return new IpPrefix(InetAddress.getByName(addrString), prefixLength);
+ }
+
+ private void assertPref64OptMatches(int lifetime, IpPrefix prefix, StructNdOptPref64 opt) {
+ assertEquals(StructNdOptPref64.TYPE, opt.type);
+ assertEquals(2, opt.length);
+ assertEquals(lifetime, opt.lifetime);
+ assertEquals(prefix, opt.prefix);
+ }
+
+ /**
+ * Returns the 2-byte "scaled lifetime and prefix length code" field: 13-bit lifetime, 3-bit PLC
+ */
+ private short getPref64ScaledLifetimePlc(int lifetime, int prefixLengthCode) {
+ return (short) ((lifetime & 0xfff8) | (prefixLengthCode & 0x7));
+ }
+
+ private ByteBuffer makeNdOptPref64(int lifetime, byte[] prefix, int prefixLengthCode) {
+ if (prefix.length != 12) throw new IllegalArgumentException("Prefix must be 12 bytes");
+
+ ByteBuffer buf = ByteBuffer.allocate(16)
+ .put((byte) StructNdOptPref64.TYPE)
+ .put((byte) 2) // len=2 (16 bytes)
+ .putShort(getPref64ScaledLifetimePlc(lifetime, prefixLengthCode))
+ .put(prefix, 0, 12);
+
+ buf.flip();
+ return buf;
+ }
+
+ @Test
+ public void testParseCannedOption() throws Exception {
+ String hexBytes = "2602" // type=38, len=2 (16 bytes)
+ + "0088" // lifetime=136, PLC=0 (/96)
+ + "20010db80003000400050006"; // 2001:db8:3:4:5:6/96
+ byte[] rawBytes = HexEncoding.decode(hexBytes);
+ StructNdOptPref64 opt = StructNdOptPref64.parse(ByteBuffer.wrap(rawBytes));
+ assertPref64OptMatches(136, prefix("2001:db8:3:4:5:6::", 96), opt);
+
+ hexBytes = "2602" // type=38, len=2 (16 bytes)
+ + "2752" // lifetime=10064, PLC=2 (/56)
+ + "0064ff9b0000000000000000"; // 64:ff9b::/56
+ rawBytes = HexEncoding.decode(hexBytes);
+ opt = StructNdOptPref64.parse(ByteBuffer.wrap(rawBytes));
+ assertPref64OptMatches(10064, prefix("64:ff9b::", 56), opt);
+ }
+
+ @Test
+ public void testParsing() throws Exception {
+ // Valid.
+ ByteBuffer buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 0);
+ StructNdOptPref64 opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(600, prefix(PREFIX1, 96), opt);
+
+ // Valid, zero lifetime, /64.
+ buf = makeNdOptPref64(0, prefixBytes(PREFIX1), 1);
+ opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(0, prefix(PREFIX1, 64), opt);
+
+ // Valid, low lifetime, /56.
+ buf = makeNdOptPref64(8, prefixBytes(PREFIX2), 2);
+ opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(8, prefix(PREFIX2, 56), opt);
+ assertEquals(new IpPrefix("2001:db8:1::/56"), opt.prefix); // Prefix is truncated.
+
+ // Valid, maximum lifetime, /32.
+ buf = makeNdOptPref64(65528, prefixBytes(PREFIX2), 5);
+ opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(65528, prefix(PREFIX2, 32), opt);
+ assertEquals(new IpPrefix("2001:db8::/32"), opt.prefix); // Prefix is truncated.
+
+ // Lifetime not divisible by 8.
+ buf = makeNdOptPref64(300, prefixBytes(PREFIX2), 0);
+ opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(296, prefix(PREFIX2, 96), opt);
+
+ // Invalid prefix length codes.
+ buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 6);
+ assertNull(StructNdOptPref64.parse(buf));
+ buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 7);
+ assertNull(StructNdOptPref64.parse(buf));
+
+ // Truncated to varying lengths...
+ buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 3);
+ final int len = buf.limit();
+ for (int i = 0; i < buf.limit() - 1; i++) {
+ buf.flip();
+ buf.limit(i);
+ assertNull("Option truncated to " + i + " bytes, should have returned null",
+ StructNdOptPref64.parse(buf));
+ }
+ buf.flip();
+ buf.limit(len);
+ // ... but otherwise OK.
+ opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(600, prefix(PREFIX1, 48), opt);
+ }
+
+ @Test
+ public void testToString() throws Exception {
+ ByteBuffer buf = makeNdOptPref64(600, prefixBytes(PREFIX1), 4);
+ StructNdOptPref64 opt = StructNdOptPref64.parse(buf);
+ assertPref64OptMatches(600, prefix(PREFIX1, 40), opt);
+ assertEquals("NdOptPref64(64:ff9b::/40, 600)", opt.toString());
+ }
+}