diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-10 19:01:43 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2024-01-10 19:01:43 +0000 |
commit | 2e73039336ca1beacf174e85e84ccad8ec8b7e56 (patch) | |
tree | 3813c48b21baee7054a7ddd37fd4d5c44119bd31 | |
parent | 9c786f8049cb97792c3bbd7b05fac98b600cb496 (diff) | |
parent | 8a5811fa39b7fe3b4193ba7619e499418eed7185 (diff) | |
download | NetworkStack-aml_tz5_341510010.tar.gz |
Snap for 11296156 from 8a5811fa39b7fe3b4193ba7619e499418eed7185 to mainline-tzdata5-releaseaml_tz5_341510070aml_tz5_341510050aml_tz5_341510010aml_tz5_341510010
Change-Id: Iaece0b368116d208ed5f04f43b18ed3c068f889c
20 files changed, 2487 insertions, 981 deletions
@@ -58,6 +58,7 @@ java_defaults { name: "NetworkStackNextEnableDefaults", enabled: true, } + // This is a placeholder comment to avoid merge conflicts // as the above target may have different "enabled" values // depending on the branch @@ -72,7 +73,7 @@ java_defaults { "framework-connectivity-t", "framework-statsd", "framework-wifi", - ] + ], } // Common defaults for NetworkStack integration tests, root tests and coverage tests @@ -85,7 +86,7 @@ java_defaults { java_defaults { name: "NetworkStackReleaseApiLevel", - defaults:["NetworkStackReleaseTargetSdk"], + defaults: ["NetworkStackReleaseTargetSdk"], sdk_version: module_34_version, libs: [ "framework-configinfrastructure", @@ -93,7 +94,7 @@ java_defaults { "framework-connectivity-t", "framework-statsd", "framework-wifi", - ] + ], } // Libraries for the API shims @@ -103,12 +104,12 @@ java_defaults { "androidx.annotation_annotation", "networkstack-aidl-latest", ], - static_libs : [ - "modules-utils-build_system" + static_libs: [ + "modules-utils-build_system", ], apex_available: [ "com.android.tethering", - "//apex_available:platform", // For InProcessNetworkStack + "//apex_available:platform", // For InProcessNetworkStack ], min_sdk_version: "30", } @@ -124,6 +125,9 @@ java_library { srcs: ["apishim/common/**/*.java"], sdk_version: "system_current", visibility: ["//visibility:private"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Each level of the shims (29, 30, ...) is its own java_library compiled against the corresponding @@ -137,6 +141,9 @@ java_library { ], sdk_version: "system_29", visibility: ["//visibility:private"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } java_library { @@ -153,6 +160,7 @@ java_library { visibility: ["//visibility:private"], lint: { strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", }, } @@ -176,6 +184,9 @@ java_library { ], sdk_version: "module_31", visibility: ["//visibility:private"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } java_library { @@ -196,6 +207,9 @@ java_library { ], sdk_version: "module_33", visibility: ["//visibility:private"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } java_library { @@ -217,6 +231,9 @@ java_library { ], sdk_version: module_34_version, visibility: ["//visibility:private"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Shims for APIs being added to the current development version of Android. These APIs are not @@ -228,7 +245,10 @@ java_library { // are part of the stable shims and scanned when generating jarjar rules. java_library { name: "NetworkStackApi35Shims", - defaults: ["NetworkStackShimsDefaults", "ConnectivityNextEnableDefaults"], + defaults: [ + "NetworkStackShimsDefaults", + "ConnectivityNextEnableDefaults", + ], srcs: [ "apishim/35/**/*.java", ], @@ -243,10 +263,13 @@ java_library { "framework-connectivity", "framework-connectivity-t.stubs.module_lib", "framework-tethering", - "android.net.ipsec.ike.stubs.module_lib" + "android.net.ipsec.ike.stubs.module_lib", ], sdk_version: "module_current", visibility: ["//visibility:private"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // API current uses the API current shims directly. @@ -274,6 +297,9 @@ java_library { "//packages/modules/Connectivity/service-t", "//packages/modules/Connectivity/tests:__subpackages__", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // API stable uses jarjar to rename the latest stable apishim package from @@ -281,7 +307,10 @@ java_library { // the networkstack code. java_library { name: "NetworkStackApiStableShims", - defaults: ["NetworkStackShimsDefaults", "NetworkStackReleaseApiLevel"], + defaults: [ + "NetworkStackShimsDefaults", + "NetworkStackReleaseApiLevel", + ], static_libs: [ "NetworkStackShimsCommon", "NetworkStackApi29Shims", @@ -297,6 +326,9 @@ java_library { "//packages/modules/Connectivity/service-t", "//packages/modules/Connectivity/tests:__subpackages__", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Common defaults for android libraries containing network stack code, used to compile variants of @@ -336,7 +368,7 @@ android_library { ], srcs: [ "src/**/*.java", - ":statslog-networkstack-java-gen-current" + ":statslog-networkstack-java-gen-current", ], static_libs: [ "NetworkStackApiCurrentShims", @@ -348,12 +380,18 @@ android_library { "//packages/modules/NetworkStack/tests/unit", "//packages/modules/NetworkStack/tests/integration", ], - lint: { strict_updatability_linting: true }, + lint: { + strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", + }, } android_library { name: "NetworkStackApiStableLib", - defaults: ["NetworkStackReleaseApiLevel", "NetworkStackAndroidLibraryDefaults"], + defaults: [ + "NetworkStackReleaseApiLevel", + "NetworkStackAndroidLibraryDefaults", + ], srcs: [ "src/**/*.java", ":statslog-networkstack-java-gen-stable", @@ -370,7 +408,10 @@ android_library { "//packages/modules/NetworkStack/tests/unit", "//packages/modules/NetworkStack/tests/integration", ], - lint: { strict_updatability_linting: true }, + lint: { + strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", + }, } java_library { @@ -396,6 +437,9 @@ java_library { "//packages/modules/Connectivity/Tethering/tests/integration", "//packages/modules/Connectivity/tests/cts/net", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } java_genrule { @@ -455,12 +499,18 @@ android_app { certificate: "platform", manifest: "AndroidManifest_InProcess.xml", // InProcessNetworkStack is a replacement for NetworkStack - overrides: ["NetworkStack", "NetworkStackNext"], + overrides: [ + "NetworkStack", + "NetworkStackNext", + ], // The InProcessNetworkStack goes together with the PlatformCaptivePortalLogin, which replaces // the default CaptivePortalLogin. required: [ "PlatformCaptivePortalLogin", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Pre-merge the AndroidManifest for NetworkStackNext, so that its manifest can be merged on top @@ -472,7 +522,10 @@ android_library { "ConnectivityNextEnableDefaults", ], static_libs: ["NetworkStackApiCurrentLib"], - manifest: "AndroidManifest.xml" + manifest: "AndroidManifest.xml", + lint: { + baseline_filename: "lint-baseline.xml", + }, } // NetworkStack build targeting the current API release, for testing on in-development SDK @@ -490,13 +543,19 @@ android_app { "privapp_whitelist_com.android.networkstack", ], updatable: true, - lint: { strict_updatability_linting: true }, + lint: { + strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", + }, } // Updatable network stack for finalized API android_app { name: "NetworkStack", - defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"], + defaults: [ + "NetworkStackAppDefaults", + "NetworkStackReleaseApiLevel", + ], static_libs: ["NetworkStackApiStableLib"], certificate: "networkstack", manifest: "AndroidManifest.xml", @@ -504,13 +563,16 @@ android_app { "privapp_whitelist_com.android.networkstack", ], updatable: true, - lint: { strict_updatability_linting: true }, + lint: { + strict_updatability_linting: true, + baseline_filename: "lint-baseline.xml", + }, } cc_library_shared { name: "libnetworkstackutilsjni", srcs: [ - "jni/network_stack_utils_jni.cpp" + "jni/network_stack_utils_jni.cpp", ], header_libs: [ "bpf_headers", @@ -548,8 +610,8 @@ genrule { name: "statslog-networkstack-java-gen-current", tools: ["stats-log-api-gen"], cmd: "$(location stats-log-api-gen) --java $(out) --module network_stack" + - " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" + - " --minApiLevel 30", + " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" + + " --minApiLevel 30", out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"], } @@ -557,12 +619,11 @@ genrule { name: "statslog-networkstack-java-gen-stable", tools: ["stats-log-api-gen"], cmd: "$(location stats-log-api-gen) --java $(out) --module network_stack" + - " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" + - " --minApiLevel 30 --compileApiLevel 30", + " --javaPackage com.android.networkstack.metrics --javaClass NetworkStackStatsLog" + + " --minApiLevel 30 --compileApiLevel 30", out: ["com/android/networkstack/metrics/NetworkStackStatsLog.java"], } - version_code_networkstack_next = "300000000" version_code_networkstack_test = "999999999" @@ -570,21 +631,27 @@ genrule { name: "NetworkStackTestAndroidManifest", srcs: ["AndroidManifest.xml"], out: ["TestAndroidManifest.xml"], - cmd: "sed -E 's/versionCode=\"[0-9]+\"/versionCode=\"" - + version_code_networkstack_test - + "\"/' $(in) > $(out)", + cmd: "sed -E 's/versionCode=\"[0-9]+\"/versionCode=\"" + + version_code_networkstack_test + + "\"/' $(in) > $(out)", visibility: ["//visibility:private"], } android_app { name: "TestNetworkStack", - defaults: ["NetworkStackAppDefaults", "NetworkStackReleaseApiLevel"], + defaults: [ + "NetworkStackAppDefaults", + "NetworkStackReleaseApiLevel", + ], static_libs: ["NetworkStackApiStableLib"], certificate: "networkstack", manifest: ":NetworkStackTestAndroidManifest", required: [ "privapp_whitelist_com.android.networkstack", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // When adding or modifying protos, the jarjar rules and possibly proguard rules need @@ -601,4 +668,7 @@ java_library_static { "networkstackprotos", ], defaults: ["NetworkStackReleaseApiLevel"], + lint: { + baseline_filename: "lint-baseline.xml", + }, } diff --git a/common/networkstackclient/Android.bp b/common/networkstackclient/Android.bp index 07fd52e1..060f0da7 100644 --- a/common/networkstackclient/Android.bp +++ b/common/networkstackclient/Android.bp @@ -221,6 +221,9 @@ java_library { "com.android.tethering", "com.android.wifi", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } java_library { @@ -265,4 +268,7 @@ java_library { "com.android.tethering", "com.android.wifi", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java index 2ef9c08e..ee2990b8 100644 --- a/src/android/net/apf/ApfFilter.java +++ b/src/android/net/apf/ApfFilter.java @@ -1148,7 +1148,7 @@ public class ApfFilter implements AndroidPacketFilter { // Generate code to match the packet bytes. if (section.type == PacketSection.Type.MATCH) { gen.addLoadImmediate(Register.R0, section.start); - gen.addJumpIfBytesNotEqual(Register.R0, + gen.addJumpIfBytesAtR0NotEqual( Arrays.copyOfRange(mPacket.array(), section.start, section.start + section.length), nextFilterLabel); @@ -1283,7 +1283,7 @@ public class ApfFilter implements AndroidPacketFilter { final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked(); gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel); // A NAT-T keepalive packet contains 1 byte payload with the value 0xff // Check payload length is 1 @@ -1298,11 +1298,11 @@ public class ApfFilter implements AndroidPacketFilter { // Check that the ports match gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); gen.addAdd(ETH_HEADER_LEN); - gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel); // Payload offset = R0 + UDP header length gen.addAdd(UDP_HEADER_LEN); - gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel); maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE); gen.addJump(mCountAndDropLabel); @@ -1398,7 +1398,7 @@ public class ApfFilter implements AndroidPacketFilter { final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked(); gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel); // Skip to the next filter if it's not zero-sized : // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0 @@ -1420,7 +1420,7 @@ public class ApfFilter implements AndroidPacketFilter { gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN); gen.addAddR1(); - gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel); maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK); gen.addJump(mCountAndDropLabel); @@ -1522,7 +1522,7 @@ public class ApfFilter implements AndroidPacketFilter { // Drop if not ARP IPv4. gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET); maybeSetupCounter(gen, Counter.DROPPED_ARP_NON_IPV4); - gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndDropLabel); + gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndDropLabel); // Drop if unknown ARP opcode. gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET); @@ -1538,7 +1538,7 @@ public class ApfFilter implements AndroidPacketFilter { // Pass if non-broadcast reply. gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY); - gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel); // Either a request, or a broadcast reply. gen.defineLabel(checkTargetIPv4); @@ -1552,7 +1552,7 @@ public class ApfFilter implements AndroidPacketFilter { // and broadcast replies with a different target IPv4 address. gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET); maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST); - gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel); + gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel); } maybeSetupCounter(gen, Counter.PASSED_ARP); @@ -1600,7 +1600,7 @@ public class ApfFilter implements AndroidPacketFilter { gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET); // NOTE: Relies on R1 containing IPv4 header offset. gen.addAddR1(); - gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter); + gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter); maybeSetupCounter(gen, Counter.PASSED_DHCP); gen.addJump(mCountAndPassLabel); @@ -1639,7 +1639,7 @@ public class ApfFilter implements AndroidPacketFilter { // TODO: can we invert this condition to fall through to the common pass case below? maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST); gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel); maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST); gen.addJump(mCountAndDropLabel); } @@ -1763,8 +1763,7 @@ public class ApfFilter implements AndroidPacketFilter { // TODO: Drop only if they don't contain the address of on-link neighbours. final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15); gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix, - skipUnsolicitedMulticastNALabel); + gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel); maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA); gen.addJump(mCountAndDropLabel); @@ -1821,8 +1820,7 @@ public class ApfFilter implements AndroidPacketFilter { // Check it's L2 mDNS multicast address. gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, - skipMdnsv4Filter); + gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, skipMdnsv4Filter); // Checks it's IPv4. gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); @@ -1845,8 +1843,7 @@ public class ApfFilter implements AndroidPacketFilter { // Checks it's L2 mDNS multicast address. // Relies on R0 containing the ethernet destination mac address offset. - gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, - skipMdnsFilter); + gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter); // Checks it's IPv6. gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); @@ -1877,7 +1874,7 @@ public class ApfFilter implements AndroidPacketFilter { for (int i = 0; i < mMdnsAllowList.size(); ++i) { final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i; final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i)); - gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck); + gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck); // QNAME matched gen.addJump(mDnsAcceptPacket); // QNAME not matched @@ -2023,7 +2020,7 @@ public class ApfFilter implements AndroidPacketFilter { // Drop non-IP non-ARP broadcasts, pass the rest gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST); - gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel); maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST); gen.addJump(mCountAndDropLabel); diff --git a/src/android/net/apf/ApfGenerator.java b/src/android/net/apf/ApfGenerator.java index 0c4007bc..6346a02b 100644 --- a/src/android/net/apf/ApfGenerator.java +++ b/src/android/net/apf/ApfGenerator.java @@ -16,7 +16,13 @@ package android.net.apf; +import static android.net.apf.ApfGenerator.Register.R0; +import static android.net.apf.ApfGenerator.Register.R1; + +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.HexDump; import java.util.ArrayList; import java.util.HashMap; @@ -26,7 +32,7 @@ import java.util.List; * APF assembler/generator. A tool for generating an APF program. * * Call add*() functions to add instructions to the program, then call - * {@link generate} to get the APF bytecode for the program. + * {@link ApfGenerator#generate} to get the APF bytecode for the program. * * @hide */ @@ -63,7 +69,12 @@ public class ApfGenerator { OR(11), // Or, e.g. "or R0,5" SH(12), // Left shift, e.g, "sh R0, 5" or "sh R0, -5" (shifts right) LI(13), // Load immediate, e.g. "li R0,5" (immediate encoded as signed value) - JMP(14), // Jump, e.g. "jmp label" + // Jump, e.g. "jmp label" + // In APFv6, we use JMP(R=1) to encode the DATA instruction. DATA is executed as a jump. + // It tells how many bytes of the program regions are used to store the data and followed + // by the actual data bytes. + // "e.g. data 5, abcde" + JMP(14), JEQ(15), // Compare equal and branch, e.g. "jeq R0,5,label" JNE(16), // Compare not equal and branch, e.g. "jne R0,5,label" JGT(17), // Compare greater than and branch, e.g. "jgt R0,5,label" @@ -73,12 +84,17 @@ public class ApfGenerator { EXT(21), // Followed by immediate indicating ExtendedOpcodes. LDDW(22), // Load 4 bytes from data memory address (register + immediate): "lddw R0, [5]R1" STDW(23), // Store 4 bytes to data memory address (register + immediate): "stdw R0, [5]R1" - WRITE(24), // Write 1, 2 or 4 bytes imm to the output buffer, e.g. "WRITE 5" - // Copy the data from input packet or APF data region to output buffer. Register bit is - // used to specify the source of data copy: R=0 means copy from packet, R=1 means copy - // from APF data region. The source offset is encoded in the first imm and the copy length - // is encoded in the second imm. "e.g. MEMCOPY(R=0), 5, 5" - MEMCOPY(25); + // Write 1, 2 or 4 bytes immediate to the output buffer and auto-increment the pointer to + // write. e.g. "write 5" + WRITE(24), + // Copy bytes from input packet/APF program/data region to output buffer and + // auto-increment the output buffer pointer. + // Register bit is used to specify the source of data copy. + // R=0 means copy from packet. + // R=1 means copy from APF program/data region. + // The copy length is stored in (u8)imm2. + // e.g. "pktcopy 5, 5" "datacopy 5, 5" + PKTDATACOPY(25); final int value; @@ -95,22 +111,50 @@ public class ApfGenerator { NEG(33), // Negate, e.g. "neg R0" SWAP(34), // Swap, e.g. "swap R0,R1" MOVE(35), // Move, e.g. "move R0,R1" - ALLOC(36), // Allocate buffer, "e.g. ALLOC R0" + // Allocate writable output buffer. + // R=0, use register R0 to store the length. R=1, encode the length in the u16 int imm2. + // "e.g. allocate R0" + // "e.g. allocate 123" + ALLOCATE(36), // Transmit and deallocate the buffer (transmission can be delayed until the program // terminates). R=0 means discard the buffer, R=1 means transmit the buffer. // "e.g. trans" // "e.g. discard" TRANSMIT(37), DISCARD(37), - EWRITE1(38), // Write 1 byte from register to the output buffer, e.g. "EWRITE1 R0" - EWRITE2(39), // Write 2 bytes from register to the output buffer, e.g. "EWRITE2 R0" - EWRITE4(40), // Write 4 bytes from register to the output buffer, e.g. "EWRITE4 R0" - // Copy the data from input packet to output buffer. The source offset is encoded as [Rx - // + second imm]. The copy length is encoded in the third imm. "e.g. EPKTCOPY [R0 + 5], 5" + // Write 1, 2 or 4 byte value from register to the output buffer and auto-increment the + // output buffer pointer. + // e.g. "ewrite1 r0" + EWRITE1(38), + EWRITE2(39), + EWRITE4(40), + // Copy bytes from input packet/APF program/data region to output buffer and + // auto-increment the output buffer pointer. + // The copy src offset is stored in R0. + // when R=0, the copy length is stored in (u8)imm2. + // when R=1, the copy length is stored in R1. + // e.g. "pktcopy r0, 5", "pktcopy r0, r1", "datacopy r0, 5", "datacopy r0, r1" EPKTCOPY(41), - // Copy the data from APF data region to output buffer. The source offset is encoded as [Rx - // + second imm]. The copy length is encoded in the third imm. "e.g. EDATACOPY [R0 + 5], 5" - EDATACOPY(42); + EDATACOPY(42), + // Jumps if the UDP payload content (starting at R0) does not contain ont + // of the specified QNAME, applying case insensitivity. + // R0: Offset to UDP payload content + // R=0/1 meanining 'does not match' vs 'matches' + // imm1: Opcode + // imm2: Label offset + // imm3(u8): Question type (PTR/SRV/TXT/A/AAAA) + // imm4(bytes): TLV-encoded QNAME list (null-terminated) + // e.g.: "jdnsqmatch R0,label,0x0c,\002aa\005local\0\0" + JDNSQMATCH(43), // Jumps if the UDP payload content (starting at R0) does not contain one + // of the specified NAME in answers/authority/additional records, applying + // case insensitivity. + // R=0/1 meanining 'does not match' vs 'matches' + // R0: Offset to UDP payload content + // imm1: Opcode + // imm2: Label offset + // imm3(bytes): TLV-encoded QNAME list (null-terminated) + // e.g.: "jdnsamatch R0,label,0x0c,\002aa\005local\0\0" + JDNSAMATCH(44); final int value; @@ -129,34 +173,136 @@ public class ApfGenerator { } } - private static class Immediate { - public final boolean mSigned; - public final byte mImmSize; + private enum IntImmediateType { + INDETERMINATE_SIZE_SIGNED, + INDETERMINATE_SIZE_UNSIGNED, + SIGNED_8, + UNSIGNED_8, + SIGNED_BE16, + UNSIGNED_BE16, + SIGNED_BE32, + UNSIGNED_BE32; + } + + private static class IntImmediate { + public final IntImmediateType mImmediateType; public final int mValue; - Immediate(int value, boolean signed) { - this(value, signed, calculateImmSize(value, signed)); + IntImmediate(int value, IntImmediateType type) { + mImmediateType = type; + mValue = value; } - Immediate(int value, boolean signed, byte size) { - mValue = value; - mSigned = signed; - mImmSize = size; + private int calculateIndeterminateSize() { + switch (mImmediateType) { + case INDETERMINATE_SIZE_SIGNED: + return calculateImmSize(mValue, true /* signed */); + case INDETERMINATE_SIZE_UNSIGNED: + return calculateImmSize(mValue, false /* signed */); + default: + // For IMM with determinate size, return 0 to allow Math.max() calculation in + // caller function. + return 0; + } + } + + private int getEncodingSize(int immFieldSize) { + switch (mImmediateType) { + case SIGNED_8: + case UNSIGNED_8: + return 1; + case SIGNED_BE16: + case UNSIGNED_BE16: + return 2; + case SIGNED_BE32: + case UNSIGNED_BE32: + return 4; + case INDETERMINATE_SIZE_SIGNED: + case INDETERMINATE_SIZE_UNSIGNED: { + int minSizeRequired = calculateIndeterminateSize(); + if (minSizeRequired > immFieldSize) { + throw new IllegalStateException( + String.format("immFieldSize: %d is too small to encode value %d", + immFieldSize, mValue)); + } + return immFieldSize; + } + } + throw new IllegalStateException("UnhandledInvalid IntImmediateType: " + mImmediateType); + } + + private int writeValue(byte[] bytecode, Integer writingOffset, int immFieldSize) { + return Instruction.writeValue(mValue, bytecode, writingOffset, + getEncodingSize(immFieldSize)); + } + + public static IntImmediate newSigned(int imm) { + return new IntImmediate(imm, IntImmediateType.INDETERMINATE_SIZE_SIGNED); + } + + public static IntImmediate newUnsigned(long imm) { + // upperBound is 2^32 - 1 + checkRange("Unsigned IMM", imm, 0 /* lowerBound */, + 4294967295L /* upperBound */); + return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_UNSIGNED); + } + + public static IntImmediate newTwosComplementUnsigned(long imm) { + checkRange("Unsigned TwosComplement IMM", imm, Integer.MIN_VALUE, + 4294967295L /* upperBound */); + return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_UNSIGNED); + } + + public static IntImmediate newTwosComplementSigned(long imm) { + checkRange("Signed TwosComplement IMM", imm, Integer.MIN_VALUE, + 4294967295L /* upperBound */); + return new IntImmediate((int) imm, IntImmediateType.INDETERMINATE_SIZE_SIGNED); + } + + public static IntImmediate newS8(byte imm) { + checkRange("S8 IMM", imm, Byte.MIN_VALUE, Byte.MAX_VALUE); + return new IntImmediate(imm, IntImmediateType.SIGNED_8); + } + + public static IntImmediate newU8(int imm) { + checkRange("U8 IMM", imm, 0, 255); + return new IntImmediate(imm, IntImmediateType.UNSIGNED_8); + } + + public static IntImmediate newS16(short imm) { + return new IntImmediate(imm, IntImmediateType.SIGNED_BE16); + } + + public static IntImmediate newU16(int imm) { + checkRange("U16 IMM", imm, 0, 65535); + return new IntImmediate(imm, IntImmediateType.UNSIGNED_BE16); + } + + public static IntImmediate newS32(int imm) { + return new IntImmediate(imm, IntImmediateType.SIGNED_BE32); + } + + public static IntImmediate newU32(long imm) { + // upperBound is 2^32 - 1 + checkRange("U32 IMM", imm, 0 /* lowerBound */, + 4294967295L /* upperBound */); + return new IntImmediate((int) imm, IntImmediateType.UNSIGNED_BE32); } @Override public String toString() { - return "Immediate{" + "mSigned=" + mSigned + ", mImmSize=" + mImmSize + ", mValue=" - + mValue + '}'; + return "IntImmediate{" + "mImmediateType=" + mImmediateType + ", mValue=" + mValue + + '}'; } } private class Instruction { private final byte mOpcode; // A "Opcode" value. private final byte mRegister; // A "Register" value. - public final List<Immediate> mImms = new ArrayList<>(); + public final List<IntImmediate> mIntImms = new ArrayList<>(); // When mOpcode is a jump: - private byte mTargetLabelSize; + private int mTargetLabelSize; + private int mLenFieldOverride = -1; private String mTargetLabel; // When mOpcode == Opcodes.LABEL: private String mLabel; @@ -169,27 +315,81 @@ public class ApfGenerator { mRegister = (byte) register.value; } + Instruction(ExtendedOpcodes extendedOpcodes, Register register) { + this(Opcodes.EXT, register); + addUnsigned(extendedOpcodes.value); + } + + Instruction(ExtendedOpcodes extendedOpcodes, int slot, Register register) + throws IllegalInstructionException { + this(Opcodes.EXT, register); + if (slot < 0 || slot >= MEMORY_SLOTS) { + throw new IllegalInstructionException("illegal memory slot number: " + slot); + } + addUnsigned(extendedOpcodes.value + slot); + } + Instruction(Opcodes opcode) { - this(opcode, Register.R0); + this(opcode, R0); + } + + Instruction(ExtendedOpcodes extendedOpcodes) { + this(extendedOpcodes, R0); + } + + Instruction addSigned(int imm) { + mIntImms.add(IntImmediate.newSigned(imm)); + return this; + } + + Instruction addUnsigned(int imm) { + mIntImms.add(IntImmediate.newUnsigned(imm)); + return this; + } + + + Instruction addTwosCompSigned(int imm) { + mIntImms.add(IntImmediate.newTwosComplementSigned(imm)); + return this; + } + + + Instruction addTwosCompUnsigned(int imm) { + mIntImms.add(IntImmediate.newTwosComplementUnsigned(imm)); + return this; } - void addUnsignedImm(int imm) { - addImm(new Immediate(imm, false)); + Instruction addS8(byte imm) { + mIntImms.add(IntImmediate.newS8(imm)); + return this; } - void addUnsignedImm(int imm, byte size) { - addImm(new Immediate(imm, false, size)); + Instruction addU8(int imm) { + mIntImms.add(IntImmediate.newU8(imm)); + return this; } - void addSignedImm(int imm) { - addImm(new Immediate(imm, true)); + Instruction addS16(short imm) { + mIntImms.add(IntImmediate.newS16(imm)); + return this; } - void addImm(Immediate imm) { - mImms.add(imm); + Instruction addU16(int imm) { + mIntImms.add(IntImmediate.newU16(imm)); + return this; } - void setLabel(String label) throws IllegalInstructionException { + Instruction addS32(int imm) { + mIntImms.add(IntImmediate.newS32(imm)); + return this; + } + + Instruction addU32(long imm) { + mIntImms.add(IntImmediate.newU32(imm)); + return this; + } + + Instruction setLabel(String label) throws IllegalInstructionException { if (mLabels.containsKey(label)) { throw new IllegalInstructionException("duplicate label " + label); } @@ -198,18 +398,23 @@ public class ApfGenerator { } mLabel = label; mLabels.put(label, this); + return this; } - void setTargetLabel(String label) { + Instruction setTargetLabel(String label) { mTargetLabel = label; mTargetLabelSize = 4; // May shrink later on in generate(). + return this; } - void setBytesImm(byte[] bytes) { - if (mOpcode != Opcodes.JNEBS.value) { - throw new IllegalStateException("adding compare bytes to non-JNEBS instruction"); - } + Instruction overrideLenField(int size) { + mLenFieldOverride = size; + return this; + } + + Instruction setBytesImm(byte[] bytes) { mBytesImm = bytes; + return this; } /** @@ -220,11 +425,12 @@ public class ApfGenerator { return 0; } int size = 1; - byte maxImmSize = getMaxImmSize(); - // For the copy opcode, the last imm is the length field is always 1 byte - size += mImms.size() * maxImmSize; + int indeterminateSize = calculateRequiredIndeterminateSize(); + for (IntImmediate imm : mIntImms) { + size += imm.getEncodingSize(indeterminateSize); + } if (mTargetLabel != null) { - size += maxImmSize; + size += indeterminateSize; } if (mBytesImm != null) { size += mBytesImm.length; @@ -241,20 +447,34 @@ public class ApfGenerator { if (mTargetLabel == null) { return false; } - int oldSize = size(); int oldTargetLabelSize = mTargetLabelSize; mTargetLabelSize = calculateImmSize(calculateTargetLabelOffset(), false); if (mTargetLabelSize > oldTargetLabelSize) { throw new IllegalStateException("instruction grew"); } - return size() < oldSize; + return mTargetLabelSize < oldTargetLabelSize; } /** * Assemble value for instruction size field. */ - private byte generateImmSizeField() { - byte immSize = getMaxImmSize(); + private int generateImmSizeField() { + // If we already know the size the length field, just use it + switch (mLenFieldOverride) { + case -1: + break; + case 1: + return 1; + case 2: + return 2; + case 4: + return 3; + default: + throw new IllegalStateException( + "mLenFieldOverride has invalid value: " + mLenFieldOverride); + } + // Otherwise, calculate + int immSize = calculateRequiredIndeterminateSize(); // Encode size field to fit in 2 bits: 0->0, 1->1, 2->2, 3->4. return immSize == 4 ? 3 : immSize; } @@ -263,7 +483,7 @@ public class ApfGenerator { * Assemble first byte of generated instruction. */ private byte generateInstructionByte() { - byte sizeField = generateImmSizeField(); + int sizeField = generateImmSizeField(); return (byte)((mOpcode << 3) | (sizeField << 1) | mRegister); } @@ -276,7 +496,7 @@ public class ApfGenerator { * be sign extended and the truncation should simply throw away their signed * upper bits. */ - private int writeValue(int value, byte[] bytecode, int writingOffset, byte immSize) { + private static int writeValue(int value, byte[] bytecode, int writingOffset, int immSize) { for (int i = immSize - 1; i >= 0; i--) { bytecode[writingOffset++] = (byte)((value >> (i * 8)) & 255); } @@ -284,7 +504,7 @@ public class ApfGenerator { } /** - * Generate bytecode for this instruction at offset {@link offset}. + * Generate bytecode for this instruction at offset {@link Instruction#offset}. */ void generate(byte[] bytecode) throws IllegalInstructionException { if (mOpcode == Opcodes.LABEL.value) { @@ -292,13 +512,20 @@ public class ApfGenerator { } int writingOffset = offset; bytecode[writingOffset++] = generateInstructionByte(); - byte maxImmSize = getMaxImmSize(); + int indeterminateSize = calculateRequiredIndeterminateSize(); + int startOffset = 0; + if (mOpcode == Opcodes.EXT.value) { + // For extend opcode, always write the actual opcode first. + writingOffset = mIntImms.get(startOffset++).writeValue(bytecode, writingOffset, + indeterminateSize); + } if (mTargetLabel != null) { writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset, - maxImmSize); + indeterminateSize); } - for (Immediate imm : mImms) { - writingOffset = writeValue(imm.mValue, bytecode, writingOffset, maxImmSize); + for (int i = startOffset; i < mIntImms.size(); ++i) { + writingOffset = mIntImms.get(i).writeValue(bytecode, writingOffset, + indeterminateSize); } if (mBytesImm != null) { System.arraycopy(mBytesImm, 0, bytecode, writingOffset, mBytesImm.length); @@ -311,17 +538,15 @@ public class ApfGenerator { } /** - * Calculate the size of either the immediate fields or the target label field, if either is - * present. Most instructions have either immediates or a target label field, but for the - * instructions that have both, the size of the target label field must be the same as the - * size of the immediate fields, because there is only one length field in the instruction - * byte, hence why this function simply takes the maximum of those sizes, so neither is - * truncated. + * Calculates the maximum indeterminate size of all IMMs in this instruction. + * <p> + * This method finds the largest size needed to encode any indeterminate-sized IMMs in + * the instruction. This size will be stored in the immLen field. */ - private byte getMaxImmSize() { - byte maxSize = mTargetLabelSize; - for (int i = 0; i < mImms.size(); ++i) { - maxSize = (byte) Math.max(maxSize, mImms.get(i).mImmSize); + private int calculateRequiredIndeterminateSize() { + int maxSize = mTargetLabelSize; + for (IntImmediate imm : mIntImms) { + maxSize = Math.max(maxSize, imm.calculateIndeterminateSize()); } return maxSize; } @@ -401,6 +626,7 @@ public class ApfGenerator { // This version number syncs up with APF_VERSION in hardware/google/apf/apf_interpreter.h public static final int MIN_APF_VERSION = 2; public static final int MIN_APF_VERSION_IN_DEV = 5; + public static final int APF_VERSION_4 = 4; private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>(); @@ -434,11 +660,12 @@ public class ApfGenerator { } } - private void addInstruction(Instruction instruction) { + private ApfGenerator append(Instruction instruction) { if (mGenerated) { throw new IllegalStateException("Program already generated"); } mInstructions.add(instruction); + return this; } /** @@ -457,53 +684,38 @@ public class ApfGenerator { * In this case "next_filter" may not have any generated code associated with it. */ public ApfGenerator defineLabel(String name) throws IllegalInstructionException { - Instruction instruction = new Instruction(Opcodes.LABEL); - instruction.setLabel(name); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.LABEL).setLabel(name)); } /** * Add an unconditional jump instruction to the end of the program. */ public ApfGenerator addJump(String target) { - Instruction instruction = new Instruction(Opcodes.JMP); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.JMP).setTargetLabel(target)); } /** * Add an instruction to the end of the program to load the byte at offset {@code offset} * bytes from the beginning of the packet into {@code register}. */ - public ApfGenerator addLoad8(Register register, int offset) { - Instruction instruction = new Instruction(Opcodes.LDB, register); - instruction.addUnsignedImm(offset); - addInstruction(instruction); - return this; + public ApfGenerator addLoad8(Register r, int ofs) { + return append(new Instruction(Opcodes.LDB, r).addUnsigned(ofs)); } /** * Add an instruction to the end of the program to load 16-bits at offset {@code offset} * bytes from the beginning of the packet into {@code register}. */ - public ApfGenerator addLoad16(Register register, int offset) { - Instruction instruction = new Instruction(Opcodes.LDH, register); - instruction.addUnsignedImm(offset); - addInstruction(instruction); - return this; + public ApfGenerator addLoad16(Register r, int ofs) { + return append(new Instruction(Opcodes.LDH, r).addUnsigned(ofs)); } /** * Add an instruction to the end of the program to load 32-bits at offset {@code offset} * bytes from the beginning of the packet into {@code register}. */ - public ApfGenerator addLoad32(Register register, int offset) { - Instruction instruction = new Instruction(Opcodes.LDW, register); - instruction.addUnsignedImm(offset); - addInstruction(instruction); - return this; + public ApfGenerator addLoad32(Register r, int ofs) { + return append(new Instruction(Opcodes.LDW, r).addUnsigned(ofs)); } /** @@ -511,11 +723,8 @@ public class ApfGenerator { * {@code register}. The offset of the loaded byte from the beginning of the packet is * the sum of {@code offset} and the value in register R1. */ - public ApfGenerator addLoad8Indexed(Register register, int offset) { - Instruction instruction = new Instruction(Opcodes.LDBX, register); - instruction.addUnsignedImm(offset); - addInstruction(instruction); - return this; + public ApfGenerator addLoad8Indexed(Register r, int ofs) { + return append(new Instruction(Opcodes.LDBX, r).addUnsigned(ofs)); } /** @@ -523,11 +732,8 @@ public class ApfGenerator { * {@code register}. The offset of the loaded 16-bits from the beginning of the packet is * the sum of {@code offset} and the value in register R1. */ - public ApfGenerator addLoad16Indexed(Register register, int offset) { - Instruction instruction = new Instruction(Opcodes.LDHX, register); - instruction.addUnsignedImm(offset); - addInstruction(instruction); - return this; + public ApfGenerator addLoad16Indexed(Register r, int ofs) { + return append(new Instruction(Opcodes.LDHX, r).addUnsigned(ofs)); } /** @@ -535,109 +741,81 @@ public class ApfGenerator { * {@code register}. The offset of the loaded 32-bits from the beginning of the packet is * the sum of {@code offset} and the value in register R1. */ - public ApfGenerator addLoad32Indexed(Register register, int offset) { - Instruction instruction = new Instruction(Opcodes.LDWX, register); - instruction.addUnsignedImm(offset); - addInstruction(instruction); - return this; + public ApfGenerator addLoad32Indexed(Register r, int ofs) { + return append(new Instruction(Opcodes.LDWX, r).addUnsigned(ofs)); } /** * Add an instruction to the end of the program to add {@code value} to register R0. */ - public ApfGenerator addAdd(int value) { - Instruction instruction = new Instruction(Opcodes.ADD); - instruction.addUnsignedImm(value); - addInstruction(instruction); - return this; + public ApfGenerator addAdd(int val) { + return append(new Instruction(Opcodes.ADD).addTwosCompUnsigned(val)); } /** * Add an instruction to the end of the program to multiply register R0 by {@code value}. */ - public ApfGenerator addMul(int value) { - Instruction instruction = new Instruction(Opcodes.MUL); - instruction.addUnsignedImm(value); - addInstruction(instruction); - return this; + public ApfGenerator addMul(int val) { + return append(new Instruction(Opcodes.MUL).addUnsigned(val)); } /** * Add an instruction to the end of the program to divide register R0 by {@code value}. */ - public ApfGenerator addDiv(int value) { - Instruction instruction = new Instruction(Opcodes.DIV); - instruction.addUnsignedImm(value); - addInstruction(instruction); - return this; + public ApfGenerator addDiv(int val) { + return append(new Instruction(Opcodes.DIV).addUnsigned(val)); } /** * Add an instruction to the end of the program to logically and register R0 with {@code value}. */ - public ApfGenerator addAnd(int value) { - Instruction instruction = new Instruction(Opcodes.AND); - instruction.addUnsignedImm(value); - addInstruction(instruction); - return this; + public ApfGenerator addAnd(int val) { + return append(new Instruction(Opcodes.AND).addTwosCompUnsigned(val)); } /** * Add an instruction to the end of the program to logically or register R0 with {@code value}. */ - public ApfGenerator addOr(int value) { - Instruction instruction = new Instruction(Opcodes.OR); - instruction.addUnsignedImm(value); - addInstruction(instruction); - return this; + public ApfGenerator addOr(int val) { + return append(new Instruction(Opcodes.OR).addTwosCompUnsigned(val)); } /** * Add an instruction to the end of the program to shift left register R0 by {@code value} bits. */ - public ApfGenerator addLeftShift(int value) { - Instruction instruction = new Instruction(Opcodes.SH); - instruction.addSignedImm(value); - addInstruction(instruction); - return this; + // TODO: consider whether should change the argument type to byte + public ApfGenerator addLeftShift(int val) { + return append(new Instruction(Opcodes.SH).addSigned(val)); } /** * Add an instruction to the end of the program to shift right register R0 by {@code value} * bits. */ - public ApfGenerator addRightShift(int value) { - Instruction instruction = new Instruction(Opcodes.SH); - instruction.addSignedImm(-value); - addInstruction(instruction); - return this; + // TODO: consider whether should change the argument type to byte + public ApfGenerator addRightShift(int val) { + return append(new Instruction(Opcodes.SH).addSigned(-val)); } /** * Add an instruction to the end of the program to add register R1 to register R0. */ public ApfGenerator addAddR1() { - Instruction instruction = new Instruction(Opcodes.ADD, Register.R1); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.ADD, R1)); } /** * Add an instruction to the end of the program to multiply register R0 by register R1. */ public ApfGenerator addMulR1() { - Instruction instruction = new Instruction(Opcodes.MUL, Register.R1); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.MUL, R1)); } /** * Add an instruction to the end of the program to divide register R0 by register R1. */ public ApfGenerator addDivR1() { - Instruction instruction = new Instruction(Opcodes.DIV, Register.R1); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.DIV, R1)); } /** @@ -645,9 +823,7 @@ public class ApfGenerator { * and store the result back into register R0. */ public ApfGenerator addAndR1() { - Instruction instruction = new Instruction(Opcodes.AND, Register.R1); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.AND, R1)); } /** @@ -655,9 +831,7 @@ public class ApfGenerator { * and store the result back into register R0. */ public ApfGenerator addOrR1() { - Instruction instruction = new Instruction(Opcodes.OR, Register.R1); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.OR, R1)); } /** @@ -665,111 +839,77 @@ public class ApfGenerator { * register R1. */ public ApfGenerator addLeftShiftR1() { - Instruction instruction = new Instruction(Opcodes.SH, Register.R1); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.SH, R1)); } /** * Add an instruction to the end of the program to move {@code value} into {@code register}. */ public ApfGenerator addLoadImmediate(Register register, int value) { - Instruction instruction = new Instruction(Opcodes.LI, register); - instruction.addSignedImm(value); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.LI, register).addSigned(value)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value equals {@code value}. */ - public ApfGenerator addJumpIfR0Equals(int value, String target) { - Instruction instruction = new Instruction(Opcodes.JEQ); - instruction.addUnsignedImm(value); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0Equals(int val, String tgt) { + return append(new Instruction(Opcodes.JEQ).addTwosCompUnsigned(val).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value does not equal {@code value}. */ - public ApfGenerator addJumpIfR0NotEquals(int value, String target) { - Instruction instruction = new Instruction(Opcodes.JNE); - instruction.addUnsignedImm(value); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0NotEquals(int val, String tgt) { + return append(new Instruction(Opcodes.JNE).addTwosCompUnsigned(val).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value is greater than {@code value}. */ - public ApfGenerator addJumpIfR0GreaterThan(int value, String target) { - Instruction instruction = new Instruction(Opcodes.JGT); - instruction.addUnsignedImm(value); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0GreaterThan(int val, String tgt) { + return append(new Instruction(Opcodes.JGT).addUnsigned(val).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value is less than {@code value}. */ - public ApfGenerator addJumpIfR0LessThan(int value, String target) { - Instruction instruction = new Instruction(Opcodes.JLT); - instruction.addUnsignedImm(value); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0LessThan(int val, String tgt) { + return append(new Instruction(Opcodes.JLT).addUnsigned(val).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value has any bits set that are also set in {@code value}. */ - public ApfGenerator addJumpIfR0AnyBitsSet(int value, String target) { - Instruction instruction = new Instruction(Opcodes.JSET); - instruction.addUnsignedImm(value); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0AnyBitsSet(int val, String tgt) { + return append(new Instruction(Opcodes.JSET).addTwosCompUnsigned(val).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value equals register R1's value. */ - public ApfGenerator addJumpIfR0EqualsR1(String target) { - Instruction instruction = new Instruction(Opcodes.JEQ, Register.R1); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0EqualsR1(String tgt) { + return append(new Instruction(Opcodes.JEQ, R1).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value does not equal register R1's value. */ - public ApfGenerator addJumpIfR0NotEqualsR1(String target) { - Instruction instruction = new Instruction(Opcodes.JNE, Register.R1); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0NotEqualsR1(String tgt) { + return append(new Instruction(Opcodes.JNE, R1).setTargetLabel(tgt)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value is greater than register R1's value. */ - public ApfGenerator addJumpIfR0GreaterThanR1(String target) { - Instruction instruction = new Instruction(Opcodes.JGT, Register.R1); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0GreaterThanR1(String tgt) { + return append(new Instruction(Opcodes.JGT, R1).setTargetLabel(tgt)); } /** @@ -777,132 +917,104 @@ public class ApfGenerator { * value is less than register R1's value. */ public ApfGenerator addJumpIfR0LessThanR1(String target) { - Instruction instruction = new Instruction(Opcodes.JLT, Register.R1); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + return append(new Instruction(Opcodes.JLT, R1).setTargetLabel(target)); } /** * Add an instruction to the end of the program to jump to {@code target} if register R0's * value has any bits set that are also set in R1's value. */ - public ApfGenerator addJumpIfR0AnyBitsSetR1(String target) { - Instruction instruction = new Instruction(Opcodes.JSET, Register.R1); - instruction.setTargetLabel(target); - addInstruction(instruction); - return this; + public ApfGenerator addJumpIfR0AnyBitsSetR1(String tgt) { + return append(new Instruction(Opcodes.JSET, R1).setTargetLabel(tgt)); } /** - * Add an instruction to the end of the program to jump to {@code target} if the bytes of the - * packet at an offset specified by {@code register} don't match {@code bytes}, {@code register} - * must be R0. + * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the + * packet at an offset specified by {@code register} don't match {@code bytes} + * R=0 means check for not equal */ - public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target) + public ApfGenerator addJumpIfBytesAtR0NotEqual(byte[] bytes, String tgt) { + return append(new Instruction(Opcodes.JNEBS).addUnsigned( + bytes.length).setTargetLabel(tgt).setBytesImm(bytes)); + } + + /** + * Add an instruction to the end of the program to jump to {@code tgt} if the bytes of the + * packet at an offset specified by {@code register} match {@code bytes} + * R=1 means check for equal. + */ + public ApfGenerator addJumpIfBytesAtR0Equal(byte[] bytes, String tgt) throws IllegalInstructionException { - if (register == Register.R1) { - throw new IllegalInstructionException("JNEBS fails with R1"); - } - Instruction instruction = new Instruction(Opcodes.JNEBS, register); - instruction.addUnsignedImm(bytes.length); - instruction.setTargetLabel(target); - instruction.setBytesImm(bytes); - addInstruction(instruction); - return this; + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(Opcodes.JNEBS, R1).addUnsigned( + bytes.length).setTargetLabel(tgt).setBytesImm(bytes)); } /** * Add an instruction to the end of the program to load memory slot {@code slot} into * {@code register}. */ - public ApfGenerator addLoadFromMemory(Register register, int slot) + public ApfGenerator addLoadFromMemory(Register r, int slot) throws IllegalInstructionException { - if (slot < 0 || slot > (MEMORY_SLOTS - 1)) { - throw new IllegalInstructionException("illegal memory slot number: " + slot); - } - Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.addUnsignedImm(ExtendedOpcodes.LDM.value + slot); - addInstruction(instruction); - return this; + return append(new Instruction(ExtendedOpcodes.LDM, slot, r)); } /** * Add an instruction to the end of the program to store {@code register} into memory slot * {@code slot}. */ - public ApfGenerator addStoreToMemory(Register register, int slot) + public ApfGenerator addStoreToMemory(Register r, int slot) throws IllegalInstructionException { - if (slot < 0 || slot > (MEMORY_SLOTS - 1)) { - throw new IllegalInstructionException("illegal memory slot number: " + slot); - } - Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.addUnsignedImm(ExtendedOpcodes.STM.value + slot); - addInstruction(instruction); - return this; + return append(new Instruction(ExtendedOpcodes.STM, slot, r)); } /** * Add an instruction to the end of the program to logically not {@code register}. */ - public ApfGenerator addNot(Register register) { - Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.addUnsignedImm(ExtendedOpcodes.NOT.value); - addInstruction(instruction); - return this; + public ApfGenerator addNot(Register r) { + return append(new Instruction(ExtendedOpcodes.NOT, r)); } /** * Add an instruction to the end of the program to negate {@code register}. */ - public ApfGenerator addNeg(Register register) { - Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.addUnsignedImm(ExtendedOpcodes.NEG.value); - addInstruction(instruction); - return this; + public ApfGenerator addNeg(Register r) { + return append(new Instruction(ExtendedOpcodes.NEG, r)); } /** * Add an instruction to swap the values in register R0 and register R1. */ public ApfGenerator addSwap() { - Instruction instruction = new Instruction(Opcodes.EXT); - instruction.addUnsignedImm(ExtendedOpcodes.SWAP.value); - addInstruction(instruction); - return this; + return append(new Instruction(ExtendedOpcodes.SWAP)); } /** * Add an instruction to the end of the program to move the value into * {@code register} from the other register. */ - public ApfGenerator addMove(Register register) { - Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.addUnsignedImm(ExtendedOpcodes.MOVE.value); - addInstruction(instruction); - return this; + public ApfGenerator addMove(Register r) { + return append(new Instruction(ExtendedOpcodes.MOVE, r)); } /** * Add an instruction to the end of the program to let the program immediately return PASS. */ - public ApfGenerator addPass() throws IllegalInstructionException { - Instruction instruction = new Instruction(Opcodes.PASS, Register.R0); - addInstruction(instruction); - return this; + public ApfGenerator addPass() { + // PASS requires using R0 because it shares opcode with DROP + return append(new Instruction(Opcodes.PASS)); } /** * Add an instruction to the end of the program to increment the counter value and * immediately return PASS. */ - public ApfGenerator addCountAndPass(int counterNumber) throws IllegalInstructionException { + public ApfGenerator addCountAndPass(int cnt) throws IllegalInstructionException { requireApfVersion(MIN_APF_VERSION_IN_DEV); - checkCounterNumber(counterNumber); - Instruction instruction = new Instruction(Opcodes.PASS, Register.R0); - instruction.addUnsignedImm(counterNumber); - addInstruction(instruction); - return this; + checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */, + 1000 /* upperBound */); + // PASS requires using R0 because it shares opcode with DROP + return append(new Instruction(Opcodes.PASS).addUnsigned(cnt)); } /** @@ -910,35 +1022,52 @@ public class ApfGenerator { */ public ApfGenerator addDrop() throws IllegalInstructionException { requireApfVersion(MIN_APF_VERSION_IN_DEV); - Instruction instruction = new Instruction(Opcodes.DROP, Register.R1); - addInstruction(instruction); - return this; + // DROP requires using R1 because it shares opcode with PASS + return append(new Instruction(Opcodes.DROP, R1)); } /** * Add an instruction to the end of the program to increment the counter value and * immediately return DROP. */ - public ApfGenerator addCountAndDrop(int counterNumber) throws IllegalInstructionException { + public ApfGenerator addCountAndDrop(int cnt) throws IllegalInstructionException { requireApfVersion(MIN_APF_VERSION_IN_DEV); - checkCounterNumber(counterNumber); - Instruction instruction = new Instruction(Opcodes.DROP, Register.R1); - instruction.addUnsignedImm(counterNumber); - addInstruction(instruction); - return this; + checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */, + 1000 /* upperBound */); + // DROP requires using R1 because it shares opcode with PASS + return append(new Instruction(Opcodes.DROP, R1).addUnsigned(cnt)); + } + + /** + * Add an instruction to the end of the program to call the apf_allocate_buffer() function. + * Buffer length to be allocated is stored in register 0. + */ + public ApfGenerator addAllocateR0() throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.ALLOCATE)); } /** * Add an instruction to the end of the program to call the apf_allocate_buffer() function. * - * @param register the register value contains the buffer size. + * @param size the buffer length to be allocated. */ - public ApfGenerator addAlloc(Register register) throws IllegalInstructionException { - requireApfVersion(5); - Instruction instruction = new Instruction(Opcodes.EXT, register); - instruction.addUnsignedImm(ExtendedOpcodes.ALLOC.value); - addInstruction(instruction); - return this; + public ApfGenerator addAllocate(int size) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + // R1 means the extra be16 immediate is present + return append(new Instruction(ExtendedOpcodes.ALLOCATE, R1).addU16(size)); + } + + /** + * Add an instruction to the beginning of the program to reserve the data region. + * @param data the actual data byte + */ + public ApfGenerator addData(byte[] data) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + if (!mInstructions.isEmpty()) { + throw new IllegalInstructionException("data instruction has to come first"); + } + return append(new Instruction(Opcodes.JMP, R1).addUnsigned(data.length).setBytesImm(data)); } /** @@ -946,10 +1075,8 @@ public class ApfGenerator { */ public ApfGenerator addTransmit() throws IllegalInstructionException { requireApfVersion(MIN_APF_VERSION_IN_DEV); - Instruction instruction = new Instruction(Opcodes.EXT, Register.R0); - instruction.addUnsignedImm(ExtendedOpcodes.TRANSMIT.value); - addInstruction(instruction); - return this; + // TRANSMIT requires using R0 because it shares opcode with DISCARD + return append(new Instruction(ExtendedOpcodes.TRANSMIT)); } /** @@ -957,213 +1084,270 @@ public class ApfGenerator { */ public ApfGenerator addDiscard() throws IllegalInstructionException { requireApfVersion(MIN_APF_VERSION_IN_DEV); - Instruction instruction = new Instruction(Opcodes.EXT, Register.R1); - instruction.addUnsignedImm(ExtendedOpcodes.DISCARD.value); - addInstruction(instruction); - return this; + // DISCARD requires using R1 because it shares opcode with TRANSMIT + return append(new Instruction(ExtendedOpcodes.DISCARD, R1)); } - // TODO: add back when support WRITE opcode -// /** -// * Add an instruction to the end of the program to write 1, 2 or 4 bytes value to output -// buffer. -// * -// * @param value the value to write -// * @param size the size of the value -// * @return the ApfGenerator object -// * @throws IllegalInstructionException throws when size is not 1, 2 or 4 -// */ -// public ApfGenerator addWrite(int value, byte size) throws IllegalInstructionException { -// requireApfVersion(5); -// if (!(size == 1 || size == 2 || size == 4)) { -// throw new IllegalInstructionException("length field must be 1, 2 or 4"); -// } -// if (size < calculateImmSize(value, false)) { -// throw new IllegalInstructionException( -// String.format("the value %d is unfit into size: %d", value, size)); -// } -// Instruction instruction = new Instruction(Opcodes.WRITE); -// instruction.addUnsignedImm(value, size); -// addInstruction(instruction); -// return this; -// } - - // TODO: add back when support EWRITE opcode -// /** -// * Add an instruction to the end of the program to write 1, 2 or 4 bytes value from register -// * to output buffer. -// * -// * @param register the register contains the value to be written -// * @param size the size of the value -// * @return the ApfGenerator object -// * @throws IllegalInstructionException throws when size is not 1, 2 or 4 -// */ -// public ApfGenerator addWrite(Register register, byte size) -// throws IllegalInstructionException { -// requireApfVersion(5); -// if (!(size == 1 || size == 2 || size == 4)) { -// throw new IllegalInstructionException( -// "length field must be 1, 2 or 4"); -// } -// Instruction instruction = new Instruction(Opcodes.EXT, register); -// if (size == 1) { -// instruction.addUnsignedImm(ExtendedOpcodes.EWRITE1.value); -// } else if (size == 2) { -// instruction.addUnsignedImm(ExtendedOpcodes.EWRITE2.value); -// } else { -// instruction.addUnsignedImm(ExtendedOpcodes.EWRITE4.value); -// } -// addInstruction(instruction); -// return this; -// } - - // TODO: add back when support PKTCOPY/DATACOPY opcode -// /** -// * Add an instruction to the end of the program to copy data from APF data region to output -// * buffer. -// * -// * @param srcOffset the offset inside the APF data region for where to start copy -// * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at -// * one time. -// * @return the ApfGenerator object -// * @throws IllegalInstructionException throws when imm size is incorrectly set. -// */ -// public ApfGenerator addDataCopy(int srcOffset, int length) -// throws IllegalInstructionException { -// return addMemCopy(srcOffset, length, Register.R1); -// } -// -// /** -// * Add an instruction to the end of the program to copy data from input packet to output -// buffer. -// * -// * @param srcOffset the offset inside the input packet for where to start copy -// * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at -// * one time. -// * @return the ApfGenerator object -// * @throws IllegalInstructionException throws when imm size is incorrectly set. -// */ -// public ApfGenerator addPacketCopy(int srcOffset, int length) -// throws IllegalInstructionException { -// return addMemCopy(srcOffset, length, Register.R0); -// } -// -// private ApfGenerator addMemCopy(int srcOffset, int length, Register register) -// throws IllegalInstructionException { -// requireApfVersion(5); -// checkCopyLength(length); -// checkCopyOffset(srcOffset); -// Instruction instruction = new Instruction(Opcodes.MEMCOPY, register); -// // if the offset == 0, it should still be encoded with 1 byte size. -// if (srcOffset == 0) { -// instruction.addUnsignedImm(srcOffset, (byte) 1 /* size */); -// } else { -// instruction.addUnsignedImm(srcOffset); -// } -// instruction.addUnsignedImm(length, (byte) 1 /* size */); -// addInstruction(instruction); -// return this; -// } -// TODO: add back when support EPKTCOPY/EDATACOPY opcode -// /** -// * Add an instruction to the end of the program to copy data from APF data region to output -// * buffer. -// * -// * @param register the register that stored the base offset value. -// * @param relativeOffset the offset inside the APF data region for where to start copy -// * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at -// * one time. -// * @return the ApfGenerator object -// * @throws IllegalInstructionException throws when imm size is incorrectly set. -// */ -// public ApfGenerator addDataCopy(Register register, int relativeOffset, int length) -// throws IllegalInstructionException { -// return addMemcopy(register, relativeOffset, length, ExtendedOpcodes.EDATACOPY.value); -// } -// -// /** -// * Add an instruction to the end of the program to copy data from input packet to output -// buffer. -// * -// * @param register the register that stored the base offset value. -// * @param relativeOffset the offset inside the input packet for where to start copy -// * @param length the length of bytes needed to be copied, only <= 255 bytes can be copied at -// * one time. -// * @return the ApfGenerator object -// * @throws IllegalInstructionException throws when imm size is incorrectly set. -// */ -// public ApfGenerator addPacketCopy(Register register, int relativeOffset, int length) -// throws IllegalInstructionException { -// return addMemcopy(register, relativeOffset, length, ExtendedOpcodes.EPKTCOPY.value); -// } -// -// private ApfGenerator addMemcopy(Register register, int relativeOffset, int length, int opcode) -// throws IllegalInstructionException { -// requireApfVersion(5); -// checkCopyLength(length); -// checkCopyOffset(relativeOffset); -// Instruction instruction = new Instruction(Opcodes.EXT, register); -// instruction.addUnsignedImm(opcode); -// // if the offset == 0, it should still be encoded with 1 byte size. -// if (relativeOffset == 0) { -// instruction.addUnsignedImm(relativeOffset, (byte) 1 /* size */); -// } else { -// instruction.addUnsignedImm(relativeOffset); -// } -// instruction.addUnsignedImm(length, (byte) 1 /* size */); -// addInstruction(instruction); -// return this; -// } - - private void checkCopyLength(int length) { - if (length < 0 || length > 255) { - throw new IllegalArgumentException( - "copy length must between 0 to 255, length: " + length); - } + /** + * Add an instruction to the end of the program to write 1 byte value to output buffer. + */ + public ApfGenerator addWriteU8(int val) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(Opcodes.WRITE).overrideLenField(1).addU8(val)); + } + + /** + * Add an instruction to the end of the program to write 2 bytes value to output buffer. + */ + public ApfGenerator addWriteU16(int val) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(Opcodes.WRITE).overrideLenField(2).addU16(val)); + } + + /** + * Add an instruction to the end of the program to write 4 bytes value to output buffer. + */ + public ApfGenerator addWriteU32(long val) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(Opcodes.WRITE).overrideLenField(4).addU32(val)); + } + + /** + * Add an instruction to the end of the program to write 1 byte value from register to output + * buffer. + */ + public ApfGenerator addWriteU8(Register reg) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EWRITE1, reg)); + } + + /** + * Add an instruction to the end of the program to write 2 byte value from register to output + * buffer. + */ + public ApfGenerator addWriteU16(Register reg) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EWRITE2, reg)); + } + + /** + * Add an instruction to the end of the program to write 4 byte value from register to output + * buffer. + */ + public ApfGenerator addWriteU32(Register reg) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EWRITE4, reg)); + } + + /** + * Add an instruction to the end of the program to copy data from APF program/data region to + * output buffer and auto-increment the output buffer pointer. + * + * @param src the offset inside the APF program/data region for where to start copy. + * @param len the length of bytes needed to be copied, only <= 255 bytes can be copied at + * one time. + * @return the ApfGenerator object + */ + public ApfGenerator addDataCopy(int src, int len) + throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(Opcodes.PKTDATACOPY, R1).addUnsigned(src).addU8(len)); + } + + /** + * Add an instruction to the end of the program to copy data from input packet to output + * buffer and auto-increment the output buffer pointer. + * + * @param src the offset inside the input packet for where to start copy. + * @param len the length of bytes needed to be copied, only <= 255 bytes can be copied at + * one time. + * @return the ApfGenerator object + */ + public ApfGenerator addPacketCopy(int src, int len) + throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(Opcodes.PKTDATACOPY, R0).addUnsigned(src).addU8(len)); } - private void checkCopyOffset(int offset) { - if (offset < 0) { - throw new IllegalArgumentException( - "offset must be non less than zero, offset: " + offset); + /** + * Add an instruction to the end of the program to copy data from APF program/data region to + * output buffer and auto-increment the output buffer pointer. + * Source offset is stored in R0. + * + * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once. + * @return the ApfGenerator object + */ + public ApfGenerator addDataCopyFromR0(int len) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EDATACOPY).addU8(len)); + } + + /** + * Add an instruction to the end of the program to copy data from input packet to output + * buffer and auto-increment the output buffer pointer. + * Source offset is stored in R0. + * + * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once. + * @return the ApfGenerator object + */ + public ApfGenerator addPacketCopyFromR0(int len) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EPKTCOPY).addU8(len)); + } + + /** + * Add an instruction to the end of the program to copy data from APF program/data region to + * output buffer and auto-increment the output buffer pointer. + * Source offset is stored in R0. + * Copy length is stored in R1. + * + * @return the ApfGenerator object + */ + public ApfGenerator addDataCopyFromR0LenR1() throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EDATACOPY, R1)); + } + + /** + * Add an instruction to the end of the program to copy data from input packet to output + * buffer and auto-increment the output buffer pointer. + * Source offset is stored in R0. + * Copy length is stored in R1. + * + * @return the ApfGenerator object + */ + public ApfGenerator addPacketCopyFromR0LenR1() throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + return append(new Instruction(ExtendedOpcodes.EPKTCOPY, R1)); + } + + /** + * Check if the byte is valid dns character: A-Z,0-9,-,_ + */ + private static boolean isValidDnsCharacter(byte c) { + return (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_'; + } + + private static void validateNames(@NonNull byte[] names) { + final int len = names.length; + if (len < 4) { + throw new IllegalArgumentException("qnames must have at least length 4"); } + final String errorMessage = "qname: " + HexDump.toHexString(names) + + "is not null-terminated list of TLV-encoded names"; + int i = 0; + while (i < len - 1) { + int label_len = names[i++]; + if (label_len < 1 || label_len > 63) { + throw new IllegalArgumentException( + "label len: " + label_len + " must be between 1 and 63"); + } + if (i + label_len >= len - 1) { + throw new IllegalArgumentException(errorMessage); + } + while (label_len-- > 0) { + if (!isValidDnsCharacter(names[i++])) { + throw new IllegalArgumentException("qname: " + HexDump.toHexString(names) + + " contains invalid character"); + } + } + if (names[i] == 0) { + i++; // skip null terminator. + } + } + if (names[len - 1] != 0) { + throw new IllegalArgumentException(errorMessage); + } + } + + /** + * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP + * payload's DNS questions do NOT contain the QNAME specified in {@code qnames} and qtype + * equals {@code qtype}. Examines the payload starting at the offset in R0. + * R = 0 means check for "does not contain". + */ + public ApfGenerator addJumpIfPktAtR0DoesNotContainDnsQ(@NonNull byte[] qnames, int qtype, + @NonNull String tgt) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + validateNames(qnames); + return append(new Instruction(ExtendedOpcodes.JDNSQMATCH).setTargetLabel(tgt).addU8( + qtype).setBytesImm(qnames)); + } + + /** + * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP + * payload's DNS questions contain the QNAME specified in {@code qnames} and qtype + * equals {@code qtype}. Examines the payload starting at the offset in R0. + * R = 1 means check for "contain". + */ + public ApfGenerator addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, int qtype, + @NonNull String tgt) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + validateNames(qnames); + return append(new Instruction(ExtendedOpcodes.JDNSQMATCH, R1).setTargetLabel(tgt).addU8( + qtype).setBytesImm(qnames)); + } + + /** + * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP + * payload's DNS answers/authority/additional records do NOT contain the NAME + * specified in {@code Names}. Examines the payload starting at the offset in R0. + * R = 0 means check for "does not contain". + */ + public ApfGenerator addJumpIfPktAtR0DoesNotContainDnsA(@NonNull byte[] names, + @NonNull String tgt) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + validateNames(names); + return append(new Instruction(ExtendedOpcodes.JDNSAMATCH).setTargetLabel(tgt).setBytesImm( + names)); } - private void checkCounterNumber(int counterNumber) { - if (counterNumber < 1 || counterNumber > 1000) { - throw new IllegalArgumentException( - "Counter number must be in range (0, 1000], counterNumber: " + counterNumber); + /** + * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP + * payload's DNS answers/authority/additional records contain the NAME + * specified in {@code Names}. Examines the payload starting at the offset in R0. + * R = 1 means check for "contain". + */ + public ApfGenerator addJumpIfPktAtR0ContainDnsA(@NonNull byte[] names, + @NonNull String tgt) throws IllegalInstructionException { + requireApfVersion(MIN_APF_VERSION_IN_DEV); + validateNames(names); + return append(new Instruction(ExtendedOpcodes.JDNSAMATCH, R1).setTargetLabel( + tgt).setBytesImm(names)); + } + + private static void checkRange(@NonNull String variableName, long value, long lowerBound, + long upperBound) { + if (value >= lowerBound && value <= upperBound) { + return; } + throw new IllegalArgumentException( + String.format("%s: %d, must be in range [%d, %d]", variableName, value, lowerBound, + upperBound)); } /** * Add an instruction to the end of the program to load 32 bits from the data memory into * {@code register}. The source address is computed by adding the signed immediate * @{code offset} to the other register. - * Requires APF v3 or greater. + * Requires APF v4 or greater. */ - public ApfGenerator addLoadData(Register destinationRegister, int offset) + public ApfGenerator addLoadData(Register dst, int ofs) throws IllegalInstructionException { - requireApfVersion(3); - Instruction instruction = new Instruction(Opcodes.LDDW, destinationRegister); - instruction.addSignedImm(offset); - addInstruction(instruction); - return this; + requireApfVersion(APF_VERSION_4); + return append(new Instruction(Opcodes.LDDW, dst).addSigned(ofs)); } /** * Add an instruction to the end of the program to store 32 bits from {@code register} into the * data memory. The destination address is computed by adding the signed immediate * @{code offset} to the other register. - * Requires APF v3 or greater. + * Requires APF v4 or greater. */ - public ApfGenerator addStoreData(Register sourceRegister, int offset) + public ApfGenerator addStoreData(Register src, int ofs) throws IllegalInstructionException { - requireApfVersion(3); - Instruction instruction = new Instruction(Opcodes.STDW, sourceRegister); - instruction.addSignedImm(offset); - addInstruction(instruction); - return this; + requireApfVersion(APF_VERSION_4); + return append(new Instruction(Opcodes.STDW, src).addSigned(ofs)); } /** @@ -1182,7 +1366,7 @@ public class ApfGenerator { /** * Calculate the size of the imm. */ - private static byte calculateImmSize(int imm, boolean signed) { + private static int calculateImmSize(int imm, boolean signed) { if (imm == 0) { return 0; } diff --git a/src/android/net/apf/DnsUtils.java b/src/android/net/apf/DnsUtils.java index 0300d349..5bd2515b 100644 --- a/src/android/net/apf/DnsUtils.java +++ b/src/android/net/apf/DnsUtils.java @@ -301,7 +301,7 @@ public class DnsUtils { gen.addJumpIfR0NotEquals(label.length(), noMatchLabel); gen.addLoadFromMemory(R0, SLOT_CURRENT_PARSE_OFFSET); gen.addAdd(1); - gen.addJumpIfBytesNotEqual(R0, label.getBytes(), noMatchLabel); + gen.addJumpIfBytesAtR0NotEqual(label.getBytes(), noMatchLabel); // Prep offset of next label. gen.addAdd(label.length()); diff --git a/src/android/net/apf/LegacyApfFilter.java b/src/android/net/apf/LegacyApfFilter.java index cc09856d..6b93d89f 100644 --- a/src/android/net/apf/LegacyApfFilter.java +++ b/src/android/net/apf/LegacyApfFilter.java @@ -982,7 +982,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Generate code to match the packet bytes. if (section.type == PacketSection.Type.MATCH) { gen.addLoadImmediate(Register.R0, section.start); - gen.addJumpIfBytesNotEqual(Register.R0, + gen.addJumpIfBytesAtR0NotEqual( Arrays.copyOfRange(mPacket.array(), section.start, section.start + section.length), nextFilterLabel); @@ -1065,7 +1065,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { final String nextFilterLabel = "natt_keepalive_filter" + getUniqueNumberLocked(); gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel); // A NAT-T keepalive packet contains 1 byte payload with the value 0xff // Check payload length is 1 @@ -1080,11 +1080,11 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Check that the ports match gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); gen.addAdd(ETH_HEADER_LEN); - gen.addJumpIfBytesNotEqual(Register.R0, mPortFingerprint, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mPortFingerprint, nextFilterLabel); // Payload offset = R0 + UDP header length gen.addAdd(UDP_HEADER_LEN); - gen.addJumpIfBytesNotEqual(Register.R0, mPayload, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mPayload, nextFilterLabel); maybeSetupCounter(gen, Counter.DROPPED_IPV4_NATT_KEEPALIVE); gen.addJump(mCountAndDropLabel); @@ -1180,7 +1180,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { final String nextFilterLabel = "keepalive_ack" + getUniqueNumberLocked(); gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN + IPV4_SRC_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, mSrcDstAddr, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mSrcDstAddr, nextFilterLabel); // Skip to the next filter if it's not zero-sized : // TCP_HEADER_SIZE + IPV4_HEADER_SIZE - ipv4_total_length == 0 @@ -1202,7 +1202,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); gen.addLoadImmediate(Register.R0, ETH_HEADER_LEN); gen.addAddR1(); - gen.addJumpIfBytesNotEqual(Register.R0, mPortSeqAckFingerprint, nextFilterLabel); + gen.addJumpIfBytesAtR0NotEqual(mPortSeqAckFingerprint, nextFilterLabel); maybeSetupCounter(gen, Counter.DROPPED_IPV4_KEEPALIVE_ACK); gen.addJump(mCountAndDropLabel); @@ -1316,7 +1316,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Pass if not ARP IPv4. gen.addLoadImmediate(Register.R0, ARP_HEADER_OFFSET); maybeSetupCounter(gen, Counter.PASSED_ARP_NON_IPV4); - gen.addJumpIfBytesNotEqual(Register.R0, ARP_IPV4_HEADER, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ARP_IPV4_HEADER, mCountAndPassLabel); // Pass if unknown ARP opcode. gen.addLoad16(Register.R0, ARP_OPCODE_OFFSET); @@ -1332,7 +1332,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Pass if unicast reply. gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); maybeSetupCounter(gen, Counter.PASSED_ARP_UNICAST_REPLY); - gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel); // Either a unicast request, a unicast reply, or a broadcast reply. gen.defineLabel(checkTargetIPv4); @@ -1346,7 +1346,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // and broadcast replies with a different target IPv4 address. gen.addLoadImmediate(Register.R0, ARP_TARGET_IP_ADDRESS_OFFSET); maybeSetupCounter(gen, Counter.DROPPED_ARP_OTHER_HOST); - gen.addJumpIfBytesNotEqual(Register.R0, mIPv4Address, mCountAndDropLabel); + gen.addJumpIfBytesAtR0NotEqual(mIPv4Address, mCountAndDropLabel); } maybeSetupCounter(gen, Counter.PASSED_ARP); @@ -1394,7 +1394,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET); // NOTE: Relies on R1 containing IPv4 header offset. gen.addAddR1(); - gen.addJumpIfBytesNotEqual(Register.R0, mHardwareAddress, skipDhcpv4Filter); + gen.addJumpIfBytesAtR0NotEqual(mHardwareAddress, skipDhcpv4Filter); maybeSetupCounter(gen, Counter.PASSED_DHCP); gen.addJump(mCountAndPassLabel); @@ -1428,7 +1428,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // TODO: can we invert this condition to fall through to the common pass case below? maybeSetupCounter(gen, Counter.PASSED_IPV4_UNICAST); gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel); maybeSetupCounter(gen, Counter.DROPPED_IPV4_L2_BROADCAST); gen.addJump(mCountAndDropLabel); } else { @@ -1555,8 +1555,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // TODO: Drop only if they don't contain the address of on-link neighbours. final byte[] unsolicitedNaDropPrefix = Arrays.copyOf(IPV6_ALL_NODES_ADDRESS, 15); gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, unsolicitedNaDropPrefix, - skipUnsolicitedMulticastNALabel); + gen.addJumpIfBytesAtR0NotEqual(unsolicitedNaDropPrefix, skipUnsolicitedMulticastNALabel); maybeSetupCounter(gen, Counter.DROPPED_IPV6_MULTICAST_NA); gen.addJump(mCountAndDropLabel); @@ -1613,7 +1612,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Check it's L2 mDNS multicast address. gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); - gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, + gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V4_MAC_ADDRESS, skipMdnsv4Filter); // Checks it's IPv4. @@ -1631,8 +1630,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Checks it's L2 mDNS multicast address. // Relies on R0 containing the ethernet destination mac address offset. - gen.addJumpIfBytesNotEqual(Register.R0, ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, - skipMdnsFilter); + gen.addJumpIfBytesAtR0NotEqual(ETH_MULTICAST_MDNS_V6_MAC_ADDRESS, skipMdnsFilter); // Checks it's IPv6. gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET); @@ -1663,7 +1661,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { for (int i = 0; i < mMdnsAllowList.size(); ++i) { final String mDnsNextAllowedQnameCheck = "mdns_next_allowed_qname_check" + i; final byte[] encodedQname = encodeQname(mMdnsAllowList.get(i)); - gen.addJumpIfBytesNotEqual(Register.R0, encodedQname, mDnsNextAllowedQnameCheck); + gen.addJumpIfBytesAtR0NotEqual(encodedQname, mDnsNextAllowedQnameCheck); // QNAME matched gen.addJump(mDnsAcceptPacket); // QNAME not matched @@ -1777,7 +1775,7 @@ public class LegacyApfFilter implements AndroidPacketFilter { // Drop non-IP non-ARP broadcasts, pass the rest gen.addLoadImmediate(Register.R0, ETH_DEST_ADDR_OFFSET); maybeSetupCounter(gen, Counter.PASSED_NON_IP_UNICAST); - gen.addJumpIfBytesNotEqual(Register.R0, ETHER_BROADCAST, mCountAndPassLabel); + gen.addJumpIfBytesAtR0NotEqual(ETHER_BROADCAST, mCountAndPassLabel); maybeSetupCounter(gen, Counter.DROPPED_ETH_BROADCAST); gen.addJump(mCountAndDropLabel); diff --git a/src/android/net/dhcp6/Dhcp6Client.java b/src/android/net/dhcp6/Dhcp6Client.java index 291a97a0..8d53048f 100644 --- a/src/android/net/dhcp6/Dhcp6Client.java +++ b/src/android/net/dhcp6/Dhcp6Client.java @@ -20,9 +20,7 @@ import static android.net.dhcp6.Dhcp6Packet.IAID; import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation; import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_INET6; -import static android.system.OsConstants.IFA_F_NODAD; import static android.system.OsConstants.IPPROTO_UDP; -import static android.system.OsConstants.RT_SCOPE_UNIVERSE; import static android.system.OsConstants.SOCK_DGRAM; import static android.system.OsConstants.SOCK_NONBLOCK; @@ -31,14 +29,8 @@ import static com.android.net.module.util.NetworkStackConstants.DHCP6_CLIENT_POR import static com.android.net.module.util.NetworkStackConstants.DHCP6_SERVER_PORT; import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ANY; import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH; -import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR; -import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE; -import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64; -import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64; import android.content.Context; -import android.net.IpPrefix; -import android.net.LinkAddress; import android.net.ip.IpClient; import android.net.util.SocketUtils; import android.os.Handler; @@ -58,12 +50,10 @@ import com.android.internal.util.WakeupMessage; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.PacketReader; -import com.android.net.module.util.netlink.NetlinkUtils; import com.android.net.module.util.structs.IaPrefixOption; import java.io.FileDescriptor; import java.io.IOException; -import java.net.Inet6Address; import java.net.SocketException; import java.nio.ByteBuffer; import java.util.Collections; @@ -95,8 +85,6 @@ public class Dhcp6Client extends StateMachine { // Message.arg1 arguments to CMD_DHCP6_RESULT notification public static final int DHCP6_PD_SUCCESS = 1; public static final int DHCP6_PD_PREFIX_EXPIRED = 2; - public static final int DHCP6_PD_PREFIX_CHANGED = 3; - public static final int DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED = 4; // Notification from DHCPv6 state machine before quitting public static final int CMD_ON_QUIT = PUBLIC_BASE + 4; @@ -436,8 +424,8 @@ public class Dhcp6Client extends StateMachine { Log.d(TAG, "Scheduling IA_PD expiry in " + expirationTimeout + "s"); } - private void notifyPrefixDelegation(int result, @Nullable final PrefixDelegation pd) { - mController.sendMessage(CMD_DHCP6_RESULT, result, 0, pd); + private void notifyPrefixDelegation(int result, @Nullable final List<IaPrefixOption> ipos) { + mController.sendMessage(CMD_DHCP6_RESULT, result, 0, ipos); } private void clearDhcp6State() { @@ -557,10 +545,15 @@ public class Dhcp6Client extends StateMachine { return sendSolicitPacket(transId, elapsedTimeMs, pd.build()); } - // TODO: support multiple prefixes. @Override protected void receivePacket(Dhcp6Packet packet) { final PrefixDelegation pd = packet.mPrefixDelegation; + // Ignore any Advertise or Reply for Solicit(with Rapid Commit) with NoPrefixAvail + // status code, retransmit Solicit to see if any valid response from other Servers. + if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) { + Log.w(TAG, "Server responded to Solicit without available prefix, ignoring"); + return; + } if (packet instanceof Dhcp6AdvertisePacket) { Log.d(TAG, "Get prefix delegation option from Advertise: " + pd); mAdvertise = pd; @@ -601,6 +594,11 @@ public class Dhcp6Client extends StateMachine { protected void receivePacket(Dhcp6Packet packet) { if (!(packet instanceof Dhcp6ReplyPacket)) return; final PrefixDelegation pd = packet.mPrefixDelegation; + if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) { + Log.w(TAG, "Server responded to Request without available prefix, restart Solicit"); + transitionTo(mSolicitState); + return; + } Log.d(TAG, "Get prefix delegation option from Reply: " + pd); mReply = pd; mSolMaxRtMs = packet.getSolMaxRtMs().orElse(mSolMaxRtMs); @@ -621,7 +619,7 @@ public class Dhcp6Client extends StateMachine { public boolean processMessage(Message message) { switch (message.what) { case CMD_DHCP6_PD_EXPIRE: - notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, null); + notifyPrefixDelegation(DHCP6_PD_PREFIX_EXPIRED, mReply.getValidIaPrefixes()); transitionTo(mSolicitState); return HANDLED; default: @@ -639,33 +637,6 @@ public class Dhcp6Client extends StateMachine { } } - // Create an IPv6 address from the interface mac address with IFA_F_MANAGETEMPADDR - // flag, kernel will create another privacy IPv6 address on behalf of user space. - // We don't need to remember IPv6 addresses that need to extend the lifetime every - // time it enters BoundState. - private boolean addInterfaceAddress(@NonNull final Inet6Address address, - @NonNull final IaPrefixOption ipo) { - final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD; - final long now = SystemClock.elapsedRealtime(); - final long deprecationTime = now + ipo.preferred; - final long expirationTime = now + ipo.valid; - final LinkAddress la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags, - RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime); - if (!la.isGlobalPreferred()) { - Log.e(TAG, la + " is not a global preferred IPv6 address"); - return false; - } - if (!NetlinkUtils.sendRtmNewAddressRequest(mIface.index, address, - (short) RFC7421_PREFIX_LENGTH, - flags, (byte) RT_SCOPE_UNIVERSE /* scope */, - ipo.preferred, ipo.valid)) { - Log.e(TAG, "Failed to set IPv6 address " + address.getHostAddress() - + "%" + mIface.index); - return false; - } - return true; - } - /** * Client has already obtained the lease(e.g. IA_PD option) from server and stays in Bound * state until T1 expires, and then transition to Renew state to extend the lease duration. @@ -675,26 +646,9 @@ public class Dhcp6Client extends StateMachine { public void enter() { super.enter(); scheduleLeaseTimers(); - - // TODO: roll back to SOLICIT state after a delay if something wrong happens - // instead of returning directly. - for (IaPrefixOption ipo : mReply.getValidIaPrefixes()) { - // TODO: The prefix with preferred/valid lifetime of 0 is valid, but client - // should stop using the prefix immediately. Actually kernel doesn't accept - // the address with valid lifetime of 0 and returns EINVAL when it sees that. - // We should send RTM_DELADDR netlink message to kernel to delete these addresses - // from the interface if any. - // Configure IPv6 addresses based on the delegated prefix(es) on the interface. - // We've checked that delegated prefix is valid upon receiving the response from - // DHCPv6 server, and the server may assign a prefix with length less than 64. So - // for SLAAC use case we always set the prefix length to 64 even if the delegated - // prefix length is less than 64. - final IpPrefix prefix = ipo.getIpPrefix(); - final Inet6Address address = createInet6AddressFromEui64(prefix, - macAddressToEui64(mIface.macAddr)); - if (!addInterfaceAddress(address, ipo)) continue; - } - notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply); + // Pass valid delegated prefix(es) to IpClient for IPv6 address configuration and + // active prefix(es) maintenance. + notifyPrefixDelegation(DHCP6_PD_SUCCESS, mReply.getValidIaPrefixes()); } @Override @@ -723,8 +677,8 @@ public class Dhcp6Client extends StateMachine { * That forces previous delegated prefixes to expire in a natural way, and client should * also stop trying to extend the lifetime for them. That being said, the global IPv6 address * lifetime won't be updated in BoundState if corresponding prefix doesn't appear in Reply - * message, resulting in these global IPv6 addresses eventually and IpClient obtains these - * updates via netlink message and remove the delegated prefix(es) from LinkProperties. + * message, resulting in these global IPv6 addresses expire eventually and IpClient obtains + * these updates via netlink message and remove the delegated prefix(es) from LinkProperties. * - If some binding IA_PDs were absent in Reply message, client should still stay at RenewState * or RebindState and retransmit Renew/Rebind messages to see if it can get all later. So far * we only support one IA_PD option per interface, if the received Reply message doesn't take @@ -744,9 +698,15 @@ public class Dhcp6Client extends StateMachine { @Override protected void receivePacket(Dhcp6Packet packet) { if (!(packet instanceof Dhcp6ReplyPacket)) return; + final PrefixDelegation pd = packet.mPrefixDelegation; + // Stay at Renew/Rebind state if the Reply message takes NoPrefixAvail status code, + // retransmit Renew/Rebind message to server, to retry obtaining the prefixes. + if (pd.statusCode == Dhcp6Packet.STATUS_NO_PREFIX_AVAIL) { + Log.w(TAG, "Server responded to Renew/Rebind without available prefix, ignoring"); + return; + } // TODO: send a Request message to the server that responded if any of the IA_PDs in // Reply message contain NoBinding status code. - final PrefixDelegation pd = packet.mPrefixDelegation; Log.d(TAG, "Get prefix delegation option from Reply as response to Renew/Rebind " + pd); if (pd.ipos.isEmpty()) return; mReply = pd; diff --git a/src/android/net/dhcp6/Dhcp6Packet.java b/src/android/net/dhcp6/Dhcp6Packet.java index 4ef3195d..53dd274d 100644 --- a/src/android/net/dhcp6/Dhcp6Packet.java +++ b/src/android/net/dhcp6/Dhcp6Packet.java @@ -32,7 +32,6 @@ import com.android.net.module.util.structs.IaPrefixOption; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.nio.ByteOrder; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -95,16 +94,16 @@ public class Dhcp6Packet { * DHCPv6 Optional Type: Status Code. */ public static final byte DHCP6_STATUS_CODE = 13; + private static final byte MIN_STATUS_CODE_OPT_LEN = 6; protected short mStatusCode; - protected String mStatusMsg; public static final short STATUS_SUCCESS = 0; public static final short STATUS_UNSPEC_FAIL = 1; - public static final short STATUS_NO_ADDR_AVAI = 2; + public static final short STATUS_NO_ADDRS_AVAIL = 2; public static final short STATUS_NO_BINDING = 3; - public static final short STATUS_PREFIX_NOT_ONLINK = 4; + public static final short STATUS_NOT_ONLINK = 4; public static final short STATUS_USE_MULTICAST = 5; - public static final short STATUS_NO_PREFIX_AVAI = 6; + public static final short STATUS_NO_PREFIX_AVAIL = 6; /** * DHCPv6 zero-length Optional Type: Rapid Commit. Per RFC4039, both DHCPDISCOVER and DHCPACK @@ -209,14 +208,22 @@ public class Dhcp6Packet { public final int t2; @NonNull public final List<IaPrefixOption> ipos; + public final short statusCode; + @VisibleForTesting public PrefixDelegation(int iaid, int t1, int t2, - @NonNull final List<IaPrefixOption> ipos) { + @NonNull final List<IaPrefixOption> ipos, short statusCode) { Objects.requireNonNull(ipos); this.iaid = iaid; this.t1 = t1; this.t2 = t2; this.ipos = ipos; + this.statusCode = statusCode; + } + + public PrefixDelegation(int iaid, int t1, int t2, + @NonNull final List<IaPrefixOption> ipos) { + this(iaid, t1, t2, ipos, STATUS_SUCCESS /* statusCode */); } /** @@ -251,6 +258,7 @@ public class Dhcp6Packet { final int t1 = buffer.getInt(); final int t2 = buffer.getInt(); final List<IaPrefixOption> ipos = new ArrayList<IaPrefixOption>(); + short statusCode = STATUS_SUCCESS; while (buffer.remaining() > 0) { final int original = buffer.position(); final short optionType = buffer.getShort(); @@ -262,12 +270,18 @@ public class Dhcp6Packet { Log.d(TAG, "IA Prefix Option: " + ipo); ipos.add(ipo); break; - // TODO: support DHCP6_STATUS_CODE option + case DHCP6_STATUS_CODE: + statusCode = buffer.getShort(); + // Skip the status message if any. + if (optionLen > 2) { + skipOption(buffer, optionLen - 2); + } + break; default: skipOption(buffer, optionLen); } } - return new PrefixDelegation(iaid, t1, t2, ipos); + return new PrefixDelegation(iaid, t1, t2, ipos, statusCode); } catch (BufferUnderflowException e) { throw new ParseException(e.getMessage()); } @@ -282,16 +296,26 @@ public class Dhcp6Packet { /** * Build an IA_PD option from given specific parameters, including IA_PREFIX options. + * + * Per RFC8415 section 21.13 if the Status Code option does not appear in a message in + * which the option could appear, the status of the message is assumed to be Success. So + * only put the Status Code option in IA_PD when the status code is not Success. */ public ByteBuffer build(@NonNull final List<IaPrefixOption> input) { final ByteBuffer iapd = ByteBuffer.allocate(IaPdOption.LENGTH - + Struct.getSize(IaPrefixOption.class) * input.size()); + + Struct.getSize(IaPrefixOption.class) * input.size() + + (statusCode != STATUS_SUCCESS ? MIN_STATUS_CODE_OPT_LEN : 0)); iapd.putInt(iaid); iapd.putInt(t1); iapd.putInt(t2); for (IaPrefixOption ipo : input) { ipo.writeToByteBuffer(iapd); } + if (statusCode != STATUS_SUCCESS) { + iapd.putShort(DHCP6_STATUS_CODE); + iapd.putShort((short) 2); + iapd.putShort(statusCode); + } iapd.flip(); return iapd; } @@ -314,8 +338,8 @@ public class Dhcp6Packet { @Override public String toString() { - return "Prefix Delegation: iaid " + iaid + ", t1 " + t1 + ", t2 " + t2 - + ", IA prefix options: " + ipos; + return String.format("Prefix Delegation, iaid: %s, t1: %s, t2: %s, status code: %s," + + " IA prefix options: %s", iaid, t1, t2, statusCodeToString(statusCode), ipos); } /** @@ -366,6 +390,27 @@ public class Dhcp6Packet { } } + private static String statusCodeToString(short statusCode) { + switch (statusCode) { + case STATUS_SUCCESS: + return "Success"; + case STATUS_UNSPEC_FAIL: + return "UnspecFail"; + case STATUS_NO_ADDRS_AVAIL: + return "NoAddrsAvail"; + case STATUS_NO_BINDING: + return "NoBinding"; + case STATUS_NOT_ONLINK: + return "NotOnLink"; + case STATUS_USE_MULTICAST: + return "UseMulticast"; + case STATUS_NO_PREFIX_AVAIL: + return "NoPrefixAvail"; + default: + return "Unknown"; + } + } + private static void skipOption(@NonNull final ByteBuffer packet, int optionLen) throws BufferUnderflowException { for (int i = 0; i < optionLen; i++) { @@ -374,35 +419,6 @@ public class Dhcp6Packet { } /** - * Reads a string of specified length from the buffer. - * - * TODO: move to a common place which can be shared with DhcpClient. - */ - private static String readAsciiString(@NonNull final ByteBuffer buf, int byteCount, - boolean isNullOk) { - final byte[] bytes = new byte[byteCount]; - buf.get(bytes); - return readAsciiString(bytes, isNullOk); - } - - private static String readAsciiString(@NonNull final byte[] payload, boolean isNullOk) { - final byte[] bytes = payload; - int length = bytes.length; - if (!isNullOk) { - // Stop at the first null byte. This is because some DHCP options (e.g., the domain - // name) are passed to netd via FrameworkListener, which refuses arguments containing - // null bytes. We don't do this by default because vendorInfo is an opaque string which - // could in theory contain null bytes. - for (length = 0; length < bytes.length; length++) { - if (bytes[length] == 0) { - break; - } - } - } - return new String(bytes, 0, length, StandardCharsets.US_ASCII); - } - - /** * Creates a concrete Dhcp6Packet from the supplied ByteBuffer. * * The buffer only starts with a UDP encapsulation (i.e. DHCPv6 message). A subset of the @@ -426,7 +442,6 @@ public class Dhcp6Packet { byte[] serverDuid = null; byte[] clientDuid = null; short statusCode = STATUS_SUCCESS; - String statusMsg = null; boolean rapidCommit = false; int solMaxRt = 0; PrefixDelegation pd = null; @@ -487,7 +502,12 @@ public class Dhcp6Packet { case DHCP6_STATUS_CODE: expectedLen = optionLen; statusCode = packet.getShort(); - statusMsg = readAsciiString(packet, expectedLen - 2, false /* isNullOk */); + // Skip the status message (if any), which is a UTF-8 encoded text string + // suitable for display to the end user, but is not useful for Dhcp6Client + // to decide how to properly handle the status code. + if (optionLen - 2 > 0) { + skipOption(packet, optionLen - 2); + } break; case DHCP6_SOL_MAX_RT: expectedLen = 4; @@ -545,7 +565,6 @@ public class Dhcp6Packet { throw new ParseException("Missing IA_PD option"); } newPacket.mStatusCode = statusCode; - newPacket.mStatusMsg = statusMsg; newPacket.mRapidCommit = rapidCommit; newPacket.mSolMaxRt = (solMaxRt >= 60 && solMaxRt <= 86400) diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index 4f8eead7..d7ef4df2 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java @@ -19,7 +19,6 @@ package android.net.ip; import static android.net.RouteInfo.RTN_UNICAST; import static android.net.RouteInfo.RTN_UNREACHABLE; import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable; -import static android.net.dhcp6.Dhcp6Packet.PrefixDelegation; import static android.net.ip.IIpClient.PROV_IPV4_DISABLED; import static android.net.ip.IIpClient.PROV_IPV6_DISABLED; import static android.net.ip.IIpClient.PROV_IPV6_LINKLOCAL; @@ -32,22 +31,28 @@ import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY; import static android.system.OsConstants.AF_PACKET; import static android.system.OsConstants.ETH_P_ARP; import static android.system.OsConstants.ETH_P_IPV6; +import static android.system.OsConstants.IFA_F_NODAD; +import static android.system.OsConstants.RT_SCOPE_UNIVERSE; import static android.system.OsConstants.SOCK_NONBLOCK; import static android.system.OsConstants.SOCK_RAW; +import static com.android.net.module.util.LinkPropertiesUtils.CompareResult; import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY; import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST; import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST; import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH; import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID; +import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR; +import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE; import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE; import static com.android.networkstack.util.NetworkStackUtils.APF_NEW_RA_FILTER_VERSION; -import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_FORCE_DISABLE; +import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GRATUITOUS_NA_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION; -import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION; +import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64; +import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64; import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission; import android.annotation.SuppressLint; @@ -123,8 +128,10 @@ import com.android.internal.util.StateMachine; import com.android.internal.util.WakeupMessage; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.ConnectivityUtils; import com.android.net.module.util.DeviceConfigUtils; import com.android.net.module.util.InterfaceParams; +import com.android.net.module.util.LinkPropertiesUtils; import com.android.net.module.util.SharedLog; import com.android.net.module.util.SocketUtils; import com.android.net.module.util.arp.ArpPacket; @@ -154,7 +161,6 @@ import java.net.MalformedURLException; import java.net.SocketAddress; import java.net.SocketException; import java.net.URL; -import java.net.UnknownHostException; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -541,6 +547,8 @@ public class IpClient extends StateMachine { // IpClient shares a handler with Dhcp6Client: commands must not overlap public static final int DHCP6CLIENT_CMD_BASE = 2000; + private static final int DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS = + IFA_F_MANAGETEMPADDR | IFA_F_NOPREFIXROUTE | IFA_F_NODAD; // Settings and default values. private static final int MAX_LOG_RECORDS = 500; @@ -677,6 +685,8 @@ public class IpClient extends StateMachine { private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>(); // Set of IPv6 addresses from which multicast NS packets have been sent. private final Set<Inet6Address> mMulticastNsSourceAddresses = new HashSet<>(); + // Set of delegated prefixes. + private final Set<IpPrefix> mDelegatedPrefixes = new HashSet<>(); @Nullable private final DevicePolicyManager mDevicePolicyManager; @@ -694,7 +704,7 @@ public class IpClient extends StateMachine { private final boolean mUseNewApfFilter; private final boolean mEnableIpClientIgnoreLowRaLifetime; private final boolean mApfShouldHandleLightDoze; - private final boolean mApfShouldPollingCounters; + private final boolean mEnableApfPollingCounters; private InterfaceParams mInterfaceParams; @@ -721,7 +731,6 @@ public class IpClient extends StateMachine { private Integer mDadTransmits = null; private int mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET; private ApfCapabilities mCurrentApfCapabilities; - private PrefixDelegation mPrefixDelegation; private WakeupMessage mIpv6AutoconfTimeoutAlarm = null; /** @@ -928,13 +937,13 @@ public class IpClient extends StateMachine { CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS, DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS) * DateUtils.SECOND_IN_MILLIS; mUseNewApfFilter = mDependencies.isFeatureEnabled(context, APF_NEW_RA_FILTER_VERSION); + mEnableApfPollingCounters = mDependencies.isFeatureEnabled(context, + APF_POLLING_COUNTERS_VERSION); mEnableIpClientIgnoreLowRaLifetime = mDependencies.isFeatureEnabled(context, IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION); // Light doze mode status checking API is only available at T or later releases. mApfShouldHandleLightDoze = SdkLevel.isAtLeastT() && mDependencies.isFeatureNotChickenedOut( mContext, APF_HANDLE_LIGHT_DOZE_FORCE_DISABLE); - mApfShouldPollingCounters = mDependencies.isFeatureNotChickenedOut( - mContext, APF_POLLING_COUNTERS_FORCE_DISABLE); IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration( mMinRdnssLifetimeSec); @@ -1135,10 +1144,6 @@ public class IpClient extends StateMachine { return mDependencies.isFeatureEnabled(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION); } - private boolean isMulticastNsEnabled() { - return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_MULTICAST_NS_VERSION); - } - @VisibleForTesting static MacAddress getInitialBssid(final Layer2Information layer2Info, final ScanResultInfo scanResultInfo, boolean isAtLeastS) { @@ -1460,7 +1465,6 @@ public class IpClient extends StateMachine { mDhcpResults = null; mTcpBufferSizes = ""; mHttpProxy = null; - mPrefixDelegation = null; mLinkProperties = new LinkProperties(); mLinkProperties.setInterfaceName(mInterfaceName); @@ -1741,6 +1745,31 @@ public class IpClient extends StateMachine { addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers()); mShim.setNat64Prefix(newLp, mShim.getNat64Prefix(netlinkLinkProperties)); + // Check if any link address update from netlink. + final CompareResult<LinkAddress> results = + LinkPropertiesUtils.compareAddresses(mLinkProperties, newLp); + // In the case that there are multiple netlink update events about a global IPv6 address + // derived from the delegated prefix, a flag-only change event(e.g. due to the duplicate + // address detection) will cause an identical IP address to be put into both Added and + // Removed list based on the CompareResult implementation. To prevent a prefix from being + // mistakenly removed from the delegate prefix list, it is better to always check the + // removed list before checking the added list(e.g. anyway we can add the removed prefix + // back again). + for (LinkAddress la : results.removed) { + if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) { + final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH); + mDelegatedPrefixes.remove(prefix); + } + // TODO: remove onIpv6AddressRemoved callback. + } + + for (LinkAddress la : results.added) { + if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) { + final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH); + mDelegatedPrefixes.add(prefix); + } + } + // [3] Add in data from DHCPv4, if available. // // mDhcpResults is never shared with any other owner so we don't have @@ -1777,26 +1806,22 @@ public class IpClient extends StateMachine { // TODO: also look at the IPv6 RA (netlink) for captive portal URL } - // [4] Add in data from DHCPv6 Prefix Delegation, if available. - if (mPrefixDelegation != null) { - for (IaPrefixOption ipo : mPrefixDelegation.ipos) { - try { - final IpPrefix destination = - new IpPrefix(Inet6Address.getByAddress(ipo.prefix), ipo.prefixLen); - // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to this route - // based on the delegated prefix. To prevent the traffic loop between host and - // upstream delegated router. Because we specify the IFA_F_NOPREFIXROUTE when - // adding the IPv6 address, the kernel does not create a delegated prefix route, - // as a result, the user space won't receive any RTM_NEWROUTE message about the - // delegated prefix, we still need to install an unreachable route for the - // delegated prefix manually in LinkProperties to notify the caller this update. - // TODO: support RTN_BLACKHOLE in netd and use that on newer Android versions. - final RouteInfo route = new RouteInfo(destination, null /* gateway */, - mInterfaceName, RTN_UNREACHABLE); - newLp.addRoute(route); - } catch (UnknownHostException e) { - Log.wtf(mTag, "Invalid delegated prefix " + HexDump.toHexString(ipo.prefix)); - } + // [4] Add route with delegated prefix according to the global address update. + if (mDhcp6PrefixDelegationEnabled) { + for (IpPrefix destination : mDelegatedPrefixes) { + // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to + // this route based on the delegated prefix. To prevent the traffic loop + // between host and upstream delegated router. Because we specify the + // IFA_F_NOPREFIXROUTE when adding the IPv6 address, the kernel does not + // create a delegated prefix route, as a result, the user space won't + // receive any RTM_NEWROUTE message about the delegated prefix, we still + // need to install an unreachable route for the delegated prefix manually + // in LinkProperties to notify the caller this update. + // TODO: support RTN_BLACKHOLE in netd and use that on newer Android + // versions. + final RouteInfo route = new RouteInfo(destination, + null /* gateway */, mInterfaceName, RTN_UNREACHABLE); + newLp.addRoute(route); } } @@ -1994,6 +2019,24 @@ public class IpClient extends StateMachine { } } + private static boolean hasFlag(@NonNull final LinkAddress la, final int flags) { + return (la.getFlags() & flags) == flags; + + } + + // Check whether a global IPv6 stable address is derived from DHCPv6 prefix delegation. + // Address derived from delegated prefix should be: + // - unicast global routable address + // - with prefix length of 64 + // - has IFA_F_MANAGETEMPADDR, IFA_F_NOPREFIXROUTE and IFA_F_NODAD flags + private static boolean isIpv6StableDelegatedAddress(@NonNull final LinkAddress la) { + return la.isIpv6() + && !ConnectivityUtils.isIPv6ULA(la.getAddress()) + && (la.getPrefixLength() == RFC7421_PREFIX_LENGTH) + && (la.getScope() == (byte) RT_SCOPE_UNIVERSE) + && hasFlag(la, DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS); + } + // Returns false if we have lost provisioning, true otherwise. private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) { final LinkProperties newLp = assembleLinkProperties(); @@ -2035,9 +2078,7 @@ public class IpClient extends StateMachine { // // TODO: stop sending this multicast NS after deployment of RFC9131 in the field, leverage // the gratuitous NA to update the first-hop router's neighbor cache entry. - if (isMulticastNsEnabled()) { - maybeSendMulticastNSes(newLp); - } + maybeSendMulticastNSes(newLp); // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update, // wait for the provisioning completion and record the latency. @@ -2441,6 +2482,7 @@ public class IpClient extends StateMachine { mHasDisabledAcceptRaDefrtrOnProvLoss = false; mGratuitousNaTargetAddresses.clear(); mMulticastNsSourceAddresses.clear(); + mDelegatedPrefixes.clear(); resetLinkProperties(); if (mStartTimeMillis > 0) { @@ -2903,7 +2945,7 @@ public class IpClient extends StateMachine { if (mApfFilter == null) { mCallback.setFallbackMulticastFilter(mMulticastFiltering); } - if (mApfShouldPollingCounters) { + if (mEnableApfPollingCounters) { sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs); } @@ -2998,27 +3040,68 @@ public class IpClient extends StateMachine { } } - private void clearIpv6PrefixDelegationAddresses() { - if (mPrefixDelegation == null) { - Log.wtf(mTag, "PrefixDelegation shouldn't be null when DHCPv6 PD fails."); - return; - } - final IaPrefixOption ipo = mPrefixDelegation.ipos.get(0); - final IpPrefix prefix; - try { - prefix = new IpPrefix(Inet6Address.getByAddress(ipo.prefix), RFC7421_PREFIX_LENGTH); - } catch (UnknownHostException e) { - Log.wtf(TAG, "Invalid delegated prefix " + HexDump.toHexString(ipo.prefix)); - return; - } - - // Delete the global IPv6 address based on delegated prefix from interface. + private void deleteIpv6PrefixDelegationAddresses(final IpPrefix prefix) { for (LinkAddress la : mLinkProperties.getLinkAddresses()) { final InetAddress address = la.getAddress(); if (prefix.contains(address)) { - NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index, - (Inet6Address) address, (short) la.getPrefixLength()); + if (!NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index, + (Inet6Address) address, (short) la.getPrefixLength())) { + Log.e(TAG, "Failed to delete IPv6 address " + address.getHostAddress()); + } + } + } + } + + private void addInterfaceAddress(@NonNull final Inet6Address address, + @NonNull final IaPrefixOption ipo) { + final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD; + final long now = SystemClock.elapsedRealtime(); + final long deprecationTime = now + ipo.preferred; + final long expirationTime = now + ipo.valid; + final LinkAddress la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags, + RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime); + if (!la.isGlobalPreferred()) { + Log.w(TAG, la + " is not a global IPv6 address"); + return; + } + if (!NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index, address, + (short) RFC7421_PREFIX_LENGTH, + flags, (byte) RT_SCOPE_UNIVERSE /* scope */, + ipo.preferred, ipo.valid)) { + Log.e(TAG, "Failed to set IPv6 address on " + address.getHostAddress() + + "%" + mInterfaceParams.index); + } + } + + private void updateDelegatedAddresses(@NonNull final List<IaPrefixOption> valid) { + if (valid.isEmpty()) return; + for (IaPrefixOption ipo : valid) { + final IpPrefix prefix = ipo.getIpPrefix(); + // The prefix with preferred/valid lifetime of 0 is considered as a valid prefix, + // it can be passed to IpClient from Dhcp6Client, however, client should stop using + // the global addresses derived from this prefix immediately. + if (ipo.withZeroLifetimes()) { + Log.d(TAG, "Delete IPv6 address derived from prefix " + prefix + + " with 0 preferred/valid lifetime"); + deleteIpv6PrefixDelegationAddresses(prefix); } + // Otherwise, configure IPv6 addresses derived from the delegated prefix(es) on + // the interface. We've checked that delegated prefix is valid upon receiving the + // response from DHCPv6 server, and the server may assign a prefix with length less + // than 64. So for SLAAC use case we always set the prefix length to 64 even if the + // delegated prefix length is less than 64. + final Inet6Address address = createInet6AddressFromEui64(prefix, + macAddressToEui64(mInterfaceParams.macAddr)); + addInterfaceAddress(address, ipo); + } + } + + private void removeExpiredDelegatedAddresses(@NonNull final List<IaPrefixOption> expired) { + if (expired.isEmpty()) return; + for (IaPrefixOption ipo : expired) { + final IpPrefix prefix = ipo.getIpPrefix(); + Log.d(TAG, "Delete IPv6 address derived from expired prefix " + prefix); + deleteIpv6PrefixDelegationAddresses(prefix); } } @@ -3216,15 +3299,14 @@ public class IpClient extends StateMachine { case Dhcp6Client.CMD_DHCP6_RESULT: switch(msg.arg1) { case Dhcp6Client.DHCP6_PD_SUCCESS: - mPrefixDelegation = (PrefixDelegation) msg.obj; + final List<IaPrefixOption> toBeUpdated = (List<IaPrefixOption>) msg.obj; + updateDelegatedAddresses(toBeUpdated); handleLinkPropertiesUpdate(SEND_CALLBACKS); break; case Dhcp6Client.DHCP6_PD_PREFIX_EXPIRED: - case Dhcp6Client.DHCP6_PD_PREFIX_CHANGED: - case Dhcp6Client.DHCP6_PD_PREFIX_MSG_EXCHANGE_TERMINATED: - clearIpv6PrefixDelegationAddresses(); - mPrefixDelegation = null; + final List<IaPrefixOption> toBeRemoved = (List<IaPrefixOption>) msg.obj; + removeExpiredDelegatedAddresses(toBeRemoved); handleLinkPropertiesUpdate(SEND_CALLBACKS); break; diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java index 40a4bb65..e252a68b 100644 --- a/src/android/net/ip/IpReachabilityMonitor.java +++ b/src/android/net/ip/IpReachabilityMonitor.java @@ -23,7 +23,6 @@ import static android.net.metrics.IpReachabilityEvent.PROVISIONING_LOST_ORGANIC; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION; -import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION; import android.content.Context; import android.net.ConnectivityManager; @@ -236,7 +235,6 @@ public class IpReachabilityMonitor { private int mInterSolicitIntervalMs; @NonNull private final Callback mCallback; - private final boolean mMulticastResolicitEnabled; private final boolean mIgnoreIncompleteIpv6DnsServerEnabled; private final boolean mIgnoreIncompleteIpv6DefaultRouterEnabled; @@ -260,8 +258,6 @@ public class IpReachabilityMonitor { mUsingMultinetworkPolicyTracker = usingMultinetworkPolicyTracker; mCm = context.getSystemService(ConnectivityManager.class); mDependencies = dependencies; - mMulticastResolicitEnabled = dependencies.isFeatureNotChickenedOut(context, - IP_REACHABILITY_MCAST_RESOLICIT_VERSION); mIgnoreIncompleteIpv6DnsServerEnabled = dependencies.isFeatureNotChickenedOut(context, IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION); mIgnoreIncompleteIpv6DefaultRouterEnabled = dependencies.isFeatureEnabled(context, @@ -274,10 +270,8 @@ public class IpReachabilityMonitor { // In case the overylaid parameters specify an invalid configuration, set the parameters // to the hardcoded defaults first, then set them to the values used in the steady state. try { - int numResolicits = mMulticastResolicitEnabled - ? NUD_MCAST_RESOLICIT_NUM - : INVALID_NUD_MCAST_RESOLICIT_NUM; - setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS, numResolicits); + setNeighborParameters(MIN_NUD_SOLICIT_NUM, MIN_NUD_SOLICIT_INTERVAL_MS, + NUD_MCAST_RESOLICIT_NUM); } catch (Exception e) { Log.e(TAG, "Failed to adjust neighbor parameters with hardcoded defaults"); } @@ -414,8 +408,7 @@ public class IpReachabilityMonitor { private void handleNeighborReachable(@Nullable final NeighborEvent prev, @NonNull final NeighborEvent event) { - if (mMulticastResolicitEnabled - && hasDefaultRouterNeighborMacAddressChanged(prev, event)) { + if (hasDefaultRouterNeighborMacAddressChanged(prev, event)) { // This implies device has confirmed the neighbor's reachability from // other states(e.g., NUD_PROBE or NUD_STALE), checking if the mac // address hasn't changed is required. If Mac address does change, then @@ -581,8 +574,7 @@ public class IpReachabilityMonitor { private long getProbeWakeLockDuration() { final long gracePeriodMs = 500; - final int numSolicits = - mNumSolicits + (mMulticastResolicitEnabled ? NUD_MCAST_RESOLICIT_NUM : 0); + final int numSolicits = mNumSolicits + NUD_MCAST_RESOLICIT_NUM; return (long) (numSolicits * mInterSolicitIntervalMs) + gracePeriodMs; } diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java index 85da4003..37f10eae 100755 --- a/src/com/android/networkstack/util/NetworkStackUtils.java +++ b/src/com/android/networkstack/util/NetworkStackUtils.java @@ -197,13 +197,6 @@ public class NetworkStackUtils { public static final String IPCLIENT_GRATUITOUS_NA_VERSION = "ipclient_gratuitous_na_version"; /** - * Experiment flag to send multicast NS from the global IPv6 GUA to the solicited-node - * multicast address based on the default router's IPv6 link-local address, which helps - * flush the first-hop routers' neighbor cache entry for the global IPv6 GUA. - */ - public static final String IPCLIENT_MULTICAST_NS_VERSION = "ipclient_multicast_ns_version"; - - /** * Experiment flag to enable sending Gratuitous APR and Gratuitous Neighbor Advertisement for * all assigned IPv4 and IPv6 GUAs after completing L2 roaming. */ @@ -219,13 +212,6 @@ public class NetworkStackUtils { "ipclient_accept_ipv6_link_local_dns_version"; /** - * Experiment flag to enable "mcast_resolicit" neighbor parameter in IpReachabilityMonitor, - * set it to 3 by default. - */ - public static final String IP_REACHABILITY_MCAST_RESOLICIT_VERSION = - "ip_reachability_mcast_resolicit_version"; - - /** * Experiment flag to attempt to ignore the on-link IPv6 DNS server which fails to respond to * address resolution. */ @@ -250,12 +236,22 @@ public class NetworkStackUtils { */ public static final String APF_NEW_RA_FILTER_VERSION = "apf_new_ra_filter_version"; /** + * Experiment flag to enable the feature of polling counters in Apf. + */ + public static final String APF_POLLING_COUNTERS_VERSION = "apf_polling_counters_version"; + /** * Experiment flag to enable the feature of ignoring any individual RA section with lifetime * below accept_ra_min_lft sysctl. */ public static final String IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION = "ipclient_ignore_low_ra_lifetime_version"; + /** + * Feature flag to send private DNS resolution queries and probes on a background thread. + */ + public static final String NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION = + "networkmonitor_async_privdns_resolution"; + /**** BEGIN Feature Kill Switch Flags ****/ /** @@ -272,12 +268,6 @@ public class NetworkStackUtils { "apf_handle_light_doze_force_disable"; /** - * Kill switch flag to disable the feature of polling counters in Apf. - */ - public static final String APF_POLLING_COUNTERS_FORCE_DISABLE = - "apf_polling_counters_force_disable"; - - /** * Kill switch flag to disable the feature of skipping Tcp socket info polling when light * doze mode is enabled. */ diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 9303f956..78a47ea8 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java @@ -23,6 +23,9 @@ import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS; import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_PROBE_SPEC; import static android.net.ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL; import static android.net.DnsResolver.FLAG_EMPTY; +import static android.net.DnsResolver.FLAG_NO_CACHE_LOOKUP; +import static android.net.DnsResolver.TYPE_A; +import static android.net.DnsResolver.TYPE_AAAA; import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_INVALID; import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_PARTIAL_CONNECTIVITY; import static android.net.INetworkMonitor.NETWORK_TEST_RESULT_VALID; @@ -122,6 +125,7 @@ import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.os.Build; import android.os.Bundle; +import android.os.CancellationSignal; import android.os.Message; import android.os.Process; import android.os.RemoteException; @@ -140,7 +144,9 @@ import android.telephony.CellSignalStrength; import android.telephony.SignalStrength; import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.util.ArraySet; import android.util.Log; +import android.util.Pair; import android.util.SparseArray; import androidx.annotation.ArrayRes; @@ -234,6 +240,10 @@ public class NetworkMonitor extends StateMachine { @VisibleForTesting static final String CONFIG_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT = "captive_portal_dns_probe_timeout"; + @VisibleForTesting + static final String CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS = + "async_privdns_probe_timeout_ms"; + private static final int DEFAULT_PRIVDNS_PROBE_TIMEOUT_MS = 10_000; private static final int SOCKET_TIMEOUT_MS = 10000; private static final int PROBE_TIMEOUT_MS = 3000; @@ -404,6 +414,32 @@ public class NetworkMonitor extends StateMachine { */ private static final int EVENT_RESOURCE_CONFIG_CHANGED = 25; + /** + * Message to self to notify that private DNS strict mode hostname resolution has finished. + * + * <p>arg2 = Last DNS rcode. + * obj = Pair<List<InetAddress>, DnsCallback>: query results and DnsCallback used. + */ + private static final int CMD_STRICT_MODE_RESOLUTION_COMPLETED = 26; + + /** + * Message to self to notify that the private DNS probe has finished. + * + * <p>arg2 = Last DNS rcode. + * obj = Pair<List<InetAddress>, DnsCallback>: query results and DnsCallback used. + */ + private static final int CMD_PRIVATE_DNS_PROBE_COMPLETED = 27; + + /** + * Message to self to notify that private DNS hostname resolution or probing has failed. + */ + private static final int CMD_PRIVATE_DNS_EVALUATION_FAILED = 28; + + /** + * Message to self to notify that a DNS query has timed out. + */ + private static final int CMD_DNS_TIMEOUT = 29; + // Start mReevaluateDelayMs at this value and double. @VisibleForTesting static final int INITIAL_REEVALUATE_DELAY_MS = 1000; @@ -504,6 +540,10 @@ public class NetworkMonitor extends StateMachine { private final State mEvaluatingState = new EvaluatingState(); private final State mCaptivePortalState = new CaptivePortalState(); private final State mEvaluatingPrivateDnsState = new EvaluatingPrivateDnsState(); + private final State mStartingPrivateDnsEvaluation = new StartingPrivateDnsEvaluation(); + private final State mResolvingPrivateDnsState = new ResolvingPrivateDnsState(); + private final State mProbingForPrivateDnsState = new ProbingForPrivateDnsState(); + private final State mProbingState = new ProbingState(); private final State mWaitingForNextProbeState = new WaitingForNextProbeState(); private final State mEvaluatingBandwidthState = new EvaluatingBandwidthState(); @@ -544,6 +584,8 @@ public class NetworkMonitor extends StateMachine { private final boolean mMetricsEnabled; private final boolean mReevaluateWhenResumeEnabled; + private final boolean mAsyncPrivdnsResolutionEnabled; + @NonNull private final NetworkInformationShim mInfoShim = NetworkInformationShimImpl.newInstance(); @@ -614,6 +656,9 @@ public class NetworkMonitor extends StateMachine { addState(mWaitingForNextProbeState, mEvaluatingState); addState(mCaptivePortalState, mMaybeNotifyState); addState(mEvaluatingPrivateDnsState, mDefaultState); + addState(mStartingPrivateDnsEvaluation, mEvaluatingPrivateDnsState); + addState(mResolvingPrivateDnsState, mEvaluatingPrivateDnsState); + addState(mProbingForPrivateDnsState, mEvaluatingPrivateDnsState); addState(mEvaluatingBandwidthState, mDefaultState); addState(mValidatedState, mDefaultState); setInitialState(mDefaultState); @@ -632,6 +677,8 @@ public class NetworkMonitor extends StateMachine { NetworkStackUtils.VALIDATION_METRICS_VERSION); mReevaluateWhenResumeEnabled = deps.isFeatureEnabled( context, NetworkStackUtils.REEVALUATE_WHEN_RESUME); + mAsyncPrivdnsResolutionEnabled = deps.isFeatureEnabled(context, + NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION); mUseHttps = getUseHttpsValidation(); mCaptivePortalUserAgent = getCaptivePortalUserAgent(); mCaptivePortalFallbackSpecs = @@ -1049,6 +1096,13 @@ public class NetworkMonitor extends StateMachine { if (tst != null) { tst.setOpportunisticMode(cfg.inOpportunisticMode()); } + if (mAsyncPrivdnsResolutionEnabled) { + // When using async privdns validation, reevaluate on any change of + // configuration (even if turning it off), as this will handle + // cancelling current attempts and transitioning to validated state. + removeMessages(CMD_EVALUATE_PRIVATE_DNS); + sendMessage(CMD_EVALUATE_PRIVATE_DNS); + } break; } @@ -1599,13 +1653,28 @@ public class NetworkMonitor extends StateMachine { @Override public boolean processMessage(Message msg) { switch (msg.what) { - case CMD_EVALUATE_PRIVATE_DNS: + case CMD_EVALUATE_PRIVATE_DNS: { + if (mAsyncPrivdnsResolutionEnabled) { + // Cancel any previously scheduled retry attempt + removeMessages(CMD_EVALUATE_PRIVATE_DNS); + + if (inStrictMode()) { + // Note this may happen even in the case where the current state is + // resolve or probe: private DNS evaluation would then restart. + transitionTo(mStartingPrivateDnsEvaluation); + } else { + mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS); + transitionToPrivateDnsEvaluationSuccessState(); + } + break; + } + if (inStrictMode()) { - if (!isStrictModeHostnameResolved()) { + if (!isStrictModeHostnameResolved(mPrivateDnsConfig)) { resolveStrictModeHostname(); - if (isStrictModeHostnameResolved()) { - notifyPrivateDnsConfigResolved(); + if (isStrictModeHostnameResolved(mPrivateDnsConfig)) { + notifyPrivateDnsConfigResolved(mPrivateDnsConfig); } else { handlePrivateDnsEvaluationFailure(); // The private DNS probe fails-fast if the server hostname cannot @@ -1630,23 +1699,24 @@ public class NetworkMonitor extends StateMachine { handlePrivateDnsEvaluationFailure(); break; } - handlePrivateDnsEvaluationSuccess(); + mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, + true /* succeeded */); } else { mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS); } - - if (needEvaluatingBandwidth()) { - transitionTo(mEvaluatingBandwidthState); - } else { - // All good! - transitionTo(mValidatedState); - } + transitionToPrivateDnsEvaluationSuccessState(); break; - case CMD_PRIVATE_DNS_SETTINGS_CHANGED: + } + case CMD_PRIVATE_DNS_SETTINGS_CHANGED: { // When settings change the reevaluation timer must be reset. mPrivateDnsReevalDelayMs = INITIAL_REEVALUATE_DELAY_MS; // Let the message bubble up and be handled by parent states as usual. return NOT_HANDLED; + } + // Only used with mAsyncPrivdnsResolutionEnabled + case CMD_PRIVATE_DNS_EVALUATION_FAILED: { + reschedulePrivateDnsEvaluation(); + } default: return NOT_HANDLED; } @@ -1657,12 +1727,6 @@ public class NetworkMonitor extends StateMachine { return !TextUtils.isEmpty(mPrivateDnsProviderHostname); } - private boolean isStrictModeHostnameResolved() { - return (mPrivateDnsConfig != null) - && mPrivateDnsConfig.hostname.equals(mPrivateDnsProviderHostname) - && (mPrivateDnsConfig.ips.length > 0); - } - private void resolveStrictModeHostname() { try { // Do a blocking DNS resolution using the network-assigned nameservers. @@ -1675,24 +1739,15 @@ public class NetworkMonitor extends StateMachine { } } - private void notifyPrivateDnsConfigResolved() { - try { - mCallback.notifyPrivateDnsConfigResolved(mPrivateDnsConfig.toParcel()); - } catch (RemoteException e) { - Log.e(TAG, "Error sending private DNS config resolved notification", e); - } - } - - private void handlePrivateDnsEvaluationSuccess() { - mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, - true /* succeeded */); - } - private void handlePrivateDnsEvaluationFailure() { mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, false /* succeeded */); mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID, null /* redirectUrl */); + reschedulePrivateDnsEvaluation(); + } + + private void reschedulePrivateDnsEvaluation() { // Queue up a re-evaluation with backoff. // // TODO: Consider abandoning this state after a few attempts and @@ -1730,6 +1785,285 @@ public class NetworkMonitor extends StateMachine { } } + private void transitionToPrivateDnsEvaluationSuccessState() { + if (needEvaluatingBandwidth()) { + transitionTo(mEvaluatingBandwidthState); + } else { + // All good! + transitionTo(mValidatedState); + } + } + + private class StartingPrivateDnsEvaluation extends State { + @Override + public void enter() { + transitionTo(mResolvingPrivateDnsState); + } + } + + private class DnsCallback implements DnsResolver.Callback<List<InetAddress>> { + private final int mReplyMessage; + final CancellationSignal mCancellationSignal; + final boolean mHighPriorityResults; + + DnsCallback(int replyMessage, boolean highPriorityResults) { + mReplyMessage = replyMessage; + mCancellationSignal = new CancellationSignal(); + mHighPriorityResults = highPriorityResults; + } + + @Override + public void onAnswer(List<InetAddress> answer, int rcode) { + sendMessage(mReplyMessage, 0, rcode, new Pair<>(answer, this)); + } + + @Override + public void onError(DnsResolver.DnsException error) { + sendMessage(mReplyMessage, 0, error.code, new Pair<>(null, this)); + } + } + + /** + * Base class for a state that is sending a DNS query, cancelled if the state is exited. + */ + private abstract class DnsQueryState extends State { + private static final int ERROR_TIMEOUT = -1; + private final int mCompletedCommand; + private final ArraySet<DnsCallback> mPendingQueries = new ArraySet<>(2); + private final List<InetAddress> mResults = new ArrayList<>(); + private String mQueryName; + private long mStartTime; + + private DnsQueryState(int completedCommand) { + mCompletedCommand = completedCommand; + } + + @Override + public void enter() { + mPendingQueries.clear(); + mResults.clear(); + mStartTime = SystemClock.elapsedRealtimeNanos(); + + mQueryName = getQueryName(); + if (TextUtils.isEmpty(mQueryName)) { + // No query necessary (in particular not in strict mode): skip DNS query states + mEvaluationState.removeProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS); + transitionToPrivateDnsEvaluationSuccessState(); + return; + } + + final DnsResolver resolver = mDependencies.getDnsResolver(); + mPendingQueries.addAll(sendQueries(mQueryName, resolver)); + sendMessageDelayed(CMD_DNS_TIMEOUT, getTimeoutMs()); + } + + @Override + public void exit() { + removeMessages(CMD_DNS_TIMEOUT); + cancelAllQueries(); + } + + @Override + public boolean processMessage(Message msg) { + if (msg.what == mCompletedCommand) { + final Pair<List<InetAddress>, DnsCallback> result = + (Pair<List<InetAddress>, DnsCallback>) msg.obj; + if (!mPendingQueries.remove(result.second)) { + // Ignore previous queries if the state was exited and re-entered. This state + // calls cancelAllQueries on exit, but this may still happen if results were + // already posted when the querier processed the cancel request. + return HANDLED; + } + + if (result.first != null) { + if (result.second.mHighPriorityResults) { + mResults.addAll(0, result.first); + } else { + mResults.addAll(result.first); + } + } + + if (mPendingQueries.isEmpty()) { + removeMessages(CMD_DNS_TIMEOUT); + final long time = SystemClock.elapsedRealtimeNanos() - mStartTime; + onQueryDone(mQueryName, mResults, msg.arg2 /* lastRCode */, time); + } + return HANDLED; + } else if (msg.what == CMD_DNS_TIMEOUT) { + cancelAllQueries(); + // If some queries were successful, onQueryDone will still proceed, even if + // lastRCode is not a success code. + onQueryDone(mQueryName, mResults, ERROR_TIMEOUT /* lastRCode */, + SystemClock.elapsedRealtimeNanos() - mStartTime); + return HANDLED; + } + return NOT_HANDLED; + } + + private void cancelAllQueries() { + for (int i = 0; i < mPendingQueries.size(); i++) { + mPendingQueries.valueAt(i).mCancellationSignal.cancel(); + } + mPendingQueries.clear(); + } + + abstract void onQueryDone(@NonNull String queryName, @NonNull List<InetAddress> answer, + int lastRCode, long elapsedNanos); + + @NonNull + abstract String getQueryName(); + + abstract List<DnsCallback> sendQueries(@NonNull String queryName, + @NonNull DnsResolver resolver); + + abstract long getTimeoutMs(); + } + + private class ResolvingPrivateDnsState extends DnsQueryState { + private ResolvingPrivateDnsState() { + super(CMD_STRICT_MODE_RESOLUTION_COMPLETED); + } + + @Override + List<DnsCallback> sendQueries(@NonNull String queryName, @NonNull DnsResolver resolver) { + // Follow legacy behavior that sent AAAA and A queries synchronously in sequence: AAAA + // is marked as highPriorityResults, so they are placed first in the resulting list. + final DnsCallback v6Cb = new DnsCallback(CMD_STRICT_MODE_RESOLUTION_COMPLETED, + true /* highPriorityResults */); + final DnsCallback v4Cb = new DnsCallback(CMD_STRICT_MODE_RESOLUTION_COMPLETED, + false /* highPriorityResults */); + + resolver.query(mCleartextDnsNetwork, queryName, TYPE_AAAA, FLAG_NO_CACHE_LOOKUP, + Runnable::run, v6Cb.mCancellationSignal, v6Cb); + resolver.query(mCleartextDnsNetwork, queryName, TYPE_A, FLAG_NO_CACHE_LOOKUP, + Runnable::run, v4Cb.mCancellationSignal, v4Cb); + + return List.of(v6Cb, v4Cb); + } + + @Override + void onQueryDone(@NonNull String queryName, @NonNull List<InetAddress> answer, + int lastRCode, long elapsedNanos) { + if (!Objects.equals(queryName, mPrivateDnsProviderHostname)) { + validationLog("Ignoring stale private DNS resolve answers for " + queryName + + " (now \"" + mPrivateDnsProviderHostname + "\"): " + answer); + // This may happen if mPrivateDnsProviderHostname was changed, in which case + // reevaluation must have been queued (CMD_EVALUATE_PRIVATE_DNS), but results for + // the first evaluation are received before the reevaluation command gets processed. + // Just ignore the results and wait for reevaluation to be processed. + // More generally, reevaluation is scheduled every time the hostname changes, so + // IP addresses matching the hostname are eventually received, but intermediate + // results should be ignored to avoid reporting a PrivateDnsConfig with IP addresses + // that don't match mPrivateDnsProviderHostname. + return; + } + + if (!answer.isEmpty()) { + final InetAddress[] ips = answer.toArray(new InetAddress[0]); + final PrivateDnsConfig config = + new PrivateDnsConfig(mPrivateDnsProviderHostname, ips); + notifyPrivateDnsConfigResolved(config); + + validationLog("Strict mode hostname resolution " + elapsedNanos + "ns OK " + + answer + " for " + mPrivateDnsProviderHostname); + transitionTo(mProbingForPrivateDnsState); + } else { + mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, + false /* succeeded */); + mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID, + null /* redirectUrl */); + + validationLog("Strict mode hostname resolution " + elapsedNanos + "ns FAIL " + + "lastRCode " + lastRCode + " for " + mPrivateDnsProviderHostname); + sendMessage(CMD_PRIVATE_DNS_EVALUATION_FAILED); + + // The private DNS probe fails-fast if the server hostname cannot + // be resolved. Record it as a failure with zero latency. + recordProbeEventMetrics(ProbeType.PT_PRIVDNS, 0 /* latency */, + ProbeResult.PR_FAILURE, null /* capportData */); + } + } + + @NonNull + @Override + String getQueryName() { + return mPrivateDnsProviderHostname; + } + + @Override + long getTimeoutMs() { + return getDnsProbeTimeout(); + } + } + + private class ProbingForPrivateDnsState extends DnsQueryState { + private ProbingForPrivateDnsState() { + super(CMD_PRIVATE_DNS_PROBE_COMPLETED); + } + + @Override + public void enter() { + super.enter(); + } + + @Override + List<DnsCallback> sendQueries(@NonNull String queryName, @NonNull DnsResolver resolver) { + final DnsCallback cb = new DnsCallback(CMD_PRIVATE_DNS_PROBE_COMPLETED, + false /* highPriorityResults */); + resolver.query(mNetwork, queryName, FLAG_EMPTY, Runnable::run, cb.mCancellationSignal, + cb); + return Collections.singletonList(cb); + } + + @Override + void onQueryDone(@NonNull String queryName, @NonNull List<InetAddress> answer, + int lastRCode, long elapsedNanos) { + final boolean success = !answer.isEmpty(); + recordProbeEventMetrics(ProbeType.PT_PRIVDNS, elapsedNanos, + success ? ProbeResult.PR_SUCCESS : + ProbeResult.PR_FAILURE, null /* capportData */); + logValidationProbe(elapsedNanos, PROBE_PRIVDNS, success ? DNS_SUCCESS : DNS_FAILURE); + + final String strIps = Objects.toString(answer); + validationLog(PROBE_PRIVDNS, queryName, + String.format("%dus: %s", elapsedNanos / 1000, strIps)); + + mEvaluationState.noteProbeResult(NETWORK_VALIDATION_PROBE_PRIVDNS, success); + if (success) { + transitionToPrivateDnsEvaluationSuccessState(); + } else { + mEvaluationState.reportEvaluationResult(NETWORK_VALIDATION_RESULT_INVALID, + null /* redirectUrl */); + sendMessage(CMD_PRIVATE_DNS_EVALUATION_FAILED); + } + } + + @Override + long getTimeoutMs() { + return getAsyncPrivateDnsProbeTimeout(); + } + + @NonNull + @Override + String getQueryName() { + return UUID.randomUUID().toString().substring(0, 8) + PRIVATE_DNS_PROBE_HOST_SUFFIX; + } + } + + private boolean isStrictModeHostnameResolved(PrivateDnsConfig config) { + return (config != null) + && config.hostname.equals(mPrivateDnsProviderHostname) + && (config.ips.length > 0); + } + + private void notifyPrivateDnsConfigResolved(@NonNull PrivateDnsConfig config) { + try { + mCallback.notifyPrivateDnsConfigResolved(config.toParcel()); + } catch (RemoteException e) { + Log.e(TAG, "Error sending private DNS config resolved notification", e); + } + } + private class ProbingState extends State { private Thread mThread; @@ -2186,6 +2520,11 @@ public class NetworkMonitor extends StateMachine { CONFIG_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT, DEFAULT_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT); } + private int getAsyncPrivateDnsProbeTimeout() { + return mDependencies.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, + CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS, DEFAULT_PRIVDNS_PROBE_TIMEOUT_MS); + } + /** * Gets an integer setting from resources or device config * diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp index 8300c73f..ec058d7a 100644 --- a/tests/integration/Android.bp +++ b/tests/integration/Android.bp @@ -68,6 +68,9 @@ android_library { static_libs: [ "NetworkStackApiStableLib", ], + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Network stack integration tests. @@ -84,6 +87,9 @@ android_test { jarjar_rules: ":NetworkStackJarJarRules", host_required: ["net-tests-utils-host-common"], test_config_template: "AndroidTestTemplate_Integration.xml", + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Network stack next integration tests. @@ -107,6 +113,9 @@ android_test { jarjar_rules: ":NetworkStackJarJarRules", host_required: ["net-tests-utils-host-common"], test_config_template: "AndroidTestTemplate_Integration.xml", + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Network stack integration root tests. @@ -124,12 +133,18 @@ android_test { "NetworkStackApiStableLib", ], platform_apis: true, - test_suites: ["general-tests", "mts-networking"], + test_suites: [ + "general-tests", + "mts-networking", + ], compile_multilib: "both", manifest: "AndroidManifest_root.xml", jarjar_rules: ":NetworkStackJarJarRules", host_required: ["net-tests-utils-host-common"], test_config_template: "AndroidTestTemplate_Integration.xml", + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Special version of the network stack tests that includes all tests necessary for code coverage @@ -138,7 +153,10 @@ android_test { name: "NetworkStackCoverageTests", certificate: "networkstack", platform_apis: true, - test_suites: ["device-tests", "mts-networking"], + test_suites: [ + "device-tests", + "mts-networking", + ], test_config: "AndroidTest_Coverage.xml", defaults: [ "NetworkStackReleaseTargetSdk", @@ -154,4 +172,7 @@ android_test { compile_multilib: "both", manifest: "AndroidManifest_coverage.xml", jarjar_rules: ":NetworkStackJarJarRules", + lint: { + baseline_filename: "lint-baseline.xml", + }, } diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java index c733f9c9..2f1f7d16 100644 --- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java +++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java @@ -312,6 +312,7 @@ public abstract class IpClientIntegrationTestCommon { protected static final long TEST_TIMEOUT_MS = 2_000L; private static final long TEST_WAIT_ENOBUFS_TIMEOUT_MS = 30_000L; + private static final long TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS = 15_000L; @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); @@ -779,6 +780,8 @@ public abstract class IpClientIntegrationTestCommon { enableRealAlarm("DhcpClient." + mIfaceName + ".KICK"); // Enable alarm for IPv6 autoconf via SLAAC in IpClient. enableRealAlarm("IpClient." + mIfaceName + ".EVENT_IPV6_AUTOCONF_TIMEOUT"); + // Enable packet retransmit alarm in Dhcp6Client. + enableRealAlarm("Dhcp6Client." + mIfaceName + ".KICK"); } mIIpClient = makeIIpClient(mIfaceName, mCb); @@ -4137,12 +4140,6 @@ public abstract class IpClientIntegrationTestCommon { return ns; } - // Override this function with disabled experiment flag by default, in order not to - // affect those tests which are just related to basic IpReachabilityMonitor infra. - private void prepareIpReachabilityMonitorTest() throws Exception { - prepareIpReachabilityMonitorTest(false /* isMulticastResolicitEnabled */); - } - private void assertNotifyNeighborLost(Inet6Address targetIp, NudEventType eventType) throws Exception { // For root test suite, rely on the IIpClient aidl interface version constant defined in @@ -4175,8 +4172,7 @@ public abstract class IpClientIntegrationTestCommon { verify(mCb, never()).onReachabilityLost(any()); } - private void prepareIpReachabilityMonitorTest(boolean isMulticastResolicitEnabled) - throws Exception { + private void prepareIpReachabilityMonitorTest() throws Exception { final ScanResultInfo info = makeScanResultInfo(TEST_DEFAULT_SSID, TEST_DEFAULT_BSSID); ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() .withLayer2Information(new Layer2Information(TEST_L2KEY, TEST_CLUSTER, @@ -4185,8 +4181,6 @@ public abstract class IpClientIntegrationTestCommon { .withDisplayName(TEST_DEFAULT_SSID) .withoutIPv4() .build(); - setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION, - isMulticastResolicitEnabled); startIpClientProvisioning(config); verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true); doIpv6OnlyProvisioning(); @@ -4200,11 +4194,15 @@ public abstract class IpClientIntegrationTestCommon { final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations(); final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource(); - assertEquals(expectedNudSolicitNum, nsList.size()); - for (NeighborSolicitation ns : nsList) { + int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM; + assertEquals(expectedSize, nsList.size()); + for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) { assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */, ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */); } + for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) { + assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */); + } } @Test @@ -4239,43 +4237,10 @@ public abstract class IpClientIntegrationTestCommon { assertNeverNotifyNeighborLost(); } - private void runIpReachabilityMonitorMcastResolicitProbeFailedTest() throws Exception { - prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */); - - final List<NeighborSolicitation> nsList = waitForMultipleNeighborSolicitations(); - final int expectedNudSolicitNum = readNudSolicitNumPostRoamingFromResource(); - int expectedSize = expectedNudSolicitNum + NUD_MCAST_RESOLICIT_NUM; - assertEquals(expectedSize, nsList.size()); - for (NeighborSolicitation ns : nsList.subList(0, expectedNudSolicitNum)) { - assertUnicastNeighborSolicitation(ns, ROUTER_MAC /* dstMac */, - ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */); - } - for (NeighborSolicitation ns : nsList.subList(expectedNudSolicitNum, nsList.size())) { - assertMulticastNeighborSolicitation(ns, ROUTER_LINK_LOCAL /* targetIp */); - } - } - - @Test - public void testIpReachabilityMonitor_mcastResolicitProbeFailed() throws Exception { - runIpReachabilityMonitorMcastResolicitProbeFailedTest(); - assertNotifyNeighborLost(ROUTER_LINK_LOCAL /* targetIp */, - NudEventType.NUD_POST_ROAMING_FAILED_CRITICAL); - } - - @Test @SignatureRequiredTest(reason = "requires mock callback object") - public void testIpReachabilityMonitor_mcastResolicitProbeFailed_legacyCallback() - throws Exception { - when(mCb.getInterfaceVersion()).thenReturn(12 /* assign an older interface aidl version */); - - runIpReachabilityMonitorMcastResolicitProbeFailedTest(); - verify(mCb, timeout(TEST_TIMEOUT_MS)).onReachabilityLost(any()); - verify(mCb, never()).onReachabilityFailure(any()); - } - @Test public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithSameLinkLayerAddress() throws Exception { - prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */); + prepareIpReachabilityMonitorTest(); final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */, ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */); @@ -4292,7 +4257,7 @@ public abstract class IpClientIntegrationTestCommon { @Test public void testIpReachabilityMonitor_mcastResolicitProbeReachableWithDiffLinkLayerAddress() throws Exception { - prepareIpReachabilityMonitorTest(true /* isMulticastResolicitEnabled */); + prepareIpReachabilityMonitorTest(); final NeighborSolicitation ns = waitForUnicastNeighborSolicitation(ROUTER_MAC /* dstMac */, ROUTER_LINK_LOCAL /* dstIp */, ROUTER_LINK_LOCAL /* targetIp */); @@ -4664,9 +4629,6 @@ public abstract class IpClientIntegrationTestCommon { .withoutIPv4() .build(); - setFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION, - true /* isUnsolicitedNsEnabled */); - assertTrue(isFeatureEnabled(NetworkStackUtils.IPCLIENT_MULTICAST_NS_VERSION)); startIpClientProvisioning(config); doIpv6OnlyProvisioning(); @@ -5216,9 +5178,7 @@ public abstract class IpClientIntegrationTestCommon { x -> x.isIpv6Provisioned() && hasIpv6AddressPrefixedWith(x, prefix) && hasIpv6AddressPrefixedWith(x, prefix1) - // TODO: comment this line to make the test passed, remove the comment later - // once IpClient supports multi-prefixes. - // && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE) + && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE) && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE) // IPv6 link-local, four global delegated IPv6 addresses && x.getLinkAddresses().size() == 5 @@ -5280,14 +5240,15 @@ public abstract class IpClientIntegrationTestCommon { mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, (Inet6Address) mClientIpAddress, false /* rapidCommit */)); verify(mCb, never()).onProvisioningFailure(any()); + // IPv6 addresses derived from prefix with 0 preferred/valid lifetime should be deleted. verify(mCb, timeout(TEST_TIMEOUT_MS)).onLinkPropertiesChange(argThat( x -> x.isIpv6Provisioned() - && hasIpv6AddressPrefixedWith(x, prefix) + && !hasIpv6AddressPrefixedWith(x, prefix) && hasIpv6AddressPrefixedWith(x, prefix1) - && hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE) + && !hasRouteTo(x, "2001:db8:1::/64", RTN_UNREACHABLE) && hasRouteTo(x, "2001:db8:2::/64", RTN_UNREACHABLE) - // IPv6 link-local, four global delegated IPv6 addresses - && x.getLinkAddresses().size() == 5 + // IPv6 link-local, two global delegated IPv6 addresses with prefix1 + && x.getLinkAddresses().size() == 3 )); handler.post(() -> renewAlarm.onAlarm()); @@ -5296,7 +5257,8 @@ public abstract class IpClientIntegrationTestCommon { packet = getNextDhcp6Packet(); assertTrue(packet instanceof Dhcp6RenewPacket); final List<IaPrefixOption> renewIpos = packet.getPrefixDelegation().ipos; - assertEquals(1, renewIpos.size()); // don't renew prefix 2001:db8:1::/64 + assertEquals(1, renewIpos.size()); // don't renew prefix 2001:db8:1::/64 with 0 + // preferred/valid lifetime assertEquals(prefix1, renewIpos.get(0).getIpPrefix()); } @@ -5317,7 +5279,7 @@ public abstract class IpClientIntegrationTestCommon { clearInvocations(mCb); - // Reply with the requested prefix with preferred/valid lifetime of 0. + // Reply with the requested prefix with the same t1/t2/lifetime. final IpPrefix prefix = new IpPrefix("2001:db8:1::/64"); final IaPrefixOption ipo = buildIaPrefixOption(prefix, 3600 /* preferred */, 3600 /* valid */); @@ -5339,6 +5301,220 @@ public abstract class IpClientIntegrationTestCommon { } @Test + public void testDhcp6Pd_multipleIaPrefixOptions() throws Exception { + final InOrder inOrder = inOrder(mCb); + final IpPrefix prefix1 = new IpPrefix("2001:db8:1::/64"); + final IpPrefix prefix2 = new IpPrefix("2400:db8:100::/64"); + final IpPrefix prefix3 = new IpPrefix("fd7c:9df8:7f39:dc89::/64"); + final IaPrefixOption ipo1 = buildIaPrefixOption(prefix1, 4500 /* preferred */, + 7200 /* valid */); + final IaPrefixOption ipo2 = buildIaPrefixOption(prefix2, 5600 /* preferred */, + 6000 /* valid */); + final IaPrefixOption ipo3 = buildIaPrefixOption(prefix3, 7200 /* preferred */, + 14400 /* valid */); + prepareDhcp6PdTest(); + handleDhcp6Packets(Arrays.asList(ipo1, ipo2, ipo3), 3600 /* t1 */, 4500 /* t2 */, + true /* shouldReplyRapidCommit */); + + final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class); + verifyWithTimeout(inOrder, mCb).onProvisioningSuccess(captor.capture()); + LinkProperties lp = captor.getValue(); + + // Sometimes privacy address or route may appear later along with onLinkPropertiesChange + // callback, in this case we wait a bit longer to see all of these properties appeared and + // then verify if they are what we are looking for. + if (lp.getLinkAddresses().size() < 5 || lp.getRoutes().size() < 4) { + final CompletableFuture<LinkProperties> lpFuture = new CompletableFuture<>(); + verifyWithTimeout(inOrder, mCb).onLinkPropertiesChange(argThat(x -> { + if (!x.isIpv6Provisioned()) return false; + if (x.getLinkAddresses().size() != 5) return false; + if (x.getRoutes().size() != 4) return false; + lpFuture.complete(x); + return true; + })); + lp = lpFuture.get(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } + assertNotNull(lp); + assertTrue(hasIpv6AddressPrefixedWith(lp, prefix1)); + assertTrue(hasIpv6AddressPrefixedWith(lp, prefix2)); + assertFalse(hasIpv6AddressPrefixedWith(lp, prefix3)); + assertTrue(hasRouteTo(lp, prefix1.toString(), RTN_UNREACHABLE)); + assertTrue(hasRouteTo(lp, prefix2.toString(), RTN_UNREACHABLE)); + assertFalse(hasRouteTo(lp, prefix3.toString(), RTN_UNREACHABLE)); + } + + private void runDhcp6PacketWithNoPrefixAvailStatusCodeTest(boolean shouldReplyWithAdvertise) + throws Exception { + prepareDhcp6PdTest(); + Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS); + assertTrue(packet instanceof Dhcp6SolicitPacket); + + final PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 0 /* t1 */, 0 /* t2 */, + new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL); + final ByteBuffer iapd = pd.build(); + if (shouldReplyWithAdvertise) { + mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress)); + } else { + mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress, true /* rapidCommit */)); + } + + // Check if client will ignore Advertise or Reply for Rapid Commit Solicit and + // retransmit Solicit. + packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS); + assertTrue(packet instanceof Dhcp6SolicitPacket); + } + + @Test + public void testDhcp6AdvertiseWithNoPrefixAvailStatusCode() throws Exception { + // Advertise + runDhcp6PacketWithNoPrefixAvailStatusCodeTest(true /* shouldReplyWithAdvertise */); + } + + @Test + public void testDhcp6ReplyForRapidCommitSolicitWithNoPrefixAvailStatusCode() throws Exception { + // Reply + runDhcp6PacketWithNoPrefixAvailStatusCodeTest(false /* shouldReplyWithAdvertise */); + } + + @Test + public void testDhcp6ReplyForRequestWithNoPrefixAvailStatusCode() throws Exception { + prepareDhcp6PdTest(); + Dhcp6Packet packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS); + assertTrue(packet instanceof Dhcp6SolicitPacket); + + final IpPrefix prefix = new IpPrefix("2001:db8:1::/64"); + final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */, + 7200 /* valid */); + PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */, + 2000 /* t2 */, Arrays.asList(ipo)); + ByteBuffer iapd = pd.build(); + mPacketReader.sendResponse(buildDhcp6Advertise(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress)); + + packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS); + assertTrue(packet instanceof Dhcp6RequestPacket); + + // Reply for Request with NoPrefixAvail status code. Not sure if this is reasonable in + // practice, but Server can do everything it wants. + pd = new PrefixDelegation(packet.getIaId(), 0 /* t1 */, 0 /* t2 */, + new ArrayList<IaPrefixOption>() /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL); + iapd = pd.build(); + mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress, false /* rapidCommit */)); + + // Check if client will ignore Reply for Request with NoPrefixAvail status code, and + // rollback to SolicitState. + packet = getNextDhcp6Packet(PACKET_TIMEOUT_MS); + assertTrue(packet instanceof Dhcp6SolicitPacket); + } + + @Test + @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms") + public void testDhcp6ReplyForRenewWithNoPrefixAvailStatusCode() throws Exception { + prepareDhcp6PdRenewTest(); + + final InOrder inOrder = inOrder(mAlarm); + final Handler handler = mDependencies.mDhcp6Client.getHandler(); + final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler); + + handler.post(() -> renewAlarm.onAlarm()); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + Dhcp6Packet packet = getNextDhcp6Packet(); + assertTrue(packet instanceof Dhcp6RenewPacket); + + // Reply with normal IA_PD. + final IpPrefix prefix = new IpPrefix("2001:db8:1::/64"); + final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */, + 7200 /* valid */); + PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */, + 2000 /* t2 */, Arrays.asList(ipo)); + ByteBuffer iapd = pd.build(); + mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress, false /* rapidCommit */)); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + // Trigger another Renew message. + handler.post(() -> renewAlarm.onAlarm()); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + packet = getNextDhcp6Packet(); + assertTrue(packet instanceof Dhcp6RenewPacket); + + // Reply for Renew with NoPrefixAvail status code, check if client will retransmit the + // Renew message. + pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */, 4500 /* t2 */, + new ArrayList<IaPrefixOption>(0) /* ipos */, Dhcp6Packet.STATUS_NO_PREFIX_AVAIL); + iapd = pd.build(); + mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress, false /* rapidCommit */)); + + packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS); + assertTrue(packet instanceof Dhcp6RenewPacket); + } + + @Test + @SignatureRequiredTest(reason = "Need to mock the DHCP6 renew/rebind alarms") + public void testDhcp6ReplyForRebindWithNoPrefixAvailStatusCode() throws Exception { + prepareDhcp6PdRenewTest(); + + final InOrder inOrder = inOrder(mAlarm); + final Handler handler = mDependencies.mDhcp6Client.getHandler(); + final OnAlarmListener renewAlarm = expectAlarmSet(inOrder, "RENEW", 3600, handler); + final OnAlarmListener rebindAlarm = expectAlarmSet(inOrder, "REBIND", 4500, handler); + + handler.post(() -> renewAlarm.onAlarm()); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + Dhcp6Packet packet = getNextDhcp6Packet(); + assertTrue(packet instanceof Dhcp6RenewPacket); + + handler.post(() -> rebindAlarm.onAlarm()); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + packet = getNextDhcp6Packet(); + assertTrue(packet instanceof Dhcp6RebindPacket); + + // Reply with normal IA_PD. + final IpPrefix prefix = new IpPrefix("2001:db8:1::/64"); + final IaPrefixOption ipo = buildIaPrefixOption(prefix, 4500 /* preferred */, + 7200 /* valid */); + PrefixDelegation pd = new PrefixDelegation(packet.getIaId(), 1000 /* t1 */, + 2000 /* t2 */, Arrays.asList(ipo)); + ByteBuffer iapd = pd.build(); + mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress, false /* rapidCommit */)); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + // Trigger another Rebind message. + handler.post(() -> renewAlarm.onAlarm()); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + packet = getNextDhcp6Packet(); + assertTrue(packet instanceof Dhcp6RenewPacket); + + handler.post(() -> rebindAlarm.onAlarm()); + HandlerUtils.waitForIdle(handler, TEST_TIMEOUT_MS); + + packet = getNextDhcp6Packet(); + assertTrue(packet instanceof Dhcp6RebindPacket); + + // Reply for Rebind with NoPrefixAvail status code, check if client will retransmit the + // Rebind message. + pd = new PrefixDelegation(packet.getIaId(), 3600 /* t1 */, + 4500 /* t2 */, new ArrayList<IaPrefixOption>(0) /* ipos */, + Dhcp6Packet.STATUS_NO_PREFIX_AVAIL); + iapd = pd.build(); + mPacketReader.sendResponse(buildDhcp6Reply(packet, iapd.array(), mClientMac, + (Inet6Address) mClientIpAddress, false /* rapidCommit */)); + + packet = getNextDhcp6Packet(TEST_WAIT_RENEW_REBIND_RETRANSMIT_MS); + assertTrue(packet instanceof Dhcp6RebindPacket); + } + + @Test @SignatureRequiredTest(reason = "InterfaceParams.getByName requires CAP_NET_ADMIN") public void testSendRtmDelAddressMethod() throws Exception { ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 464e4a17..ea297146 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -21,7 +21,10 @@ package { java_defaults { name: "NetworkStackTestsDefaults", platform_apis: true, - srcs: ["src/**/*.java", "src/**/*.kt"], + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], resource_dirs: ["res"], static_libs: [ "androidx.test.ext.junit", @@ -39,7 +42,7 @@ java_defaults { ], defaults: [ "framework-connectivity-test-defaults", - "libnetworkstackutilsjni_deps" + "libnetworkstackutilsjni_deps", ], jni_libs: [ // For mockito extended @@ -66,6 +69,9 @@ android_test { static_libs: ["NetworkStackApiCurrentLib"], compile_multilib: "both", // Workaround for b/147785146 for mainline-presubmit jarjar_rules: ":NetworkStackJarJarRules", + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Library containing the unit tests. This is used by the coverage test target to pull in the @@ -76,18 +82,24 @@ android_library { min_sdk_version: "30", defaults: ["NetworkStackTestsDefaults"], static_libs: ["NetworkStackApiStableLib"], - lint: { test: true }, + lint: { + test: true, + baseline_filename: "lint-baseline.xml", + }, visibility: [ "//packages/modules/NetworkStack/tests/integration", "//packages/modules/Connectivity/tests:__subpackages__", "//packages/modules/Connectivity/Tethering/tests:__subpackages__", - ] + ], } android_test { name: "NetworkStackTests", min_sdk_version: "30", - test_suites: ["general-tests", "mts"], + test_suites: [ + "general-tests", + "mts", + ], defaults: [ "NetworkStackTestsDefaults", "connectivity-mainline-presubmit-java-defaults", @@ -95,6 +107,9 @@ android_test { static_libs: ["NetworkStackApiStableLib"], compile_multilib: "both", jarjar_rules: ":NetworkStackJarJarRules", + lint: { + baseline_filename: "lint-baseline.xml", + }, } // Additional dependencies of libnetworkstackutilsjni that are not provided by the system when diff --git a/tests/unit/src/android/net/apf/ApfTest.java b/tests/unit/src/android/net/apf/ApfTest.java index 0b61e04d..4e1187b1 100644 --- a/tests/unit/src/android/net/apf/ApfTest.java +++ b/tests/unit/src/android/net/apf/ApfTest.java @@ -16,6 +16,7 @@ package android.net.apf; +import static android.net.apf.ApfGenerator.APF_VERSION_4; import static android.net.apf.ApfGenerator.Register.R0; import static android.net.apf.ApfGenerator.Register.R1; import static android.net.apf.ApfJniUtils.compareBpfApf; @@ -631,7 +632,7 @@ public class ApfTest { // Test jump if bytes not equal. gen = new ApfGenerator(MIN_APF_VERSION); gen.addLoadImmediate(R0, 1); - gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL); + gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL); program = gen.generate(); assertEquals(6, program.length); assertEquals((13 << 3) | (1 << 1) | 0, program[0]); @@ -643,20 +644,20 @@ public class ApfTest { assertDrop(program, new byte[MIN_PKT_SIZE], 0); gen = new ApfGenerator(MIN_APF_VERSION); gen.addLoadImmediate(R0, 1); - gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL); + gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL); byte[] packet123 = {0,123,0,0,0,0,0,0,0,0,0,0,0,0,0}; assertPass(gen, packet123, 0); gen = new ApfGenerator(MIN_APF_VERSION); - gen.addJumpIfBytesNotEqual(R0, new byte[]{123}, gen.DROP_LABEL); + gen.addJumpIfBytesAtR0NotEqual(new byte[]{123}, gen.DROP_LABEL); assertDrop(gen, packet123, 0); gen = new ApfGenerator(MIN_APF_VERSION); gen.addLoadImmediate(R0, 1); - gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL); + gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 30, 4, 5}, gen.DROP_LABEL); byte[] packet12345 = {0,1,2,3,4,5,0,0,0,0,0,0,0,0,0}; assertDrop(gen, packet12345, 0); gen = new ApfGenerator(MIN_APF_VERSION); gen.addLoadImmediate(R0, 1); - gen.addJumpIfBytesNotEqual(R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL); + gen.addJumpIfBytesAtR0NotEqual(new byte[]{1, 2, 3, 4, 5}, gen.DROP_LABEL); assertPass(gen, packet12345, 0); } @@ -747,23 +748,23 @@ public class ApfTest { ApfGenerator gen; // Load data with no offset: lddw R0, [0 + r1] - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadData(R0, 0); assertProgramEquals(new byte[]{LDDW_OP | SIZE0}, gen.generate()); // Store data with 8bit negative offset: lddw r0, [-42 + r1] - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addStoreData(R0, -42); assertProgramEquals(new byte[]{STDW_OP | SIZE8, -42}, gen.generate()); // Store data to R1 with 16bit negative offset: stdw r1, [-0x1122 + r0] - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addStoreData(R1, -0x1122); assertProgramEquals(new byte[]{STDW_OP | SIZE16 | R1_REG, (byte)0xEE, (byte)0xDE}, gen.generate()); // Load data to R1 with 32bit negative offset: lddw r1, [0xDEADBEEF + r0] - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadData(R1, 0xDEADBEEF); assertProgramEquals( new byte[]{LDDW_OP | SIZE32 | R1_REG, @@ -781,12 +782,12 @@ public class ApfTest { byte[] expected_data = data.clone(); // No memory access instructions: should leave the data segment untouched. - ApfGenerator gen = new ApfGenerator(3); + ApfGenerator gen = new ApfGenerator(APF_VERSION_4); assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); // Expect value 0x87654321 to be stored starting from address -11 from the end of the // data buffer, in big-endian order. - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R0, 0x87654321); gen.addLoadImmediate(R1, -5); gen.addStoreData(R0, -6); // -5 + -6 = -11 (offset +5 with data_len=16) @@ -803,7 +804,7 @@ public class ApfTest { @Test public void testApfDataRead() throws IllegalInstructionException, Exception { // Program that DROPs if address 10 (-6) contains 0x87654321. - ApfGenerator gen = new ApfGenerator(3); + ApfGenerator gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R1, 1000); gen.addLoadData(R0, -1006); // 1000 + -1006 = -6 (offset +10 with data_len=16) gen.addJumpIfR0Equals(0x87654321, gen.DROP_LABEL); @@ -832,7 +833,7 @@ public class ApfTest { */ @Test public void testApfDataReadModifyWrite() throws IllegalInstructionException, Exception { - ApfGenerator gen = new ApfGenerator(3); + ApfGenerator gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R1, -22); gen.addLoadData(R0, 0); // Load from address 32 -22 + 0 = 10 gen.addAdd(0x78453412); // 87654321 + 78453412 = FFAA7733 @@ -859,35 +860,35 @@ public class ApfTest { byte[] expected_data = data; // Program that DROPs unconditionally. This is our the baseline. - ApfGenerator gen = new ApfGenerator(3); + ApfGenerator gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R0, 3); gen.addLoadData(R1, 7); gen.addJump(gen.DROP_LABEL); assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data); // Same program as before, but this time we're trying to load past the end of the data. - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R0, 20); gen.addLoadData(R1, 15); // 20 + 15 > 32 gen.addJump(gen.DROP_LABEL); // Not reached. assertDataMemoryContents(PASS, gen.generate(), packet, data, expected_data); // Subtracting an immediate should work... - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R0, 20); gen.addLoadData(R1, -4); gen.addJump(gen.DROP_LABEL); assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data); // ...and underflowing simply wraps around to the end of the buffer... - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R0, 20); gen.addLoadData(R1, -30); gen.addJump(gen.DROP_LABEL); assertDataMemoryContents(DROP, gen.generate(), packet, data, expected_data); // ...but doesn't allow accesses before the start of the buffer - gen = new ApfGenerator(3); + gen = new ApfGenerator(APF_VERSION_4); gen.addLoadImmediate(R0, 20); gen.addLoadData(R1, -1000); gen.addJump(gen.DROP_LABEL); // Not reached. diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt index 1b74c3ee..162feef2 100644 --- a/tests/unit/src/android/net/apf/ApfV5Test.kt +++ b/tests/unit/src/android/net/apf/ApfV5Test.kt @@ -16,6 +16,8 @@ package android.net.apf import android.net.apf.ApfGenerator.IllegalInstructionException +import android.net.apf.ApfGenerator.Register.R0 +import android.net.apf.ApfGenerator.Register.R1 import androidx.test.filters.SmallTest import androidx.test.runner.AndroidJUnit4 import java.lang.IllegalArgumentException @@ -39,13 +41,130 @@ class ApfV5Test { assertFailsWith<IllegalInstructionException> { gen.addCountAndPass(1000) } assertFailsWith<IllegalInstructionException> { gen.addTransmit() } assertFailsWith<IllegalInstructionException> { gen.addDiscard() } + assertFailsWith<IllegalInstructionException> { gen.addAllocateR0() } + assertFailsWith<IllegalInstructionException> { gen.addAllocate(100) } + assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU8(100) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU16(100) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU32(100) } + assertFailsWith<IllegalInstructionException> { gen.addPacketCopy(100, 100) } + assertFailsWith<IllegalInstructionException> { gen.addDataCopy(100, 100) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU8(R0) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU16(R0) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU32(R0) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU8(R1) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU16(R1) } + assertFailsWith<IllegalInstructionException> { gen.addWriteU32(R1) } + assertFailsWith<IllegalInstructionException> { gen.addPacketCopyFromR0LenR1() } + assertFailsWith<IllegalInstructionException> { gen.addDataCopyFromR0LenR1() } + assertFailsWith<IllegalInstructionException> { gen.addPacketCopyFromR0(10) } + assertFailsWith<IllegalInstructionException> { gen.addDataCopyFromR0(10) } + assertFailsWith<IllegalInstructionException> { + gen.addJumpIfBytesAtR0Equal(byteArrayOf('A'.code.toByte()), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) } } @Test - fun testApfInstructionArgumentCheck() { + fun testDataInstructionMustComeFirst() { var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) - assertFailsWith<IllegalArgumentException> { gen.addCountAndPass(0) } - assertFailsWith<IllegalArgumentException> { gen.addCountAndDrop(0) } + gen.addAllocateR0() + assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) } + } + + @Test + fun testApfInstructionEncodingSizeCheck() { + var gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) } + assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) } + assertFailsWith<IllegalArgumentException> { gen.addDataCopy(-1, 1) } + assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(-1, 1) } + assertFailsWith<IllegalArgumentException> { gen.addDataCopy(1, 256) } + assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(1, 256) } + assertFailsWith<IllegalArgumentException> { gen.addDataCopy(1, -1) } + assertFailsWith<IllegalArgumentException> { gen.addPacketCopy(1, -1) } + assertFailsWith<IllegalArgumentException> { gen.addPacketCopyFromR0(256) } + assertFailsWith<IllegalArgumentException> { gen.addDataCopyFromR0(256) } + assertFailsWith<IllegalArgumentException> { gen.addPacketCopyFromR0(-1) } + assertFailsWith<IllegalArgumentException> { gen.addDataCopyFromR0(-1) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, 'A'.code.toByte(), 0, 0), 256, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, 'a'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, '.'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(0, 0), 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, 'A'.code.toByte()), 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0), + 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0), + 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()), + 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, 'A'.code.toByte(), 0, 0), 256, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, 'a'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, '.'.code.toByte(), 0, 0), 0x0c, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(0, 0), 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, 'A'.code.toByte()), 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0), + 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0), + 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsQ( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()), + 0xc0, ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(1, 'a'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(1, '.'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(0, 0), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0), + ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0), + ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()), + ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(1, 'a'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(1, '.'.code.toByte(), 0, 0), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(0, 0), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(1, 'A'.code.toByte()), ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(64) + ByteArray(64) { 'A'.code.toByte() } + byteArrayOf(0, 0), + ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0), + ApfGenerator.DROP_LABEL) } + assertFailsWith<IllegalArgumentException> { gen.addJumpIfPktAtR0ContainDnsA( + byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte()), + ApfGenerator.DROP_LABEL) } } @Test @@ -81,10 +200,18 @@ class ApfV5Test { 0x03, 0xe8.toByte()), program) gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) - gen.addAlloc(ApfGenerator.Register.R0) + gen.addAllocateR0() + gen.addAllocate(1500) program = gen.generate() - assertContentEquals(byteArrayOf(encodeInstruction(21, 1, 0), 36), program) - assertContentEquals(arrayOf(" 0: alloc r0"), ApfJniUtils.disassembleApf(program)) + // encoding ALLOC opcode: opcode=21(EXT opcode number), imm=36(TRANS opcode number). + // R=0 means length stored in R0. R=1 means the length stored in imm1. + assertContentEquals(byteArrayOf( + encodeInstruction(opcode = 21, immLength = 1, register = 0), 36, + encodeInstruction(opcode = 21, immLength = 1, register = 1), 36, 0x05, + 0xDC.toByte()), + program) + // TODO: add back disassembling test check after we update the apf_disassembler + // assertContentEquals(arrayOf(" 0: alloc"), ApfJniUtils.disassembleApf(program)) gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) gen.addTransmit() @@ -100,63 +227,134 @@ class ApfV5Test { // TODO: add back disassembling test check after we update the apf_disassembler // assertContentEquals(arrayOf(" 0: trans"), ApfJniUtils.disassembleApf(program)) - // TODO: add back when support write opcode -// gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) -// gen.addWrite(0x01, 1) -// gen.addWrite(0x0102, 2) -// gen.addWrite(0x01020304, 4) -// program = gen.generate() -// assertContentEquals(byteArrayOf( -// encodeInstruction(24, 1, 0), 0x01, -// encodeInstruction(24, 2, 0), 0x01, 0x02, -// encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04 -// ), program) -// assertContentEquals(arrayOf( -// " 0: write 0x01", -// " 2: write 0x0102", -// " 5: write 0x01020304"), ApfJniUtils.disassembleApf(program)) -// -// gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) -// gen.addWrite(ApfGenerator.Register.R0, 1) -// gen.addWrite(ApfGenerator.Register.R0, 2) -// gen.addWrite(ApfGenerator.Register.R0, 4) -// program = gen.generate() -// assertContentEquals(byteArrayOf( -// encodeInstruction(21, 1, 0), 38, -// encodeInstruction(21, 1, 0), 39, -// encodeInstruction(21, 1, 0), 40 -// ), program) + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + val largeByteArray = ByteArray(256) { 0x01 } + gen.addData(largeByteArray) + program = gen.generate() + // encoding DATA opcode: opcode=14(JMP), R=1 + assertContentEquals(byteArrayOf( + encodeInstruction(opcode = 14, immLength = 2, register = 1), 0x01, 0x00) + + largeByteArray, program) + + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addWriteU8(0x01) + gen.addWriteU16(0x0102) + gen.addWriteU32(0x01020304) + gen.addWriteU8(0x00) + gen.addWriteU8(0x80) + gen.addWriteU16(0x0000) + gen.addWriteU16(0x8000) + gen.addWriteU32(0x00000000) + gen.addWriteU32(0x80000000) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(24, 1, 0), 0x01, + encodeInstruction(24, 2, 0), 0x01, 0x02, + encodeInstruction(24, 4, 0), 0x01, 0x02, 0x03, 0x04, + encodeInstruction(24, 1, 0), 0x00, + encodeInstruction(24, 1, 0), 0x80.toByte(), + encodeInstruction(24, 2, 0), 0x00, 0x00, + encodeInstruction(24, 2, 0), 0x80.toByte(), 0x00, + encodeInstruction(24, 4, 0), 0x00, 0x00, 0x00, 0x00, + encodeInstruction(24, 4, 0), 0x80.toByte(), 0x00, 0x00, + 0x00), program) + assertContentEquals(arrayOf( + " 0: write 0x01", + " 2: write 0x0102", + " 5: write 0x01020304", + " 10: write 0x00", + " 12: write 0x80", + " 14: write 0x0000", + " 17: write 0x8000", + " 20: write 0x00000000", + " 25: write 0x80000000"), + ApfJniUtils.disassembleApf(program)) + + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addWriteU8(R0) + gen.addWriteU16(R0) + gen.addWriteU32(R0) + gen.addWriteU8(R1) + gen.addWriteU16(R1) + gen.addWriteU32(R1) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(21, 1, 0), 38, + encodeInstruction(21, 1, 0), 39, + encodeInstruction(21, 1, 0), 40, + encodeInstruction(21, 1, 1), 38, + encodeInstruction(21, 1, 1), 39, + encodeInstruction(21, 1, 1), 40 + ), program) + // TODO: add back disassembling test check after we update the apf_disassembler // assertContentEquals(arrayOf( -// " 0: write r0, 1", -// " 2: write r0, 2", -// " 4: write r0, 4"), ApfJniUtils.disassembleApf(program)) - - // TODO: add back when we properly support copy opcode -// gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) -// gen.addDataCopy(1, 5) -// gen.addPacketCopy(1000, 255) -// program = gen.generate() -// assertContentEquals(byteArrayOf( -// encodeInstruction(25, 1, 1), 1, 5, -// encodeInstruction(25, 2, 0), -// 0x03.toByte(), 0xe8.toByte(), 0xff.toByte(), -// ), program) +// " 0: ewrite1 r0", +// " 2: ewrite2 r0", +// " 4: ewrite4 r0", +// " 6: ewrite1 r1", +// " 8: ewrite2 r1", +// " 10: ewrite4 r1"), ApfJniUtils.disassembleApf(program)) + + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addDataCopy(0, 10) + gen.addDataCopy(1, 5) + gen.addPacketCopy(1000, 255) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(25, 0, 1), 10, + encodeInstruction(25, 1, 1), 1, 5, + encodeInstruction(25, 2, 0), + 0x03.toByte(), 0xe8.toByte(), 0xff.toByte(), + ), program) + // TODO: add back disassembling test check after we update the apf_disassembler // assertContentEquals(arrayOf( -// " 0: dcopy 1, 5", +// " 0: dcopy 0, 5", // " 3: pcopy 1000, 255"), ApfJniUtils.disassembleApf(program)) -// -// gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) -// gen.addDataCopy(ApfGenerator.Register.R1, 0, 5) -// gen.addPacketCopy(ApfGenerator.Register.R0, 1000, 255) -// program = gen.generate() -// assertContentEquals(byteArrayOf( -// encodeInstruction(21, 1, 1), 42, 0, 5, -// encodeInstruction(21, 2, 0), -// 0, 41, 0x03.toByte(), 0xe8.toByte(), 0xff.toByte() -// ), program) + + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addPacketCopyFromR0LenR1() + gen.addPacketCopyFromR0(5) + gen.addDataCopyFromR0LenR1() + gen.addDataCopyFromR0(5) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(21, 1, 1), 41, + encodeInstruction(21, 1, 0), 41, 5, + encodeInstruction(21, 1, 1), 42, + encodeInstruction(21, 1, 0), 42, 5, + ), program) + // TODO: add back the following test case when implementing EPKTCOPY, EDATACOPY opcodes. // assertContentEquals(arrayOf( // " 0: dcopy [r1+0], 5", // " 4: pcopy [r0+1000], 255"), ApfJniUtils.disassembleApf(program)) + + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addJumpIfBytesAtR0Equal(byteArrayOf('a'.code.toByte()), ApfGenerator.DROP_LABEL) + program = gen.generate() + assertContentEquals( + byteArrayOf(encodeInstruction(opcode = 20, immLength = 1, register = 1), + 1, 1, 'a'.code.toByte()), program) + + val qnames = byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0, 0) + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addJumpIfPktAtR0DoesNotContainDnsQ(qnames, 0x0c, ApfGenerator.DROP_LABEL) + gen.addJumpIfPktAtR0ContainDnsQ(qnames, 0x0c, ApfGenerator.DROP_LABEL) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(21, 1, 0), 43, 11, 0x0c.toByte(), + ) + qnames + byteArrayOf( + encodeInstruction(21, 1, 1), 43, 1, 0x0c.toByte(), + ) + qnames, program) + + gen = ApfGenerator(ApfGenerator.MIN_APF_VERSION_IN_DEV) + gen.addJumpIfPktAtR0DoesNotContainDnsA(qnames, ApfGenerator.DROP_LABEL) + gen.addJumpIfPktAtR0ContainDnsA(qnames, ApfGenerator.DROP_LABEL) + program = gen.generate() + assertContentEquals(byteArrayOf( + encodeInstruction(21, 1, 0), 44, 10, + ) + qnames + byteArrayOf( + encodeInstruction(21, 1, 1), 44, 1, + ) + qnames, program) } private fun encodeInstruction(opcode: Int, immLength: Int, register: Int): Byte { diff --git a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt index b5e806f9..32cf4647 100644 --- a/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt +++ b/tests/unit/src/android/net/dhcp6/Dhcp6PacketTest.kt @@ -220,4 +220,168 @@ class Dhcp6PacketTest { assertEquals(423, packet.prefixDelegation.minimalPreferredLifetime) assertEquals(43200, packet.prefixDelegation.minimalValidLifetime) } + + @Test + fun testStatusCodeOptionWithStatusMessage() { + val replyHex = + // Reply, Transaction ID + "07000A47" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B02FBBAFFFEB7BC71" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003c" + + // Rapid Commit + "000e0000" + + // DNS recursive server (don't support this opton yet) + "00170010fdfd9ed6795000000000000000000001" + + // IA_PD option (t1=t2=0, empty prefix) + "0019000c000000000000000000000000" + + // Status code option: status code=NoPrefixAvail + "000d00150006" + + // Status code option: status message="no prefix available" + "6e6f2070726566697820617661696c61626c65" + val bytes = HexDump.hexStringToByteArray(replyHex) + val packet = Dhcp6Packet.decode(bytes, bytes.size) + assertTrue(packet is Dhcp6ReplyPacket) + assertEquals(0, packet.mPrefixDelegation.iaid) + assertEquals(0, packet.mPrefixDelegation.t1) + assertEquals(0, packet.mPrefixDelegation.t2) + assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mStatusCode) + } + + @Test + fun testStatusCodeOptionWithoutStatusMessage() { + val replyHex = + // Reply, Transaction ID + "07000A47" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B02FBBAFFFEB7BC71" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003c" + + // Rapid Commit + "000e0000" + + // DNS recursive server (don't support this opton yet) + "00170010fdfd9ed6795000000000000000000001" + + // IA_PD option (t1=t2=0, empty prefix) + "0019000c000000000000000000000000" + + // Status code option: status code=NoPrefixAvail + "000d00020006" + val bytes = HexDump.hexStringToByteArray(replyHex) + val packet = Dhcp6Packet.decode(bytes, bytes.size) + assertTrue(packet is Dhcp6ReplyPacket) + assertEquals(0, packet.mPrefixDelegation.iaid) + assertEquals(0, packet.mPrefixDelegation.t1) + assertEquals(0, packet.mPrefixDelegation.t2) + assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mStatusCode) + } + + @Test + fun testStatusCodeOptionInIaPdWithStatusMessage() { + val replyHex = + // Reply, Transaction ID + "07000A47" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B02FBBAFFFEB7BC71" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003c" + + // Rapid Commit + "000e0000" + + // DNS recursive server (don't support this opton yet) + "00170010fdfd9ed6795000000000000000000001" + + // IA_PD option (t1=t2=0, status code=NoPrefixAvail, + // status message="no prefix available") + "00190025000000000000000000000000000d00150006" + + "6e6f2070726566697820617661696c61626c65" + val bytes = HexDump.hexStringToByteArray(replyHex) + val packet = Dhcp6Packet.decode(bytes, bytes.size) + assertTrue(packet is Dhcp6ReplyPacket) + assertEquals(0, packet.mPrefixDelegation.iaid) + assertEquals(0, packet.mPrefixDelegation.t1) + assertEquals(0, packet.mPrefixDelegation.t2) + assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mPrefixDelegation.statusCode) + } + + @Test + fun testStatusCodeOptionInIaPdWithoutStatusMessage() { + val replyHex = + // Reply, Transaction ID + "07000A47" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B02FBBAFFFEB7BC71" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003c" + + // Rapid Commit + "000e0000" + + // DNS recursive server (don't support this opton yet) + "00170010fdfd9ed6795000000000000000000001" + + // IA_PD option (t1=t2=0, status code=NoPrefixAvail) + "00190012000000000000000000000000000d00020006" + val bytes = HexDump.hexStringToByteArray(replyHex) + val packet = Dhcp6Packet.decode(bytes, bytes.size) + assertTrue(packet is Dhcp6ReplyPacket) + assertEquals(0, packet.mPrefixDelegation.iaid) + assertEquals(0, packet.mPrefixDelegation.t1) + assertEquals(0, packet.mPrefixDelegation.t2) + assertEquals(Dhcp6Packet.STATUS_NO_PREFIX_AVAIL, packet.mPrefixDelegation.statusCode) + } + + @Test + fun testStatusCodeOptionWithTruncatedStatusMessage() { + val replyHex = + // Reply, Transaction ID + "07000A47" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B02FBBAFFFEB7BC71" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003c" + + // Rapid Commit + "000e0000" + + // DNS recursive server (don't support this opton yet) + "00170010fdfd9ed6795000000000000000000001" + + // Status code option: len=21, status code=NoPrefixAvail + "000d00150006" + + // Status code option: truncated status message="no prefix available" + "6e6f2070726566697820617661696c6162" + val bytes = HexDump.hexStringToByteArray(replyHex) + assertThrows(Dhcp6Packet.ParseException::class.java) { + Dhcp6Packet.decode(bytes, bytes.size) + } + } + + @Test + fun testStatusCodeOptionInIaPdWithTruncatedStatusMessage() { + val replyHex = + // Reply, Transaction ID + "07000A47" + + // server identifier option(option_len=10) + "0002000A0003000186C9B26AED4D" + + // client identifier option(option_len=12) + "0001000C0003001B02FBBAFFFEB7BC71" + + // SOL_MAX_RT (don't support this option yet) + "005200040000003c" + + // Rapid Commit + "000e0000" + + // DNS recursive server (don't support this opton yet) + "00170010fdfd9ed6795000000000000000000001" + + // IA_PD option (t1=t2=0, empty prefix) + "00190025000000000000000000000000" + + // Status code option: len=21, status code=NoPrefixAvail + "000d00150006" + + // truncated status message="no prefix available") + "6e6f2070726566697820617661696c6162" + val bytes = HexDump.hexStringToByteArray(replyHex) + assertThrows(Dhcp6Packet.ParseException::class.java) { + Dhcp6Packet.decode(bytes, bytes.size) + } + } } diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt index 6471f3ae..4d57df5e 100644 --- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt +++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt @@ -56,7 +56,6 @@ import com.android.net.module.util.netlink.StructNdMsg.NUD_FAILED import com.android.net.module.util.netlink.StructNdMsg.NUD_REACHABLE import com.android.net.module.util.netlink.StructNdMsg.NUD_STALE import com.android.networkstack.metrics.IpReachabilityMonitorMetrics -import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION import com.android.testutils.makeNewNeighMessage import com.android.testutils.waitForIdle import java.io.FileDescriptor @@ -259,8 +258,6 @@ class IpReachabilityMonitorTest { }.`when`(dependencies).makeIpNeighborMonitor(any(), any(), any()) doReturn(mIpReachabilityMonitorMetrics) .`when`(dependencies).getIpReachabilityMonitorMetrics() - doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(), - eq(IP_REACHABILITY_MCAST_RESOLICIT_VERSION)) val monitorFuture = CompletableFuture<IpReachabilityMonitor>() // IpReachabilityMonitor needs to be started from the handler thread diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java index 77e3a129..43bee559 100644 --- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java @@ -19,6 +19,7 @@ package com.android.server.connectivity; import static android.content.Intent.ACTION_CONFIGURATION_CHANGED; import static android.net.CaptivePortal.APP_RETURN_DISMISSED; import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS; +import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; import static android.net.DnsResolver.TYPE_A; import static android.net.DnsResolver.TYPE_AAAA; import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS; @@ -29,6 +30,7 @@ import static android.net.INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS; import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_PARTIAL; import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_SKIPPED; import static android.net.INetworkMonitor.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.InetAddresses.parseNumericAddress; import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; @@ -64,6 +66,7 @@ import static com.android.networkstack.util.NetworkStackUtils.CAPTIVE_PORTAL_USE import static com.android.networkstack.util.NetworkStackUtils.DEFAULT_CAPTIVE_PORTAL_DNS_PROBE_TIMEOUT; import static com.android.networkstack.util.NetworkStackUtils.DNS_PROBE_PRIVATE_IP_NO_INTERNET_VERSION; import static com.android.networkstack.util.NetworkStackUtils.REEVALUATE_WHEN_RESUME; +import static com.android.server.connectivity.NetworkMonitor.CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS; import static com.android.server.connectivity.NetworkMonitor.INITIAL_REEVALUATE_DELAY_MS; import static com.android.server.connectivity.NetworkMonitor.extractCharset; @@ -123,6 +126,7 @@ import android.net.Network; import android.net.NetworkAgentConfig; import android.net.NetworkCapabilities; import android.net.NetworkTestResultParcelable; +import android.net.PrivateDnsConfigParcel; import android.net.Uri; import android.net.captiveportal.CaptivePortalProbeResult; import android.net.metrics.IpConnectivityLog; @@ -168,6 +172,7 @@ import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; +import com.android.networkstack.util.NetworkStackUtils; import com.android.server.NetworkStackService.NetworkStackServiceManager; import com.android.server.connectivity.nano.CellularData; import com.android.server.connectivity.nano.DnsEvent; @@ -214,8 +219,12 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.net.ssl.SSLHandshakeException; @@ -366,11 +375,11 @@ public class NetworkMonitorTest { class DnsEntry { final String mHostname; final int mType; - final List<InetAddress> mAddresses; - DnsEntry(String host, int type, List<InetAddress> addr) { + final AddressSupplier mAddressesSupplier; + DnsEntry(String host, int type, AddressSupplier addr) { mHostname = host; mType = type; - mAddresses = addr; + mAddressesSupplier = addr; } // Full match or partial match that target host contains the entry hostname to support // random private dns probe hostname. @@ -378,6 +387,21 @@ public class NetworkMonitorTest { return hostname.endsWith(mHostname) && type == mType; } } + interface AddressSupplier { + List<InetAddress> get() throws DnsResolver.DnsException; + } + + class InstantAddressSupplier implements AddressSupplier { + private final List<InetAddress> mAddresses; + InstantAddressSupplier(List<InetAddress> addresses) { + mAddresses = addresses; + } + @Override + public List<InetAddress> get() { + return mAddresses; + } + } + private final ArrayList<DnsEntry> mAnswers = new ArrayList<DnsEntry>(); private boolean mNonBypassPrivateDnsWorking = true; @@ -387,40 +411,76 @@ public class NetworkMonitorTest { } /** Clears all DNS entries. */ - private synchronized void clearAll() { - mAnswers.clear(); + private void clearAll() { + synchronized (mAnswers) { + mAnswers.clear(); + } } /** Returns the answer for a given name and type on the given mock network. */ - private synchronized List<InetAddress> getAnswer(Object mock, String hostname, int type) { - if (mock == mNetwork && !mNonBypassPrivateDnsWorking) { - return null; + private CompletableFuture<List<InetAddress>> getAnswer(Network mockNetwork, String hostname, + int type) { + if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) { + return CompletableFuture.completedFuture(null); } - return mAnswers.stream().filter(e -> e.matches(hostname, type)) - .map(answer -> answer.mAddresses).findFirst().orElse(null); + final AddressSupplier answerSupplier; + + synchronized (mAnswers) { + answerSupplier = mAnswers.stream() + .filter(e -> e.matches(hostname, type)) + .map(answer -> answer.mAddressesSupplier).findFirst().orElse(null); + } + if (answerSupplier == null) { + return CompletableFuture.completedFuture(null); + } + + if (answerSupplier instanceof InstantAddressSupplier) { + // Save latency waiting for a query thread if the answer is hardcoded. + return CompletableFuture.completedFuture( + ((InstantAddressSupplier) answerSupplier).get()); + } + final CompletableFuture<List<InetAddress>> answerFuture = new CompletableFuture<>(); + new Thread(() -> { + try { + answerFuture.complete(answerSupplier.get()); + } catch (DnsResolver.DnsException e) { + answerFuture.completeExceptionally(e); + } + }).start(); + return answerFuture; } /** Sets the answer for a given name and type. */ - private synchronized void setAnswer(String hostname, String[] answer, int type) - throws UnknownHostException { - DnsEntry record = new DnsEntry(hostname, type, generateAnswer(answer)); - // Remove the existing one. - mAnswers.removeIf(entry -> entry.matches(hostname, type)); - // Add or replace a new record. - mAnswers.add(record); + private void setAnswer(String hostname, String[] answer, int type) { + setAnswer(hostname, new InstantAddressSupplier(generateAnswer(answer)), type); + } + + private void setAnswer(String hostname, AddressSupplier answerSupplier, int type) { + DnsEntry record = new DnsEntry(hostname, type, answerSupplier); + synchronized (mAnswers) { + // Remove the existing one. + mAnswers.removeIf(entry -> entry.matches(hostname, type)); + // Add or replace a new record. + mAnswers.add(record); + } } private List<InetAddress> generateAnswer(String[] answer) { if (answer == null) return new ArrayList<>(); - return Arrays.stream(answer).map(addr -> InetAddress.parseNumericAddress(addr)) - .collect(toList()); + return Arrays.stream(answer).map(InetAddresses::parseNumericAddress).collect(toList()); } /** Simulates a getAllByName call for the specified name on the specified mock network. */ - private InetAddress[] getAllByName(Object mock, String hostname) + private InetAddress[] getAllByName(Network mockNetwork, String hostname) throws UnknownHostException { - List<InetAddress> answer = queryAllTypes(mock, hostname); + final List<InetAddress> answer; + try { + answer = queryAllTypes(mockNetwork, hostname).get( + HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (ExecutionException | InterruptedException | TimeoutException e) { + throw new AssertionError("No mock DNS reply within timeout", e); + } if (answer == null || answer.size() == 0) { throw new UnknownHostException(hostname); } @@ -428,57 +488,76 @@ public class NetworkMonitorTest { } // Regardless of the type, depends on what the responses contained in the network. - private List<InetAddress> queryAllTypes(Object mock, String hostname) { - List<InetAddress> answer = new ArrayList<>(); - addAllIfNotNull(answer, getAnswer(mock, hostname, TYPE_A)); - addAllIfNotNull(answer, getAnswer(mock, hostname, TYPE_AAAA)); - return answer; - } - - private void addAllIfNotNull(List<InetAddress> list, List<InetAddress> c) { - if (c != null) { - list.addAll(c); + private CompletableFuture<List<InetAddress>> queryAllTypes( + Network mockNetwork, String hostname) { + if (mockNetwork == mNetwork && !mNonBypassPrivateDnsWorking) { + return CompletableFuture.completedFuture(null); } + + final CompletableFuture<List<InetAddress>> aFuture = + getAnswer(mockNetwork, hostname, TYPE_A) + .exceptionally(e -> Collections.emptyList()); + final CompletableFuture<List<InetAddress>> aaaaFuture = + getAnswer(mockNetwork, hostname, TYPE_AAAA) + .exceptionally(e -> Collections.emptyList()); + + final CompletableFuture<List<InetAddress>> combinedFuture = new CompletableFuture<>(); + aFuture.thenAcceptBoth(aaaaFuture, (res1, res2) -> { + final List<InetAddress> answer = new ArrayList<>(); + if (res1 != null) answer.addAll(res1); + if (res2 != null) answer.addAll(res2); + combinedFuture.complete(answer); + }); + return combinedFuture; } /** Starts mocking DNS queries. */ private void startMocking() throws UnknownHostException { // Queries on mNetwork using getAllByName. doAnswer(invocation -> { - return getAllByName(invocation.getMock(), invocation.getArgument(0)); + return getAllByName((Network) invocation.getMock(), invocation.getArgument(0)); }).when(mNetwork).getAllByName(any()); // Queries on mCleartextDnsNetwork using DnsResolver#query. doAnswer(invocation -> { - return mockQuery(invocation, 1 /* posHostname */, 3 /* posExecutor */, - 5 /* posCallback */, -1 /* posType */); + return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */, + 3 /* posExecutor */, 5 /* posCallback */, -1 /* posType */); }).when(mDnsResolver).query(any(), any(), anyInt(), any(), any(), any()); // Queries on mCleartextDnsNetwork using DnsResolver#query with QueryType. doAnswer(invocation -> { - return mockQuery(invocation, 1 /* posHostname */, 4 /* posExecutor */, - 6 /* posCallback */, 2 /* posType */); + return mockQuery(invocation, 0 /* posNetwork */, 1 /* posHostname */, + 4 /* posExecutor */, 6 /* posCallback */, 2 /* posType */); }).when(mDnsResolver).query(any(), any(), anyInt(), anyInt(), any(), any(), any()); } // Mocking queries on DnsResolver#query. - private Answer mockQuery(InvocationOnMock invocation, int posHostname, int posExecutor, - int posCallback, int posType) { + private Answer mockQuery(InvocationOnMock invocation, int posNetwork, int posHostname, + int posExecutor, int posCallback, int posType) { String hostname = (String) invocation.getArgument(posHostname); Executor executor = (Executor) invocation.getArgument(posExecutor); DnsResolver.Callback<List<InetAddress>> callback = invocation.getArgument(posCallback); - List<InetAddress> answer; - - answer = posType != -1 - ? getAnswer(invocation.getMock(), hostname, invocation.getArgument(posType)) : - queryAllTypes(invocation.getMock(), hostname); - - if (answer != null && answer.size() > 0) { - new Handler(Looper.getMainLooper()).post(() -> { - executor.execute(() -> callback.onAnswer(answer, 0)); - }); - } - // If no answers, do nothing. sendDnsProbeWithTimeout will time out and throw UHE. + Network network = invocation.getArgument(posNetwork); + + final CompletableFuture<List<InetAddress>> answerFuture = posType != -1 + ? getAnswer(network, hostname, invocation.getArgument(posType)) + : queryAllTypes(network, hostname); + + answerFuture.whenComplete((answer, exception) -> { + new Handler(Looper.getMainLooper()).post(() -> executor.execute(() -> { + if (exception != null) { + if (!(exception instanceof DnsResolver.DnsException)) { + throw new AssertionError("Test error building DNS response", exception); + } + callback.onError((DnsResolver.DnsException) exception); + return; + } + if (answer != null && answer.size() > 0) { + callback.onAnswer(answer, 0); + } + })); + }); + // If the future does not complete or has no answer do nothing. The timeout should fire. return null; } } @@ -528,6 +607,8 @@ public class NetworkMonitorTest { // it will fail the test because of timeout expired for querying AAAA and A sequentially. doReturn(200).when(mResources) .getInteger(eq(R.integer.config_captive_portal_dns_probe_timeout)); + doReturn(200).when(mDependencies).getDeviceConfigPropertyInt( + eq(NAMESPACE_CONNECTIVITY), eq(CONFIG_ASYNC_PRIVDNS_PROBE_TIMEOUT_MS), anyInt()); doAnswer((invocation) -> { URL url = invocation.getArgument(0); @@ -1239,7 +1320,7 @@ public class NetworkMonitorTest { setPortal302(mHttpConnection); final String httpHost = new URL(TEST_HTTP_URL).getHost(); mFakeDns.setAnswer(httpHost, new String[] { "2001:db8::123" }, TYPE_AAAA); - final InetAddress parsedPrivateAddr = InetAddresses.parseNumericAddress(privateAddr); + final InetAddress parsedPrivateAddr = parseNumericAddress(privateAddr); mFakeDns.setAnswer(httpHost, new String[] { privateAddr }, (parsedPrivateAddr instanceof Inet6Address) ? TYPE_AAAA : TYPE_A); } @@ -2199,8 +2280,7 @@ public class NetworkMonitorTest { NETWORK_VALIDATION_RESULT_VALID, 0 /* probesSucceeded */)); } - @Test - public void testPrivateDnsSuccess() throws Exception { + private void runPrivateDnsSuccessTest() throws Exception { setStatus(mHttpsConnection, 204); setStatus(mHttpConnection, 204); @@ -2232,7 +2312,20 @@ public class NetworkMonitorTest { } @Test - public void testProbeStatusChanged() throws Exception { + public void testPrivateDnsSuccess_SyncDns() throws Exception { + doReturn(false).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + runPrivateDnsSuccessTest(); + } + + @Test + public void testPrivateDnsSuccess_AsyncDns() throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + runPrivateDnsSuccessTest(); + } + + private void runProbeStatusChangedTest() throws Exception { // Set no record in FakeDns and expect validation to fail. setStatus(mHttpsConnection, 204); setStatus(mHttpConnection, 204); @@ -2255,7 +2348,20 @@ public class NetworkMonitorTest { } @Test - public void testPrivateDnsResolutionRetryUpdate() throws Exception { + public void testProbeStatusChanged_SyncDns() throws Exception { + doReturn(false).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + runProbeStatusChangedTest(); + } + + @Test + public void testProbeStatusChanged_AsyncDns() throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + runProbeStatusChangedTest(); + } + + private void runPrivateDnsResolutionRetryUpdateTest() throws Exception { // Set no record in FakeDns and expect validation to fail. setStatus(mHttpsConnection, 204); setStatus(mHttpConnection, 204); @@ -2300,6 +2406,197 @@ public class NetworkMonitorTest { } @Test + public void testPrivateDnsResolutionRetryUpdate_SyncDns() throws Exception { + doReturn(false).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + runPrivateDnsResolutionRetryUpdateTest(); + } + + @Test + public void testPrivateDnsResolutionRetryUpdate_AsyncDns() throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + runPrivateDnsResolutionRetryUpdateTest(); + } + + @Test + public void testAsyncPrivateDnsResolution_PartialTimeout() throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + + WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0])); + + // Only provide AAAA answer + mFakeDns.setAnswer("dns.google", new String[]{"2001:db8::1"}, TYPE_AAAA); + + notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES); + verifyNetworkTestedValidFromPrivateDns(1 /* interactions */); + + final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel(); + expectedConfig.hostname = "dns.google"; + expectedConfig.ips = new String[] {"2001:db8::1"}; + expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; + + verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig); + } + + @Test + public void testAsyncPrivateDnsResolution_PartialFailure() throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + + WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0])); + + // A succeeds, AAAA fails + mFakeDns.setAnswer("dns.google", new String[]{"192.0.2.123"}, TYPE_A); + mFakeDns.setAnswer("dns.google", () -> { + // DnsResolver.DnsException constructor is T+, so use a mock instead + throw mock(DnsResolver.DnsException.class); + }, TYPE_AAAA); + + notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES); + verifyNetworkTestedValidFromPrivateDns(1 /* interactions */); + + final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel(); + expectedConfig.hostname = "dns.google"; + expectedConfig.ips = new String[] {"192.0.2.123"}; + expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; + + verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig); + } + + @Test + public void testAsyncPrivateDnsResolution_AQuerySucceedsFirst_PrioritizeAAAA() + throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + + WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("dns.google", new InetAddress[0])); + + final ConditionVariable v4Queried = new ConditionVariable(); + mFakeDns.setAnswer("dns.google", () -> { + v4Queried.open(); + return List.of(parseNumericAddress("192.0.2.123")); + }, TYPE_A); + mFakeDns.setAnswer("dns.google", () -> { + // Make sure the v6 query processing is a bit slower than the v6 one. The small delay + // below still does not guarantee that the v4 query will complete first, but it should + // the large majority of the time, which should be enough to test it. Even if it does + // not, the test should pass. + v4Queried.block(HANDLER_TIMEOUT_MS); + SystemClock.sleep(10L); + return List.of(parseNumericAddress("2001:db8::1"), parseNumericAddress("2001:db8::2")); + }, TYPE_AAAA); + + notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES); + verifyNetworkTestedValidFromPrivateDns(1 /* interactions */); + + final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel(); + expectedConfig.hostname = "dns.google"; + // The IPv6 addresses are still first + expectedConfig.ips = new String[] {"2001:db8::1", "2001:db8::2", "192.0.2.123"}; + expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; + + verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig); + } + + @Test + public void testAsyncPrivateDnsResolution_ConfigChange_RestartsWithNewConfig() + throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + + WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("v1.google", new InetAddress[0])); + + final ConditionVariable blockReplies = new ConditionVariable(); + final CountDownLatch queriedLatch = new CountDownLatch(2); + mFakeDns.setAnswer("v1.google", () -> { + queriedLatch.countDown(); + blockReplies.block(HANDLER_TIMEOUT_MS); + return List.of(parseNumericAddress("192.0.2.123")); + }, TYPE_A); + mFakeDns.setAnswer("v1.google", () -> { + queriedLatch.countDown(); + blockReplies.block(HANDLER_TIMEOUT_MS); + return List.of(parseNumericAddress("2001:db8::1")); + }, TYPE_AAAA); + + notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES); + + queriedLatch.await(HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS); + + // Send config update while DNS queries are in flight + mFakeDns.setAnswer("v2.google", new String[] { "192.0.2.124" }, TYPE_A); + mFakeDns.setAnswer("v2.google", new String[] { "2001:db8::2" }, TYPE_AAAA); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("v2.google", new InetAddress[0])); + + // Let the original queries finish. Once DNS queries finish results are posted to the + // handler, so they will be processed on the handler after the DNS settings change. + blockReplies.open(); + + // Expect only callbacks for the 2nd configuration + verifyNetworkTestedValidFromPrivateDns(1 /* interactions */); + + final PrivateDnsConfigParcel expectedConfig = new PrivateDnsConfigParcel(); + expectedConfig.hostname = "v2.google"; + expectedConfig.ips = new String[] {"2001:db8::2", "192.0.2.124"}; + expectedConfig.privateDnsMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; + + verify(mCallbacks).notifyPrivateDnsConfigResolved(expectedConfig); + } + + @Test + public void testAsyncPrivateDnsResolution_TurnOffStrictMode_SkipsDnsValidation() + throws Exception { + doReturn(true).when(mDependencies).isFeatureEnabled( + any(), eq(NetworkStackUtils.NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION)); + setStatus(mHttpsConnection, 204); + setStatus(mHttpConnection, 204); + + WrappedNetworkMonitor wnm = makeCellNotMeteredNetworkMonitor(); + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig("v1.google", new InetAddress[0])); + + final ConditionVariable blockReplies = new ConditionVariable(); + final CountDownLatch queriedLatch = new CountDownLatch(2); + mFakeDns.setAnswer("v1.google", () -> { + queriedLatch.countDown(); + blockReplies.block(HANDLER_TIMEOUT_MS); + return List.of(parseNumericAddress("192.0.2.123")); + }, TYPE_A); + mFakeDns.setAnswer("v1.google", () -> { + queriedLatch.countDown(); + blockReplies.block(HANDLER_TIMEOUT_MS); + return List.of(parseNumericAddress("2001:db8::1")); + }, TYPE_AAAA); + + notifyNetworkConnected(wnm, CELL_NOT_METERED_CAPABILITIES); + + queriedLatch.await(HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS); + + // Send config update while DNS queries are in flight + wnm.notifyPrivateDnsSettingsChanged(new PrivateDnsConfig(true /* useTls */)); + + // Let the original queries finish. Once DNS queries finish results are posted to the + // handler, so they will be processed on the handler after the DNS settings change. + blockReplies.open(); + + verifyNetworkTestedValidFromHttps(1 /* interactions */); + verify(mCallbacks, never()).notifyPrivateDnsConfigResolved(any()); + } + + @Test public void testReevaluationInterval_networkResume() throws Exception { // Setup nothing and expect validation to fail. doReturn(true).when(mDependencies).isFeatureEnabled(any(), eq(REEVALUATE_WHEN_RESUME)); @@ -2725,8 +3022,8 @@ public class NetworkMonitorTest { } catch (UnknownHostException e) { } - mFakeDns.setAnswer("www.android.com", null, TYPE_A); - mFakeDns.setAnswer("www.android.com", null, TYPE_AAAA); + mFakeDns.setAnswer("www.android.com", (String[]) null, TYPE_A); + mFakeDns.setAnswer("www.android.com", (String[]) null, TYPE_AAAA); try { wnm.sendDnsProbeWithTimeout("www.android.com", shortTimeoutMs); fail("DNS query timed out, expected UnknownHostException"); |