Snap for 9992948 from d31736e3a0a53cd2995167086cc3d0f7d62bfc76 to mainline-media-swcodec-release Change-Id: Ic9ef0827ec01db46cf41c5c12d2c048831a2b72a
diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java index e30ebc7..6b0bdc4 100644 --- a/src/android/net/apf/DnsUtils.java +++ b/src/android/net/apf/DnsUtils.java
@@ -20,7 +20,6 @@ import static android.net.apf.ApfGenerator.Register.R1; import static com.android.net.module.util.NetworkStackConstants.ETHER_HEADER_LEN; -import static com.android.net.module.util.NetworkStackConstants.IPV6_HEADER_LEN; import static com.android.net.module.util.NetworkStackConstants.UDP_HEADER_LEN; import androidx.annotation.NonNull; @@ -264,20 +263,26 @@ gen.addJump(jumpTable.getStartLabel()); } - /** - * Returns the name of a jump label used while parsing the specified DNS label. - * TODO: use another scheme to name the labels. Using the label name does not work if the name - * to be matched contains duplicate labels. - */ - private static String getPostMatchJumpTargetForLabel(String label) { - return label + "_parsed"; + /** @return jump label that points to the start of a DNS label's parsing code. */ + private static String getStartMatchLabel(int labelIndex) { + return "dns_parse_" + labelIndex; + } + + /** @return jump label used while parsing the specified DNS label. */ + private static String getPostMatchJumpTargetForLabel(int labelIndex) { + return "dns_parsed_" + labelIndex; + } + + /** @return jump label used when the match for the specified DNS label fails. */ + private static String getNoMatchLabel(int labelIndex) { + return "dns_nomatch_" + labelIndex; } private static void addMatchLabel(@NonNull ApfGenerator gen, @NonNull JumpTable jumpTable, - @NonNull String label, @NonNull String nextLabel) throws Exception { - final String parsedLabel = getPostMatchJumpTargetForLabel(label); - final String noMatchLabel = label + "_nomatch"; - gen.defineLabel(label); + int labelIndex, @NonNull String label, @NonNull String nextLabel) throws Exception { + final String parsedLabel = getPostMatchJumpTargetForLabel(labelIndex); + final String noMatchLabel = getNoMatchLabel(labelIndex); + gen.defineLabel(getStartMatchLabel(labelIndex)); // Store return address. gen.addLoadImmediate(R0, jumpTable.getIndex(parsedLabel)); @@ -319,45 +324,46 @@ * packet, e.g., as used by MDNS. However, it currently only supports one DNS name. * * Limitations: - * - Filter size is just under 300 bytes for a typical question. - * - Because the bytecode extensively uses backwards jumps, it can hit the APF interpreter + * <ul> + * <li>Filter size is just under 300 bytes for a typical question. + * <li>Because the bytecode extensively uses backwards jumps, it can hit the APF interpreter * instruction limit. This limit causes the APF interpreter to accept the packet once it has * executed a number of instructions equal to the program length in bytes. * A program that consists *only* of this filter will be able to execute just under 300 * instructions, and will be able to correctly drop packets with two questions but not three * questions. In a real APF setup, there will be other code (e.g., RA filtering) which counts * against the limit, so the filter should be able to parse packets with more questions. - * - Matches are case-sensitive. This is due to the use of JNEBS to match DNS labels and is + * <li>Matches are case-sensitive. This is due to the use of JNEBS to match DNS labels and is * likely impossible to overcome without interpreter changes. + * </ul> * * TODO: - * - Add unit tests for the parse_dns_label and find_next_dns_question functions. - * - Add an efficient way to parse the first question in the packet. This can be done much more - * efficiently because the first name cannot be compressed. - * - Support accepting more than one name. - * - For devices where power saving is a priority (e.g., flat panel TVs), add support for + * <ul> + * <li>Add unit tests for the parse_dns_label and find_next_dns_question functions. + * <li>Support accepting more than one name. + * <li>For devices where power saving is a priority (e.g., flat panel TVs), add support for * dropping packets with more than X queries, to ensure the filter will drop the packet rather * than hit the instruction limit. + * </ul> */ - public static void generateFilter(ApfGenerator gen, boolean ipv6, String[] labels) - throws Exception { + public static void generateFilter(ApfGenerator gen, String[] labels) throws Exception { final int etherPlusUdpLen = ETHER_HEADER_LEN + UDP_HEADER_LEN; final String labelJumpTable = "jump_table"; // Initialize parsing /** + * - R1: length of IP header. * - m[SLOT_DNS_HEADER_OFFSET]: offset of DNS header * - m[SLOT_CURRENT_PARSE_OFFSET]: current parsing offset (start of question section) * - m[SLOT_AFTER_POINTER_OFFSET]: offset after first pointer in name, must be 0 when * starting a new name * - m[SLOT_NEGATIVE_QDCOUNT_REMAINING]: negative qdcount */ - if (ipv6) { - gen.addLoadImmediate(R0, IPV6_HEADER_LEN); - } else { - gen.addLoadFromMemory(R0, ApfGenerator.IPV4_HEADER_SIZE_MEMORY_SLOT); - } + // Move IP header length to R0 and use it to find the DNS header offset. + // TODO: this uses R1 for consistency with ApfFilter#generateMdnsFilterLocked. Evaluate + // using R0 instead. + gen.addMove(R0); gen.addAdd(etherPlusUdpLen); gen.addStoreToMemory(R0, SLOT_DNS_HEADER_OFFSET); @@ -386,7 +392,7 @@ // calls below) because otherwise all the jumps are backwards, and backwards jumps are more // expensive (5 bytes of bytecode) for (int i = 0; i < labels.length; i++) { - table.addLabel(getPostMatchJumpTargetForLabel(labels[i])); + table.addLabel(getPostMatchJumpTargetForLabel(i)); } table.addLabel(LABEL_START_MATCH); table.generate(gen); @@ -396,8 +402,8 @@ for (int i = 0; i < labels.length; i++) { final String nextLabel = (i == labels.length - 1) ? ApfGenerator.PASS_LABEL - : labels[i + 1]; - addMatchLabel(gen, table, labels[i], nextLabel); + : getStartMatchLabel(i + 1); + addMatchLabel(gen, table, i, labels[i], nextLabel); } gen.addJump(ApfGenerator.DROP_LABEL); }
diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java new file mode 100644 index 0000000..3fa7cb0 --- /dev/null +++ b/src/android/net/dhcp6/Dhcp6Client.java
@@ -0,0 +1,132 @@ +/* + * Copyright (C) 2023 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.dhcp6; + +import static android.system.OsConstants.AF_INET6; +import static android.system.OsConstants.IPPROTO_UDP; +import static android.system.OsConstants.SOCK_DGRAM; +import static android.system.OsConstants.SOCK_NONBLOCK; + +import static com.android.net.module.util.NetworkStackConstants.ALL_DHCP_RELAY_AGENTS_AND_SERVERS; +import static com.android.net.module.util.NetworkStackConstants.DHCP6_CLIENT_PORT; +import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT; +import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY; + +import android.content.Context; +import android.net.ip.IpClient; +import android.net.util.SocketUtils; +import android.os.Handler; +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.util.StateMachine; +import com.android.net.module.util.InterfaceParams; +import com.android.net.module.util.PacketReader; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.net.SocketException; +import java.nio.ByteBuffer; + +/** + * A DHCPv6 client. + * + * So far only support IA_PD (prefix delegation), not for IA_NA/IA_TA yet. + * + * @hide + */ +public class Dhcp6Client extends StateMachine { + private static final String TAG = Dhcp6Client.class.getSimpleName(); + private static final boolean DBG = true; + + // Internal messages. + // Dhcp6Client shares the same handler with IpClient, define the base command range for + // both public and private messages used in Dhcp6Client, to avoid commands overlap. + private static final int PRIVATE_BASE = IpClient.DHCP6CLIENT_CMD_BASE + 100; + private static final int CMD_RECEIVED_PACKET = PRIVATE_BASE + 1; + + @NonNull private final Context mContext; + + // State variables. + @NonNull private final StateMachine mController; + @NonNull private final String mIfaceName; + + private Dhcp6Client(@NonNull final Context context, @NonNull final StateMachine controller, + @NonNull final InterfaceParams iface) { + super(TAG, controller.getHandler()); + + mContext = context; + mController = controller; + mIfaceName = iface.name; + + // TODO: add state machine initialization. + } + + private class Dhcp6PacketHandler extends PacketReader { + private FileDescriptor mUdpSock; + + Dhcp6PacketHandler(Handler handler) { + super(handler); + } + + @Override + protected void handlePacket(byte[] recvbuf, int length) { + try { + final Dhcp6Packet packet = Dhcp6Packet.decodePacket(recvbuf, length); + if (DBG) Log.d(TAG, "Received packet: " + packet); + sendMessage(CMD_RECEIVED_PACKET, packet); + } catch (Dhcp6Packet.ParseException e) { + Log.e(TAG, "Can't parse DHCPv6 packet: " + e.getMessage()); + } + } + + @Override + protected FileDescriptor createFd() { + try { + mUdpSock = Os.socket(AF_INET6, SOCK_DGRAM | SOCK_NONBLOCK, IPPROTO_UDP); + SocketUtils.bindSocketToInterface(mUdpSock, mIfaceName); + Os.bind(mUdpSock, IPV6_ADDR_ANY, DHCP6_CLIENT_PORT); + } catch (SocketException | ErrnoException e) { + Log.e(TAG, "Error creating udp socket", e); + closeFd(mUdpSock); + mUdpSock = null; + return null; + } + return mUdpSock; + } + + @Override + protected int readPacket(FileDescriptor fd, byte[] packetBuffer) throws Exception { + try { + return Os.read(fd, packetBuffer, 0, packetBuffer.length); + } catch (IOException | ErrnoException e) { + Log.e(TAG, "Fail to read packet"); + throw e; + } + } + + public int transmitPacket(final ByteBuffer buf) throws ErrnoException, SocketException { + int ret = Os.sendto(mUdpSock, buf.array(), 0 /* byteOffset */, + buf.limit() /* byteCount */, 0 /* flags */, ALL_DHCP_RELAY_AGENTS_AND_SERVERS, + DHCP6_SERVER_PORT); + return ret; + } + } +}
diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java index 98c214b..6d77d4e 100644 --- a/src/android/net/dhcp6/Dhcp6Packet.java +++ b/src/android/net/dhcp6/Dhcp6Packet.java
@@ -180,7 +180,7 @@ } } - private static void skipOption(final ByteBuffer packet, int optionLen) + private static void skipOption(@NonNull final ByteBuffer packet, int optionLen) throws BufferUnderflowException { for (int i = 0; i < optionLen; i++) { packet.get(); @@ -297,6 +297,8 @@ statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */); break; default: + expectedLen = optionLen; + // BufferUnderflowException will be thrown if option is truncated. skipOption(packet, optionLen); break; } @@ -325,6 +327,12 @@ case DHCP6_MESSAGE_TYPE_REPLY: newPacket = new Dhcp6ReplyPacket(transId, clientDuid, serverDuid, iapd); break; + case DHCP6_MESSAGE_TYPE_RENEW: + newPacket = new Dhcp6RenewPacket(transId, secs, clientDuid, serverDuid, iapd); + break; + case DHCP6_MESSAGE_TYPE_REBIND: + newPacket = new Dhcp6RebindPacket(transId, secs, clientDuid, iapd); + break; default: throw new ParseException("Unimplemented DHCP6 message type %d" + messageType); } @@ -345,6 +353,15 @@ } /** + * Parse a packet from an array of bytes, stopping at the given length. + */ + public static Dhcp6Packet decodePacket(@NonNull final byte[] packet, int length) + throws ParseException { + final ByteBuffer buffer = ByteBuffer.wrap(packet, 0, length).order(ByteOrder.BIG_ENDIAN); + return decodePacket(buffer); + } + + /** * Adds an optional parameter containing an array of bytes. */ protected static void addTlv(ByteBuffer buf, short type, @NonNull byte[] payload) { @@ -403,4 +420,23 @@ new Dhcp6RequestPacket(transId, secs, clientDuid, serverDuid, iapd); return pkt.buildPacket(); } + + /** + * Builds a DHCPv6 RENEW packet from the required specified parameters. + */ + public static ByteBuffer buildRenewPacket(int transId, short secs, @NonNull final byte[] iapd, + @NonNull final byte[] clientDuid, @NonNull final byte[] serverDuid) { + final Dhcp6RenewPacket pkt = + new Dhcp6RenewPacket(transId, secs, clientDuid, serverDuid, iapd); + return pkt.buildPacket(); + } + + /** + * Builds a DHCPv6 REBIND packet from the required specified parameters. + */ + public static ByteBuffer buildRebindPacket(int transId, short secs, @NonNull final byte[] iapd, + @NonNull final byte[] clientDuid) { + final Dhcp6RebindPacket pkt = new Dhcp6RebindPacket(transId, secs, clientDuid, iapd); + return pkt.buildPacket(); + } }
diff --git a/src/android/net/dhcp6/Dhcp6RebindPacket.java b/src/android/net/dhcp6/Dhcp6RebindPacket.java new file mode 100644 index 0000000..10c10b9 --- /dev/null +++ b/src/android/net/dhcp6/Dhcp6RebindPacket.java
@@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 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.dhcp6; + +import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * DHCPv6 REBIND packet class, a client sends a Rebind message to any available server to extend + * the lifetimes on the leases assigned to the client and to update other configuration parameters. + * This message is sent after a client receives no response to a Renew message. + * + * https://tools.ietf.org/html/rfc8415#page-24 + */ +public class Dhcp6RebindPacket extends Dhcp6Packet { + /** + * Generates a rebind packet with the specified parameters. + */ + Dhcp6RebindPacket(int transId, short secs, @NonNull final byte[] clientDuid, + @NonNull final byte[] iapd) { + super(transId, secs, clientDuid, null /* serverDuid */, iapd); + } + + /** + * Build a DHCPv6 Rebind message with the specific parameters. + */ + public ByteBuffer buildPacket() { + final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH); + final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_REBIND << 24) | (mTransId & 0x0FFF); + packet.putInt(msgTypeAndTransId); + + addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid()); + addTlv(packet, DHCP6_ELAPSED_TIME, mSecs); + addTlv(packet, DHCP6_IA_PD, mIaPd); + + packet.flip(); + return packet; + } +}
diff --git a/src/android/net/dhcp6/Dhcp6RenewPacket.java b/src/android/net/dhcp6/Dhcp6RenewPacket.java new file mode 100644 index 0000000..8715b59 --- /dev/null +++ b/src/android/net/dhcp6/Dhcp6RenewPacket.java
@@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 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.dhcp6; + +import static com.android.net.module.util.NetworkStackConstants.DHCP_MAX_LENGTH; + +import androidx.annotation.NonNull; + +import java.nio.ByteBuffer; + +/** + * DHCPv6 RENEW packet class, a client sends an Renew message to the server that originally + * provided the client's leases and configuration parameters to extend the lifetimes on the + * leases assigned to the client and to update other configuration parameters. + * + * https://tools.ietf.org/html/rfc8415#page-24 + */ +public class Dhcp6RenewPacket extends Dhcp6Packet { + /** + * Generates a renew packet with the specified parameters. + */ + Dhcp6RenewPacket(int transId, short secs, @NonNull final byte[] clientDuid, + @NonNull final byte[] serverDuid, final byte[] iapd) { + super(transId, secs, clientDuid, serverDuid, iapd); + } + + /** + * Build a DHCPv6 Renew message with the specific parameters. + */ + public ByteBuffer buildPacket() { + final ByteBuffer packet = ByteBuffer.allocate(DHCP_MAX_LENGTH); + final int msgTypeAndTransId = (DHCP6_MESSAGE_TYPE_RENEW << 24) | (mTransId & 0x0FFF); + packet.putInt(msgTypeAndTransId); + + addTlv(packet, DHCP6_SERVER_IDENTIFIER, getServerDuid()); + addTlv(packet, DHCP6_CLIENT_IDENTIFIER, getClientDuid()); + addTlv(packet, DHCP6_ELAPSED_TIME, mSecs); + addTlv(packet, DHCP6_IA_PD, mIaPd); + + packet.flip(); + return packet; + } +}
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index 9abaf92..5737d87 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java
@@ -500,6 +500,9 @@ // IpClient shares a handler with DhcpClient: commands must not overlap public static final int DHCPCLIENT_CMD_BASE = 1000; + // IpClient shares a handler with Dhcp6Client: commands must not overlap + public static final int DHCP6CLIENT_CMD_BASE = 2000; + // Settings and default values. private static final int MAX_LOG_RECORDS = 500; private static final int MAX_PACKET_RECORDS = 100;
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java index 0771bff..71632af 100644 --- a/src/android/net/ip/IpClientLinkObserver.java +++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -65,6 +65,7 @@ import java.net.Inet6Address; import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -539,7 +540,17 @@ if (!mNetlinkEventParsingEnabled) return; final String[] addresses = new String[opt.servers.length]; for (int i = 0; i < opt.servers.length; i++) { - addresses[i] = opt.servers[i].getHostAddress(); + Inet6Address addr = (Inet6Address) opt.servers[i]; + if (addr.isLinkLocalAddress()) { + try { + addr = Inet6Address.getByAddress(null /* hostname */, addr.getAddress(), + mIfindex); + } catch (UnknownHostException impossible) { + Log.wtf("Cannot construct scoped Inet6Address with Inet6Address.getAddress(" + + addr.getHostAddress() + "): ", impossible); + } + } + addresses[i] = addr.getHostAddress(); } updateInterfaceDnsServerInfo(opt.header.lifetime, addresses); }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java index 9790ec9..2fb5ac8 100644 --- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java +++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -729,7 +729,14 @@ final TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () -> { final TestNetworkManager tnm = inst.getContext().getSystemService(TestNetworkManager.class); - return tnm.createTapInterface(); + try { + return tnm.createTapInterface(true /* carrierUp */, true /* bringUp */, + true /* disableIpv6ProvisioningDelay */); + } catch (NoSuchMethodError e) { + // createTapInterface(boolean, boolean, boolean) has been introduced since T, + // use the legancy API if the method is not found on previous platforms. + return tnm.createTapInterface(); + } }); mIfaceName = iface.getInterfaceName(); mClientMac = getIfaceMacAddr(mIfaceName).toByteArray(); @@ -2039,6 +2046,33 @@ reset(mCb); } + @Test + public void testRaRdnss_Ipv6LinkLocalDns() throws Exception { + ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() + .withoutIpReachabilityMonitor() + .withoutIPv4() + .build(); + startIpClientProvisioning(config); + + final ByteBuffer pio = buildPioOption(600, 300, "2001:db8:1::/64"); + // put an IPv6 link-local DNS server + final ByteBuffer rdnss = buildRdnssOption(600, ROUTER_LINK_LOCAL.getHostAddress()); + // put SLLA option to avoid address resolution for "fe80::1" + final ByteBuffer slla = buildSllaOption(); + final ByteBuffer ra = buildRaPacket(pio, rdnss, slla); + + waitForRouterSolicitation(); + mPacketReader.sendResponse(ra); + + final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class); + verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture()); + final LinkProperties lp = captor.getValue(); + assertNotNull(lp); + assertEquals(1, lp.getDnsServers().size()); + assertEquals(ROUTER_LINK_LOCAL, (Inet6Address) lp.getDnsServers().get(0)); + assertTrue(lp.isIpv6Provisioned()); + } + private void expectNat64PrefixUpdate(InOrder inOrder, IpPrefix expected) throws Exception { inOrder.verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange( argThat(lp -> Objects.equals(expected, lp.getNat64Prefix()))); @@ -2882,10 +2916,12 @@ final InOrder inOrder = inOrder(mCb); final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>(); - // Start IPv4 provisioning first and then IPv6 provisioning, which is more realistic. - // And wait until entire provisioning completes. + // Start IPv4 provisioning first and wait IPv4 provisioning to succeed, and then start + // IPv6 provisioning, which is more realistic and avoid the flaky case of both IPv4 and + // IPv6 provisioning complete at the same time. handleDhcpPackets(true /* isSuccessLease */, TEST_LEASE_DURATION_S, true /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, null /* serverSentUrl */); + verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(any()); waitForRouterSolicitation(); mPacketReader.sendResponse(ra);
diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java index 46c36a0..0b3bba7 100644 --- a/tests/unit/src/android/net/apf/ApfTest.java +++ b/tests/unit/src/android/net/apf/ApfTest.java
@@ -189,7 +189,12 @@ private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) { final String msg = "Unexpected APF verdict. To debug:\n" + " apf_run --program " + HexDump.toHexString(program) - + " --packet " + HexDump.toHexString(packet) + " --trace | less\n "; + + " --packet " + HexDump.toHexString(packet) + " --trace | less\n "; + assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge)); + } + + private void assertVerdict(String msg, int expected, byte[] program, byte[] packet, + int filterAge) { assertReturnCodesEqual(msg, expected, apfSimulate(program, packet, null, filterAge)); } @@ -1395,6 +1400,25 @@ return builder.finalizePacket().array(); } + /** Adds to the program a no-op instruction that is one byte long. */ + private void addOneByteNoop(ApfGenerator gen) { + gen.addOr(0); + } + + @Test + public void testAddOneByteNoopAddsOneByte() throws Exception { + ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION); + addOneByteNoop(gen); + assertEquals(1, gen.generate().length); + + final int count = 42; + gen = new ApfGenerator(MIN_APF_VERSION); + for (int i = 0; i < count; i++) { + addOneByteNoop(gen); + } + assertEquals(count, gen.generate().length); + } + @Test public void testQnameEncoding() { String[] qname = new String[]{"abcd", "ef", "日本"}; @@ -1457,15 +1481,21 @@ apfFilter.shutdown(); } + private ApfGenerator generateDnsFilter(boolean ipv6, String... labels) throws Exception { + ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION); + gen.addLoadImmediate(R1, ipv6 ? IPV6_HEADER_LEN : IPV4_HEADER_LEN); + DnsUtils.generateFilter(gen, labels); + return gen; + } + private void doTestDnsParsing(boolean expectPass, boolean ipv6, String filterName, byte[] pkt) throws Exception { - ApfGenerator gen = new ApfGenerator(MIN_APF_VERSION); final String[] labels = filterName.split(/*regex=*/ "[.]"); - DnsUtils.generateFilter(gen, ipv6, labels); + ApfGenerator gen = generateDnsFilter(ipv6, labels); // Hack to prevent the APF instruction limit triggering. for (int i = 0; i < 500; i++) { - gen.addOr(0); + addOneByteNoop(gen); } byte[] program = gen.generate(); @@ -1488,6 +1518,7 @@ final boolean ipv4 = false, ipv6 = true; // Packets with one question. + // Names don't start with _ because DnsPacket thinks such names are invalid. doTestDnsParsing(true, ipv6, "googlecast.tcp.local", "googlecast.tcp.local"); doTestDnsParsing(true, ipv4, "googlecast.tcp.local", "googlecast.tcp.local"); doTestDnsParsing(false, ipv6, "googlecast.tcp.lozal", "googlecast.tcp.local"); @@ -1510,6 +1541,9 @@ doTestDnsParsing(true, ipv6, "googlecast.tcp.local", "www.google.co.jp", "googlecast.tcp.local", "developer.android.com"); + // Name with duplicate labels. + doTestDnsParsing(true, ipv6, "local.tcp.local", "local.tcp.local"); + final byte[] pkt = makeMdnsCompressedV6Packet(); doTestDnsParsing(true, ipv6, "googlecast.tcp.local", pkt); doTestDnsParsing(true, ipv6, "matter.tcp.local", pkt); @@ -1517,6 +1551,106 @@ doTestDnsParsing(false, ipv6, "otherservice.tcp.local", pkt); } + private void doTestDnsParsingProgramLength(int expectedLength, + String filterName) throws Exception { + final String[] labels = filterName.split(/*regex=*/ "[.]"); + + ApfGenerator gen = generateDnsFilter(/*ipv6=*/ true, labels); + assertEquals("Program for " + filterName + " had unexpected length:", + expectedLength, gen.generate().length); + } + + /** + * Rough metric of code size. Checks how large the generated filter is in various scenarios. + * Helps ensure any changes to the code do not substantially increase APF code size. + */ + @Test + public void testDnsParsingProgramLength() throws Exception { + doTestDnsParsingProgramLength(237, "MyDevice.local"); + doTestDnsParsingProgramLength(285, "_googlecast.tcp.local"); + doTestDnsParsingProgramLength(291, "_googlecast12345.tcp.local"); + doTestDnsParsingProgramLength(244, "_googlecastZtcp.local"); + doTestDnsParsingProgramLength(249, "_googlecastZtcp12345.local"); + } + + private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead, + String filterName, byte[] pkt, String description) throws Exception { + final String[] labels = filterName.split(/*regex=*/ "[.]"); + + // Check that the generated code, when the program contains the specified number of extra + // bytes, is capable of dropping the packet. + ApfGenerator gen = generateDnsFilter(/*ipv6=*/ true, labels); + for (int i = 0; i < expectedNecessaryOverhead; i++) { + addOneByteNoop(gen); + } + final byte[] programWithJustEnoughOverhead = gen.generate(); + assertVerdict( + "Overhead too low: filter for " + filterName + " with " + expectedNecessaryOverhead + + " extra instructions unexpectedly passed " + description, + DROP, programWithJustEnoughOverhead, pkt, 0); + + if (expectedNecessaryOverhead == 0) return; + + // Check that the generated code, without the specified number of extra program bytes, + // cannot correctly drop the packet because it hits the interpreter instruction limit. + gen = generateDnsFilter(/*ipv6=*/ true, labels); + for (int i = 0; i < expectedNecessaryOverhead - 1; i++) { + addOneByteNoop(gen); + } + final byte[] programWithNotEnoughOverhead = gen.generate(); + + assertVerdict( + "Overhead too high: filter for " + filterName + " with " + expectedNecessaryOverhead + + " extra instructions unexpectedly dropped " + description, + PASS, programWithNotEnoughOverhead, pkt, 0); + } + + private void doTestDnsParsingNecessaryOverhead(int expectedNecessaryOverhead, + String filterName, String... packetNames) throws Exception { + doTestDnsParsingNecessaryOverhead(expectedNecessaryOverhead, filterName, + makeMdnsV6Packet(packetNames), + "IPv6 MDNS packet containing: " + Arrays.toString(packetNames)); + } + + /** + * Rough metric of filter efficiency. Because the filter uses backwards jumps, on complex + * packets it will not finish running before the interpreter hits the maximum number of allowed + * instructions (== number of bytes in the program) and unconditionally accepts the packet. + * This test checks much extra code the program must contain in order for the generated filter + * to successfully drop the packet. It helps ensure any changes to the code do not reduce the + * complexity of packets that the APF code can drop. + */ + @Test + public void testDnsParsingNecessaryOverhead() throws Exception { + // Simple packets can be parsed with zero extra code. + doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local", + "matter.tcp.local", "developer.android.com"); + + doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local", + "developer.android.com", "matter.tcp.local"); + + doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local", + "developer.android.com", "matter.tcp.local", "www.google.co.jp"); + + doTestDnsParsingNecessaryOverhead(0, "googlecast.tcp.local", + "developer.android.com", "matter.tcp.local", "www.google.co.jp", + "example.org"); + + // More complicated packets cause more instructions to be run and can only be dropped if + // the program contains lots of extra code. + doTestDnsParsingNecessaryOverhead(57, "googlecast.tcp.local", + "developer.android.com", "matter.tcp.local", "www.google.co.jp", + "example.org", "otherexample.net"); + + doTestDnsParsingNecessaryOverhead(115, "googlecast.tcp.local", + "developer.android.com", "matter.tcp.local", "www.google.co.jp", + "example.org", "otherexample.net", "docs.new"); + + doTestDnsParsingNecessaryOverhead(0, "foo.tcp.local", + makeMdnsCompressedV6Packet(), "compressed packet"); + + } + @Test public void testApfFilterMulticast() throws Exception { final byte[] unicastIpv4Addr = {(byte)192,0,2,63};
diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt index 264f785..2d093f7 100644 --- a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt +++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt
@@ -53,7 +53,7 @@ "01000F51" + // client identifier option(option_len=12) "0001000C0003001B024CCBFFFE5F6EA9" + - // elapsed time option(wrong option_len: 4) + // elapsed time option(wrong option_len=4) "000800040000" + // IA_PD option(option_len=41, including IA prefix option) "00190029DE3570F50000000000000000" + @@ -102,4 +102,47 @@ Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes)) } } + + @Test + fun testDecodeDhcp6AdvertisePacket() { + val advertiseHex = + // Advertise, Transaction ID + "0200078A" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B024CCBFFFE5F6EA9" + + // IA_PD option(option_len=70, including IA prefix option) + "001900460CDDCA0C000000CF0000014C" + + // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64") + "001A00190000019F0000064F402401FA00049C04810000000000000000" + + // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64") + "001A00190000019F0000A8C040FDFD9ED6795000010000000000000000" + val bytes = HexDump.hexStringToByteArray(advertiseHex) + val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes)) + assertTrue(packet is Dhcp6AdvertisePacket) + } + + @Test + fun testDecodeDhcp6SolicitPacket_unsupportedOption() { + val advertiseHex = + // Advertise, Transaction ID + "0200078A" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B024CCBFFFE5F6EA9" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003C" + + // IA_PD option(option_len=70, including IA prefix option) + "001900460CDDCA0C000000CF0000014C" + + // IA prefix option(option_len=25, prefix="2401:fa00:49c:412::/64") + "001A00190000019F0000064F402401FA00049C04810000000000000000" + + // IA prefix option(option_len=25, prefix="fdfd:9ed6:7950:2::/64") + "001A00190000019F0000A8C040FDFD9ED6795000010000000000000000" + val bytes = HexDump.hexStringToByteArray(advertiseHex) + // The unsupported option will be skipped normally and won't throw ParseException. + val packet = Dhcp6Packet.decodePacket(ByteBuffer.wrap(bytes)) + assertTrue(packet is Dhcp6AdvertisePacket) + } }