Snap for 11391017 from f2b16d3294f249f759e7e6ee2abe42917b6a7018 to mainline-media-release Change-Id: I62defc27ba6ac5abe9fb6c83d867a4f21ab434ed
diff --git a/src/android/net/apf/ApfV4Generator.java b/src/android/net/apf/ApfV4Generator.java index 970c887..bc48bef 100644 --- a/src/android/net/apf/ApfV4Generator.java +++ b/src/android/net/apf/ApfV4Generator.java
@@ -22,7 +22,6 @@ 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; @@ -45,7 +44,7 @@ super(msg); } } - private enum Opcodes { + enum Opcodes { LABEL(-1), // Unconditionally pass (if R=0) or drop (if R=1) packet. // An optional unsigned immediate value can be provided to encode the counter number. @@ -54,8 +53,7 @@ // It is a U32 big-endian value and is always incremented by 1. // This is more or less equivalent to: lddw R0, -N4; add R0,1; stdw R0, -N4; {pass,drop} // e.g. "pass", "pass 1", "drop", "drop 1" - PASS(0), - DROP(0), + PASSDROP(0), LDB(1), // Load 1 byte from immediate offset, e.g. "ldb R0, [5]" LDH(2), // Load 2 bytes from immediate offset, e.g. "ldh R0, [5]" LDW(3), // Load 4 bytes from immediate offset, e.g. "ldw R0, [5]" @@ -104,7 +102,7 @@ } // Extended opcodes. Primary opcode is Opcodes.EXT. ExtendedOpcodes are encoded in the immediate // field. - private enum ExtendedOpcodes { + enum ExtendedOpcodes { LDM(0), // Load from memory, e.g. "ldm R0,5" STM(16), // Store to memory, e.g. "stm R0,5" NOT(32), // Not, e.g. "not R0" @@ -118,10 +116,9 @@ 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. transmit" // "e.g. discard" - TRANSMIT(37), - DISCARD(37), + TRANSMITDISCARD(37), // Write 1, 2 or 4 byte value from register to the output buffer and auto-increment the // output buffer pointer. // e.g. "ewrite1 r0" @@ -297,7 +294,7 @@ } } - private class Instruction { + class Instruction { private final byte mOpcode; // A "Opcode" value. private final byte mRegister; // A "Register" value. public final List<IntImmediate> mIntImms = new ArrayList<>(); @@ -630,7 +627,7 @@ public static final int APF_VERSION_4 = 4; - private final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>(); + final ArrayList<Instruction> mInstructions = new ArrayList<Instruction>(); private final HashMap<String, Instruction> mLabels = new HashMap<String, Instruction>(); private final Instruction mDropLabel = new Instruction(Opcodes.LABEL); private final Instruction mPassLabel = new Instruction(Opcodes.LABEL); @@ -655,13 +652,13 @@ return version >= MIN_APF_VERSION; } - private void requireApfVersion(int minimumVersion) throws IllegalInstructionException { + void requireApfVersion(int minimumVersion) throws IllegalInstructionException { if (mVersion < minimumVersion) { throw new IllegalInstructionException("Requires APF >= " + minimumVersion); } } - private ApfV4Generator append(Instruction instruction) { + ApfV4Generator append(Instruction instruction) { if (mGenerated) { throw new IllegalStateException("Program already generated"); } @@ -1003,322 +1000,11 @@ */ public ApfV4Generator addPass() { // PASS requires using R0 because it shares opcode with DROP - return append(new Instruction(Opcodes.PASS)); + return append(new Instruction(Opcodes.PASSDROP)); } - /** - * Add an instruction to the end of the program to increment the counter value and - * immediately return PASS. - */ - public ApfV4Generator addCountAndPass(int cnt) throws IllegalInstructionException { - requireApfVersion(MIN_APF_VERSION_IN_DEV); - 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)); - } - - /** - * Add an instruction to the end of the program to let the program immediately return DROP. - */ - public ApfV4Generator addDrop() throws IllegalInstructionException { - requireApfVersion(MIN_APF_VERSION_IN_DEV); - // 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 ApfV4Generator addCountAndDrop(int cnt) throws IllegalInstructionException { - requireApfVersion(MIN_APF_VERSION_IN_DEV); - 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 ApfV4Generator 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 size the buffer length to be allocated. - */ - public ApfV4Generator 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 ApfV4Generator 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)); - } - - /** - * Add an instruction to the end of the program to transmit the allocated buffer. - */ - public ApfV4Generator addTransmit() throws IllegalInstructionException { - requireApfVersion(MIN_APF_VERSION_IN_DEV); - // TRANSMIT requires using R0 because it shares opcode with DISCARD - return append(new Instruction(ExtendedOpcodes.TRANSMIT)); - } - - /** - * Add an instruction to the end of the program to discard the allocated buffer. - */ - public ApfV4Generator addDiscard() throws IllegalInstructionException { - requireApfVersion(MIN_APF_VERSION_IN_DEV); - // DISCARD requires using R1 because it shares opcode with TRANSMIT - return append(new Instruction(ExtendedOpcodes.DISCARD, R1)); - } - - /** - * Add an instruction to the end of the program to write 1 byte value to output buffer. - */ - public ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator addPacketCopy(int src, int len) - throws IllegalInstructionException { - requireApfVersion(MIN_APF_VERSION_IN_DEV); - return append(new Instruction(Opcodes.PKTDATACOPY, R0).addUnsigned(src).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. - * - * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once. - * @return the ApfGenerator object - */ - public ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 ApfV4Generator 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 QNAMEs 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 ApfV4Generator 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 QNAMEs 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 ApfV4Generator 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 NAMEs - * specified in {@code Names}. Examines the payload starting at the offset in R0. - * R = 0 means check for "does not contain". - */ - public ApfV4Generator 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)); - } - - /** - * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP - * payload's DNS answers/authority/additional records contain the NAMEs - * specified in {@code Names}. Examines the payload starting at the offset in R0. - * R = 1 means check for "contain". - */ - public ApfV4Generator 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) { + static void checkRange(@NonNull String variableName, long value, long lowerBound, + long upperBound) { if (value >= lowerBound && value <= upperBound) { return; }
diff --git a/src/android/net/apf/ApfV6Generator.java b/src/android/net/apf/ApfV6Generator.java new file mode 100644 index 0000000..ff6bb4d --- /dev/null +++ b/src/android/net/apf/ApfV6Generator.java
@@ -0,0 +1,326 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.apf; + +import static android.net.apf.ApfV4Generator.Register.R0; +import static android.net.apf.ApfV4Generator.Register.R1; + +import androidx.annotation.NonNull; + +import com.android.net.module.util.HexDump; + +/** + * APFv6 assembler/generator. A tool for generating an APFv6 program. + * + * @hide + */ +public class ApfV6Generator extends ApfV4Generator { + + /** + * Creates an ApfV6Generator instance which is able to emit instructions for the specified + * {@code version} of the APF interpreter. Throws {@code IllegalInstructionException} if + * the requested version is unsupported. + * + */ + public ApfV6Generator() throws IllegalInstructionException { + super(MIN_APF_VERSION_IN_DEV); + } + + /** + * Add an instruction to the end of the program to increment the counter value and + * immediately return PASS. + */ + public ApfV4Generator addCountAndPass(int cnt) { + checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */, + 1000 /* upperBound */); + // PASS requires using R0 because it shares opcode with DROP + return append(new Instruction(Opcodes.PASSDROP).addUnsigned(cnt)); + } + + /** + * Add an instruction to the end of the program to let the program immediately return DROP. + */ + public ApfV4Generator addDrop() { + // DROP requires using R1 because it shares opcode with PASS + return append(new Instruction(Opcodes.PASSDROP, R1)); + } + + /** + * Add an instruction to the end of the program to increment the counter value and + * immediately return DROP. + */ + public ApfV4Generator addCountAndDrop(int cnt) { + checkRange("CounterNumber", cnt /* value */, 1 /* lowerBound */, + 1000 /* upperBound */); + // DROP requires using R1 because it shares opcode with PASS + return append(new Instruction(Opcodes.PASSDROP, 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 ApfV4Generator addAllocateR0() { + return append(new Instruction(ExtendedOpcodes.ALLOCATE)); + } + + /** + * Add an instruction to the end of the program to call the apf_allocate_buffer() function. + * + * @param size the buffer length to be allocated. + */ + public ApfV4Generator addAllocate(int size) { + // 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 ApfV4Generator addData(byte[] data) throws IllegalInstructionException { + if (!mInstructions.isEmpty()) { + throw new IllegalInstructionException("data instruction has to come first"); + } + return append(new Instruction(Opcodes.JMP, R1).addUnsigned(data.length).setBytesImm(data)); + } + + /** + * Add an instruction to the end of the program to transmit the allocated buffer. + */ + public ApfV4Generator addTransmit() { + // TRANSMIT requires using R0 because it shares opcode with DISCARD + return append(new Instruction(ExtendedOpcodes.TRANSMITDISCARD)); + } + + /** + * Add an instruction to the end of the program to discard the allocated buffer. + */ + public ApfV4Generator addDiscard() { + // DISCARD requires using R1 because it shares opcode with TRANSMIT + return append(new Instruction(ExtendedOpcodes.TRANSMITDISCARD, R1)); + } + + /** + * Add an instruction to the end of the program to write 1 byte value to output buffer. + */ + public ApfV4Generator addWriteU8(int val) { + 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 ApfV4Generator addWriteU16(int val) { + 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 ApfV4Generator addWriteU32(long val) { + 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 ApfV4Generator addWriteU8(Register reg) { + 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 ApfV4Generator addWriteU16(Register reg) { + 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 ApfV4Generator addWriteU32(Register reg) { + 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 ApfV4Generator addDataCopy(int src, int len) { + 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 ApfV4Generator addPacketCopy(int src, int len) { + return append(new Instruction(Opcodes.PKTDATACOPY, R0).addUnsigned(src).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. + * + * @param len the number of bytes to be copied, only <= 255 bytes can be copied at once. + * @return the ApfGenerator object + */ + public ApfV4Generator addDataCopyFromR0(int len) { + 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 ApfV4Generator addPacketCopyFromR0(int len) { + 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 ApfV4Generator addDataCopyFromR0LenR1() { + 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 ApfV4Generator addPacketCopyFromR0LenR1() { + return append(new Instruction(ExtendedOpcodes.EPKTCOPY, R1)); + } + + /** + * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP + * payload's DNS questions do NOT contain the QNAMEs 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 ApfV4Generator addJumpIfPktAtR0DoesNotContainDnsQ(@NonNull byte[] qnames, int qtype, + @NonNull String tgt) { + 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 QNAMEs 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 ApfV4Generator addJumpIfPktAtR0ContainDnsQ(@NonNull byte[] qnames, int qtype, + @NonNull String tgt) { + 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 NAMEs + * specified in {@code Names}. Examines the payload starting at the offset in R0. + * R = 0 means check for "does not contain". + */ + public ApfV4Generator addJumpIfPktAtR0DoesNotContainDnsA(@NonNull byte[] names, + @NonNull String tgt) { + validateNames(names); + return append(new Instruction(ExtendedOpcodes.JDNSAMATCH).setTargetLabel(tgt).setBytesImm( + names)); + } + + /** + * Appends a conditional jump instruction to the program: Jumps to {@code tgt} if the UDP + * payload's DNS answers/authority/additional records contain the NAMEs + * specified in {@code Names}. Examines the payload starting at the offset in R0. + * R = 1 means check for "contain". + */ + public ApfV4Generator addJumpIfPktAtR0ContainDnsA(@NonNull byte[] names, + @NonNull String tgt) { + validateNames(names); + return append(new Instruction(ExtendedOpcodes.JDNSAMATCH, R1).setTargetLabel( + tgt).setBytesImm(names)); + } + + /** + * 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); + } + } +}
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java index 5633b54..4e6a222 100644 --- a/src/android/net/dhcp/DhcpClient.java +++ b/src/android/net/dhcp/DhcpClient.java
@@ -1490,7 +1490,8 @@ // IpClient sees the IP address appear, it will enter provisioned state without any // configuration information from DHCP. http://b/146850745. notifySuccess(); - mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.ipAddress); + mController.sendMessage(CMD_CONFIGURE_LINKADDRESS, mDhcpLease.leaseDuration, 0, + mDhcpLease.ipAddress); } @Override
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index b06a632..9d1ba0e 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java
@@ -51,6 +51,7 @@ 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_POPULATE_LINK_ADDRESS_LIFETIME_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; @@ -705,6 +706,7 @@ private final boolean mEnableIpClientIgnoreLowRaLifetime; private final boolean mApfShouldHandleLightDoze; private final boolean mEnableApfPollingCounters; + private final boolean mPopulateLinkAddressLifetime; private InterfaceParams mInterfaceParams; @@ -944,6 +946,8 @@ // 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); + mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context, + IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION); IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration( mMinRdnssLifetimeSec); @@ -3259,7 +3263,24 @@ case DhcpClient.CMD_CONFIGURE_LINKADDRESS: { final LinkAddress ipAddress = (LinkAddress) msg.obj; - if (mInterfaceCtrl.setIPv4Address(ipAddress)) { + final boolean success; + if (mPopulateLinkAddressLifetime) { + // For IPv4 link addresses, there is no concept of preferred/valid + // lifetimes. Populate the ifa_cacheinfo attribute in the netlink + // message with the DHCP lease duration, which is used by the kernel + // to maintain the validity of the IP addresses. + final int leaseDuration = msg.arg1; + success = NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index, + ipAddress.getAddress(), + (short) ipAddress.getPrefixLength(), + 0 /* flags */, + (byte) RT_SCOPE_UNIVERSE /* scope */, + leaseDuration /* preferred */, + leaseDuration /* valid */); + } else { + success = mInterfaceCtrl.setIPv4Address(ipAddress); + } + if (success) { // Although it's impossible to happen that DHCP client becomes null in // RunningState and then NPE is thrown when it attempts to send a message // on an null object, sometimes it's found during stress tests. If this
diff --git a/src/android/net/ip/IpClientLinkObserver.java b/src/android/net/ip/IpClientLinkObserver.java index bf113b0..f91f569 100644 --- a/src/android/net/ip/IpClientLinkObserver.java +++ b/src/android/net/ip/IpClientLinkObserver.java
@@ -30,6 +30,7 @@ import static com.android.net.module.util.netlink.NetlinkConstants.RT_SCOPE_UNIVERSE; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_ACCEPT_IPV6_LINK_LOCAL_DNS_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE; +import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION; import android.app.AlarmManager; import android.content.Context; @@ -160,8 +161,9 @@ private final Handler mHandler; private final IpClient.Dependencies mDependencies; private final String mClatInterfaceName; - private final MyNetlinkMonitor mNetlinkMonitor; + private final IpClientNetlinkMonitor mNetlinkMonitor; private final boolean mNetlinkEventParsingEnabled; + private final boolean mPopulateLinkAddressLifetime; private boolean mClatInterfaceExists; @@ -194,7 +196,9 @@ mDependencies = deps; mNetlinkEventParsingEnabled = deps.isFeatureNotChickenedOut(context, IPCLIENT_PARSE_NETLINK_EVENTS_FORCE_DISABLE); - mNetlinkMonitor = new MyNetlinkMonitor(h, log, mTag); + mPopulateLinkAddressLifetime = deps.isFeatureEnabled(context, + IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION); + mNetlinkMonitor = new IpClientNetlinkMonitor(h, log, mTag); mHandler.post(() -> { if (!mNetlinkMonitor.start()) { Log.wtf(mTag, "Fail to start NetlinkMonitor."); @@ -427,10 +431,10 @@ * Simple NetlinkMonitor. Listen for netlink events from kernel. * All methods except the constructor must be called on the handler thread. */ - private class MyNetlinkMonitor extends NetlinkMonitor { + private class IpClientNetlinkMonitor extends NetlinkMonitor { private final Handler mHandler; - MyNetlinkMonitor(Handler h, SharedLog log, String tag) { + IpClientNetlinkMonitor(Handler h, SharedLog log, String tag) { super(h, log, tag, OsConstants.NETLINK_ROUTE, !mNetlinkEventParsingEnabled ? NetlinkConstants.RTMGRP_ND_USEROPT @@ -634,9 +638,14 @@ // The preferred/valid in ifa_cacheinfo expressed in units of seconds, convert // it to milliseconds for deprecationTime or expirationTime used in LinkAddress. - private static long getDeprecationOrExpirationTime( - @Nullable final StructIfacacheInfo cacheInfo, long lifetime, long now) { - if (cacheInfo == null) return LinkAddress.LIFETIME_UNKNOWN; + // If the experiment flag is not enabled, LinkAddress.LIFETIME_UNKNOWN is retuend, + // the same as before. + private long getDeprecationOrExpirationTime(@Nullable final StructIfacacheInfo cacheInfo, + long now, boolean deprecationTime) { + if (!mPopulateLinkAddressLifetime || (cacheInfo == null)) { + return LinkAddress.LIFETIME_UNKNOWN; + } + final long lifetime = deprecationTime ? cacheInfo.preferred : cacheInfo.valid; return (lifetime == Integer.toUnsignedLong(INFINITE_LEASE)) ? LinkAddress.LIFETIME_PERMANENT : now + lifetime * 1000; @@ -651,9 +660,9 @@ final StructIfacacheInfo cacheInfo = msg.getIfacacheInfo(); final long now = SystemClock.elapsedRealtime(); final long deprecationTime = - getDeprecationOrExpirationTime(cacheInfo, cacheInfo.preferred, now); + getDeprecationOrExpirationTime(cacheInfo, now, true /* deprecationTime */); final long expirationTime = - getDeprecationOrExpirationTime(cacheInfo, cacheInfo.valid, now); + getDeprecationOrExpirationTime(cacheInfo, now, false /* deprecationTime */); final LinkAddress la = new LinkAddress(msg.getIpAddress(), ifaddrMsg.prefixLen, msg.getFlags(), ifaddrMsg.scope, deprecationTime, expirationTime);
diff --git a/src/android/net/ip/IpReachabilityMonitor.java b/src/android/net/ip/IpReachabilityMonitor.java index 12202ad..daf9e51 100644 --- a/src/android/net/ip/IpReachabilityMonitor.java +++ b/src/android/net/ip/IpReachabilityMonitor.java
@@ -23,6 +23,7 @@ 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_IGNORE_ORGANIC_NUD_FAILURE_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION; @@ -241,6 +242,7 @@ private final boolean mIgnoreIncompleteIpv6DnsServerEnabled; private final boolean mIgnoreIncompleteIpv6DefaultRouterEnabled; private final boolean mMacChangeFailureOnlyAfterRoam; + private final boolean mIgnoreOrganicNudFailure; public IpReachabilityMonitor( Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback, @@ -270,6 +272,8 @@ IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION); mMacChangeFailureOnlyAfterRoam = dependencies.isFeatureNotChickenedOut(context, IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION); + mIgnoreOrganicNudFailure = dependencies.isFeatureEnabled(context, + IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION); mMetricsLog = metricsLog; mNetd = netd; Preconditions.checkNotNull(mNetd); @@ -527,11 +531,22 @@ isNudFailureDueToRoam(), lostProvisioning); if (lostProvisioning) { - final String logMsg = "FAILURE: LOST_PROVISIONING, " + event; + final boolean isOrganicNudFailureAndToBeIgnored = + ((type == NudEventType.NUD_ORGANIC_FAILED_CRITICAL) + && mIgnoreOrganicNudFailure); + final String logMsg = "FAILURE: LOST_PROVISIONING, " + event + + ", NUD event type: " + type.name() + + (isOrganicNudFailureAndToBeIgnored ? ", to be ignored" : ""); Log.w(TAG, logMsg); // TODO: remove |ip| when the callback signature no longer has // an InetAddress argument. - mCallback.notifyLost(ip, logMsg, type); + // Notify critical neighbor lost as long as the NUD failures + // are not from kernel organic or the NUD failure event type is + // NUD_ORGANIC_FAILED_CRITICAL but the experiment flag is not + // enabled. Regardless, the event metrics are still recoreded. + if (!isOrganicNudFailureAndToBeIgnored) { + mCallback.notifyLost(ip, logMsg, type); + } } logNudFailed(event, type); }
diff --git a/src/com/android/networkstack/util/NetworkStackUtils.java b/src/com/android/networkstack/util/NetworkStackUtils.java index c495315..0cb31fe 100755 --- a/src/com/android/networkstack/util/NetworkStackUtils.java +++ b/src/com/android/networkstack/util/NetworkStackUtils.java
@@ -234,6 +234,12 @@ "ip_reachability_router_mac_change_failure_only_after_roam_version"; /** + * Experiment flag to ignore all NUD failures from kernel organic. + */ + public static final String IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION = + "ip_reachability_ignore_organic_nud_failure_version"; + + /** * Experiment flag to enable DHCPv6 Prefix Delegation(RFC8415) in IpClient. */ public static final String IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION = @@ -243,10 +249,12 @@ * Experiment flag to enable new ra filter. */ 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. @@ -260,6 +268,14 @@ public static final String NETWORKMONITOR_ASYNC_PRIVDNS_RESOLUTION = "networkmonitor_async_privdns_resolution"; + /** + * Experiment flag to populate the IP link address lifetime such as deprecationTime and + * expirationtTime. + */ + public static final String IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION = + "ipclient_populate_link_address_lifetime_version"; + + /**** BEGIN Feature Kill Switch Flags ****/ /**
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 78a47ea..c62fb90 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -2092,6 +2092,7 @@ mThread = new Thread(() -> sendMessage(obtainMessage(CMD_PROBE_COMPLETE, token, 0, isCaptivePortal(deps, httpsUrls, httpUrls, fallbackUrl)))); mThread.start(); + mDependencies.onThreadCreated(mThread); } @Override @@ -3205,6 +3206,7 @@ ? new HttpsProbe(properties, proxy, url, captivePortalApiUrl) : new HttpProbe(properties, proxy, url, captivePortalApiUrl); mResult = CaptivePortalProbeResult.failed(probeType); + mDependencies.onThreadCreated(this); } private volatile CaptivePortalProbeResult mResult; @@ -3382,6 +3384,7 @@ // Fixed pool to prevent configuring too many urls to exhaust system resource. final ExecutorService executor = Executors.newFixedThreadPool( Math.min(num, MAX_PROBE_THREAD_POOL_SIZE)); + mDependencies.onExecutorServiceCreated(executor); final CompletionService<CaptivePortalProbeResult> ecs = new ExecutorCompletionService<CaptivePortalProbeResult>(executor); final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties); @@ -3726,6 +3729,24 @@ DataStallStatsUtils.write(stats, result); } + /** + * Callback to be called when a probing thread instance is created. + * + * This method is designed for overriding in test classes to collect + * created threads and waits for the termination. + */ + public void onThreadCreated(@NonNull Thread thread) { + } + + /** + * Callback to be called when a ExecutorService instance is created. + * + * This method is designed for overriding in test classes to collect + * created threads and waits for the termination. + */ + public void onExecutorServiceCreated(@NonNull ExecutorService ecs) { + } + public static final Dependencies DEFAULT = new Dependencies(); }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java index 2bbb879..c4dc40a 100644 --- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java +++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -746,6 +746,10 @@ setFeatureEnabled(NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION, true /* isDhcp6PrefixDelegationEnabled */); + // Enable populating the IP Link Address lifetime. + setFeatureEnabled(NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION, + true /* enabled */); + setUpTapInterface(); // It turns out that Router Solicitation will also be sent out even after the tap interface // is brought up, however, we want to wait for RS which is sent due to IPv6 stack is enabled @@ -1495,9 +1499,6 @@ final boolean shouldFirePreconnectionTimeout, final boolean timeoutBeforePreconnectionComplete) throws Exception { final long currentTime = System.currentTimeMillis(); - final ArgumentCaptor<InterfaceConfigurationParcel> ifConfig = - ArgumentCaptor.forClass(InterfaceConfigurationParcel.class); - startIpClientProvisioning(shouldReplyRapidCommitAck, true /* isDhcpPreConnectionEnabled */, false /* isDhcpIpConflictDetectEnabled */); @@ -1554,12 +1555,7 @@ } } verify(mCb, timeout(TEST_TIMEOUT_MS)).setFallbackMulticastFilter(true); - - final LinkAddress ipAddress = new LinkAddress(CLIENT_ADDR, PREFIX_LENGTH); - verify(mNetd, timeout(TEST_TIMEOUT_MS).times(1)).interfaceSetCfg(ifConfig.capture()); - assertEquals(ifConfig.getValue().ifName, mIfaceName); - assertEquals(ifConfig.getValue().ipv4Addr, ipAddress.getAddress().getHostAddress()); - assertEquals(ifConfig.getValue().prefixLength, PREFIX_LENGTH); + verifyIPv4OnlyProvisioningSuccess(Collections.singletonList(CLIENT_ADDR)); assertIpMemoryStoreNetworkAttributes(TEST_LEASE_DURATION_S, currentTime, TEST_DEFAULT_MTU); } @@ -4244,8 +4240,7 @@ NudEventType.NUD_POST_ROAMING_MAC_ADDRESS_CHANGED); } - private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses( - boolean disconnect) throws Exception { + private void prepareIpReachabilityMonitorIpv4AddressResolutionTest() throws Exception { mNetworkAgentThread = new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName()); mNetworkAgentThread.start(); @@ -4270,6 +4265,11 @@ final byte[] data = new byte[100]; random.nextBytes(data); sendUdpPacketToNetwork(mNetworkAgent.getNetwork(), SERVER_ADDR, 1234 /* port */, data); + } + + private void doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses( + boolean disconnect) throws Exception { + prepareIpReachabilityMonitorIpv4AddressResolutionTest(); // Respond to the broadcast ARP request. final ArpPacket request = getNextArpPacket(); @@ -4311,6 +4311,37 @@ doTestIpReachabilityMonitor_replyBroadcastArpRequestWithDiffMacAddresses(true); } + @Test + public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure() + throws Exception { + setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, + true /* ignoreOrganicNudFailure */); + prepareIpReachabilityMonitorIpv4AddressResolutionTest(); + + ArpPacket packet; + while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) { + // wait address resolution to complete. + } + verify(mCb, never()).onReachabilityFailure(any()); + } + + @Test + public void testIpReachabilityMonitor_ignoreIpv4DefaultRouterOrganicNudFailure_flagoff() + throws Exception { + setFeatureEnabled(NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, + false /* ignoreOrganicNudFailure */); + prepareIpReachabilityMonitorIpv4AddressResolutionTest(); + + ArpPacket packet; + while ((packet = getNextArpPacket(TEST_TIMEOUT_MS)) != null) { + // wait address resolution to complete. + } + final ArgumentCaptor<ReachabilityLossInfoParcelable> lossInfoCaptor = + ArgumentCaptor.forClass(ReachabilityLossInfoParcelable.class); + verify(mCb).onReachabilityFailure(lossInfoCaptor.capture()); + assertEquals(ReachabilityLossReason.ORGANIC, lossInfoCaptor.getValue().reason); + } + private void sendUdpPacketToNetwork(final Network network, final InetAddress remoteIp, int port, final byte[] data) throws Exception { final InetAddress laddr = @@ -4325,6 +4356,7 @@ final Inet6Address targetIp, final boolean isIgnoreIncompleteIpv6DnsServerEnabled, final boolean isIgnoreIncompleteIpv6DefaultRouterEnabled, + final boolean isIgnoreOrganicNudFailureEnabled, final boolean expectNeighborLost) throws Exception { mNetworkAgentThread = new HandlerThread(IpClientIntegrationTestCommon.class.getSimpleName()); @@ -4338,6 +4370,9 @@ setFeatureEnabled( NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION, isIgnoreIncompleteIpv6DefaultRouterEnabled); + setFeatureEnabled( + NetworkStackUtils.IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION, + isIgnoreOrganicNudFailureEnabled); final ProvisioningConfiguration config = new ProvisioningConfiguration.Builder() .build(); startIpClientProvisioning(config); @@ -4395,6 +4430,7 @@ runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp, true /* isIgnoreIncompleteIpv6DnsServerEnabled */, false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + false /* isIgnoreOrganicNudFailureEnabled */, false /* expectNeighborLost */); } @@ -4406,6 +4442,7 @@ runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp, false /* isIgnoreIncompleteIpv6DnsServerEnabled */, false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + false /* isIgnoreOrganicNudFailureEnabled */, true /* expectNeighborLost */); } @@ -4416,6 +4453,7 @@ ROUTER_LINK_LOCAL /* targetIp */, false /* isIgnoreIncompleteIpv6DnsServerEnabled */, true /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + false /* isIgnoreOrganicNudFailureEnabled */, false /* expectNeighborLost */); } @@ -4426,6 +4464,53 @@ ROUTER_LINK_LOCAL /* targetIp */, false /* isIgnoreIncompleteIpv6DnsServerEnabled */, false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + false /* isIgnoreOrganicNudFailureEnabled */, + true /* expectNeighborLost */); + } + + @Test + public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure() + throws Exception { + final Inet6Address targetIp = + (Inet6Address) InetAddresses.parseNumericAddress(IPV6_ON_LINK_DNS_SERVER); + runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp, + false /* isIgnoreIncompleteIpv6DnsServerEnabled */, + false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + true /* isIgnoreOrganicNudFailureEnabled */, + false /* expectNeighborLost */); + } + + @Test + public void testIpReachabilityMonitor_ignoreOnLinkIpv6DnsOrganicNudFailure_flagoff() + throws Exception { + final Inet6Address targetIp = + (Inet6Address) InetAddresses.parseNumericAddress(IPV6_ON_LINK_DNS_SERVER); + runIpReachabilityMonitorAddressResolutionTest(IPV6_ON_LINK_DNS_SERVER, targetIp, + false /* isIgnoreIncompleteIpv6DnsServerEnabled */, + false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + false /* isIgnoreOrganicNudFailureEnabled */, + true /* expectNeighborLost */); + } + + @Test + public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure() + throws Exception { + runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER, + ROUTER_LINK_LOCAL /* targetIp */, + false /* isIgnoreIncompleteIpv6DnsServerEnabled */, + false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + true /* isIgnoreOrganicNudFailureEnabled */, + false /* expectNeighborLost */); + } + + @Test + public void testIpReachabilityMonitor_ignoreIpv6DefaultRouterOrganicNudFailure_flagoff() + throws Exception { + runIpReachabilityMonitorAddressResolutionTest(IPV6_OFF_LINK_DNS_SERVER, + ROUTER_LINK_LOCAL /* targetIp */, + false /* isIgnoreIncompleteIpv6DnsServerEnabled */, + false /* isIgnoreIncompleteIpv6DefaultRouterEnabled */, + false /* isIgnoreOrganicNudFailureEnabled */, true /* expectNeighborLost */); } @@ -5700,10 +5785,9 @@ long when = 0; for (LinkAddress la : lp.getLinkAddresses()) { if (la.isIpv4()) { - // TODO: assert the deprecationTime and expirationTime for IPv4 address - // once we populate the correct lifetime with dhcp lease(3600s) for IPv4 - // link address. - assertLinkAddressPermanentLifetime(la); + when = now + 3600 * 1000; // DHCP lease duration + assertLinkAddressDeprecationTime(la, when); + assertLinkAddressExpirationTime(la, when); } else if (la.isIpv6() && la.getAddress().isLinkLocalAddress()) { assertLinkAddressPermanentLifetime(la); } else if (la.isIpv6() && la.isGlobalPreferred()) { @@ -5714,4 +5798,59 @@ } } } + + @Test + public void testPopulateLinkAddressLifetime_infiniteLeaseDuration() throws Exception { + // Only run the test when the flag of parsing netlink events is enabled. + assumeTrue(mIsNetlinkEventParseEnabled); + + final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder() + .withoutIPv6() + .build(); + + startIpClientProvisioning(cfg); + handleDhcpPackets(true /* isSuccessLease */, DhcpPacket.INFINITE_LEASE, + false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, + null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */, + null /* domainName */, null /* domainSearchList */); + + final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class); + verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture()); + final LinkProperties lp = captor.getValue(); + assertNotNull(lp); + for (LinkAddress la : lp.getLinkAddresses()) { + if (la.isIpv4()) { + assertLinkAddressPermanentLifetime(la); + } + } + } + + @Test + public void testPopulateLinkAddressLifetime_minimalLeaseDuration() throws Exception { + // Only run the test when the flag of parsing netlink events is enabled. + assumeTrue(mIsNetlinkEventParseEnabled); + + final ProvisioningConfiguration cfg = new ProvisioningConfiguration.Builder() + .withoutIPv6() + .build(); + + startIpClientProvisioning(cfg); + handleDhcpPackets(true /* isSuccessLease */, 59 /* lease duration */, + false /* shouldReplyRapidCommitAck */, TEST_DEFAULT_MTU, + null /* captivePortalApiUrl */, null /* ipv6OnlyWaitTime */, + null /* domainName */, null /* domainSearchList */); + + final ArgumentCaptor<LinkProperties> captor = ArgumentCaptor.forClass(LinkProperties.class); + verify(mCb, timeout(TEST_TIMEOUT_MS)).onProvisioningSuccess(captor.capture()); + final LinkProperties lp = captor.getValue(); + assertNotNull(lp); + for (LinkAddress la : lp.getLinkAddresses()) { + if (la.isIpv4()) { + final long now = SystemClock.elapsedRealtime(); + final long when = now + 60 * 1000; // minimal lease duration + assertLinkAddressDeprecationTime(la, when); + assertLinkAddressExpirationTime(la, when); + } + } + } }
diff --git a/tests/unit/src/android/net/apf/ApfV5Test.kt b/tests/unit/src/android/net/apf/ApfV5Test.kt index 58ee706..f9766fd 100644 --- a/tests/unit/src/android/net/apf/ApfV5Test.kt +++ b/tests/unit/src/android/net/apf/ApfV5Test.kt
@@ -17,7 +17,6 @@ import android.net.apf.ApfV4Generator.IllegalInstructionException import android.net.apf.ApfV4Generator.MIN_APF_VERSION -import android.net.apf.ApfV4Generator.MIN_APF_VERSION_IN_DEV import android.net.apf.ApfV4Generator.Register.R0 import android.net.apf.ApfV4Generator.Register.R1 import androidx.test.filters.SmallTest @@ -36,53 +35,15 @@ class ApfV5Test { @Test - fun testApfInstructionVersionCheck() { - var gen = ApfV4Generator(MIN_APF_VERSION) - assertFailsWith<IllegalInstructionException> { gen.addDrop() } - assertFailsWith<IllegalInstructionException> { gen.addCountAndDrop(12) } - 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()), ApfV4Generator.DROP_LABEL) } - assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsQ( - byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfV4Generator.DROP_LABEL) } - assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsQ( - byteArrayOf(1, 'A'.code.toByte()), 0x0c, ApfV4Generator.DROP_LABEL) } - assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0DoesNotContainDnsA( - byteArrayOf(1, 'A'.code.toByte()), ApfV4Generator.DROP_LABEL) } - assertFailsWith<IllegalInstructionException> { gen.addJumpIfPktAtR0ContainDnsA( - byteArrayOf(1, 'A'.code.toByte()), ApfV4Generator.DROP_LABEL) } - } - - @Test fun testDataInstructionMustComeFirst() { - var gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + var gen = ApfV6Generator() gen.addAllocateR0() assertFailsWith<IllegalInstructionException> { gen.addData(ByteArray(3) { 0x01 }) } } @Test fun testApfInstructionEncodingSizeCheck() { - var gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + var gen = ApfV6Generator() assertFailsWith<IllegalArgumentException> { gen.addAllocate(65536) } assertFailsWith<IllegalArgumentException> { gen.addAllocate(-1) } assertFailsWith<IllegalArgumentException> { gen.addDataCopy(-1, 1) } @@ -177,31 +138,43 @@ // encoding PASS opcode: opcode=0, imm_len=0, R=0 assertContentEquals( byteArrayOf(encodeInstruction(opcode = 0, immLength = 0, register = 0)), program) + assertContentEquals( + listOf("0: pass"), + ApfJniUtils.disassembleApf(program).map { it.trim() } ) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addDrop() program = gen.generate() // encoding DROP opcode: opcode=0, imm_len=0, R=1 assertContentEquals( byteArrayOf(encodeInstruction(opcode = 0, immLength = 0, register = 1)), program) + assertContentEquals( + listOf("0: drop"), + ApfJniUtils.disassembleApf(program).map { it.trim() } ) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addCountAndPass(129) program = gen.generate() // encoding COUNT(PASS) opcode: opcode=0, imm_len=size_of(imm), R=0, imm=counterNumber assertContentEquals( byteArrayOf(encodeInstruction(opcode = 0, immLength = 1, register = 0), 0x81.toByte()), program) + assertContentEquals( + listOf("0: pass 129"), + ApfJniUtils.disassembleApf(program).map { it.trim() } ) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addCountAndDrop(1000) program = gen.generate() // encoding COUNT(DROP) opcode: opcode=0, imm_len=size_of(imm), R=1, imm=counterNumber assertContentEquals( byteArrayOf(encodeInstruction(opcode = 0, immLength = 2, register = 1), 0x03, 0xe8.toByte()), program) + assertContentEquals( + listOf("0: drop 1000"), + ApfJniUtils.disassembleApf(program).map { it.trim() } ) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addAllocateR0() gen.addAllocate(1500) program = gen.generate() @@ -212,10 +185,10 @@ 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)) + assertContentEquals(listOf("0: allocate r0", "2: allocate 1500"), + ApfJniUtils.disassembleApf(program).map { it.trim() }) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addTransmit() gen.addDiscard() program = gen.generate() @@ -226,10 +199,10 @@ encodeInstruction(opcode = 21, immLength = 1, register = 0), 37, encodeInstruction(opcode = 21, immLength = 1, register = 1), 37, ), program) - // TODO: add back disassembling test check after we update the apf_disassembler - // assertContentEquals(arrayOf(" 0: trans"), ApfJniUtils.disassembleApf(program)) + assertContentEquals(listOf("0: discard", "2: transmit"), + ApfJniUtils.disassembleApf(program).map { it.trim() }) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() val largeByteArray = ByteArray(256) { 0x01 } gen.addData(largeByteArray) program = gen.generate() @@ -237,8 +210,10 @@ assertContentEquals(byteArrayOf( encodeInstruction(opcode = 14, immLength = 2, register = 1), 0x01, 0x00) + largeByteArray, program) + assertContentEquals(listOf("0: data 256," + "01".repeat(256) ), + ApfJniUtils.disassembleApf(program).map { it.trim() }) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addWriteU8(0x01) gen.addWriteU16(0x0102) gen.addWriteU32(0x01020304) @@ -260,20 +235,20 @@ 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" + assertContentEquals(listOf( + "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)) + ApfJniUtils.disassembleApf(program).map { it.trim() }) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addWriteU8(R0) gen.addWriteU16(R0) gen.addWriteU32(R0) @@ -289,16 +264,15 @@ 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: ewrite1 r0", -// " 2: ewrite2 r0", -// " 4: ewrite4 r0", -// " 6: ewrite1 r1", -// " 8: ewrite2 r1", -// " 10: ewrite4 r1"), ApfJniUtils.disassembleApf(program)) + assertContentEquals(listOf( + "0: ewrite1 r0", + "2: ewrite2 r0", + "4: ewrite4 r0", + "6: ewrite1 r1", + "8: ewrite2 r1", + "10: ewrite4 r1"), ApfJniUtils.disassembleApf(program).map { it.trim() }) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addDataCopy(0, 10) gen.addDataCopy(1, 5) gen.addPacketCopy(1000, 255) @@ -309,12 +283,12 @@ 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 0, 5", -// " 3: pcopy 1000, 255"), ApfJniUtils.disassembleApf(program)) + assertContentEquals(listOf( + "0: dcopy 0, 10", + "2: dcopy 1, 5", + "5: pcopy 1000, 255"), ApfJniUtils.disassembleApf(program).map { it.trim() }) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addPacketCopyFromR0LenR1() gen.addPacketCopyFromR0(5) gen.addDataCopyFromR0LenR1() @@ -331,7 +305,7 @@ // " 0: dcopy [r1+0], 5", // " 4: pcopy [r0+1000], 255"), ApfJniUtils.disassembleApf(program)) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addJumpIfBytesAtR0Equal(byteArrayOf('a'.code.toByte()), ApfV4Generator.DROP_LABEL) program = gen.generate() assertContentEquals( @@ -339,7 +313,7 @@ 1, 1, 'a'.code.toByte()), program) val qnames = byteArrayOf(1, 'A'.code.toByte(), 1, 'B'.code.toByte(), 0, 0) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addJumpIfPktAtR0DoesNotContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL) gen.addJumpIfPktAtR0ContainDnsQ(qnames, 0x0c, ApfV4Generator.DROP_LABEL) program = gen.generate() @@ -349,7 +323,7 @@ encodeInstruction(21, 1, 1), 43, 1, 0x0c.toByte(), ) + qnames, program) - gen = ApfV4Generator(MIN_APF_VERSION_IN_DEV) + gen = ApfV6Generator() gen.addJumpIfPktAtR0DoesNotContainDnsA(qnames, ApfV4Generator.DROP_LABEL) gen.addJumpIfPktAtR0ContainDnsA(qnames, ApfV4Generator.DROP_LABEL) program = gen.generate()
diff --git a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt index e071280..fbf311b 100644 --- a/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt +++ b/tests/unit/src/android/net/ip/IpReachabilityMonitorTest.kt
@@ -55,6 +55,7 @@ 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_IGNORE_ORGANIC_NUD_FAILURE_VERSION import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_MCAST_RESOLICIT_VERSION import com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION import com.android.testutils.makeNewNeighMessage @@ -71,7 +72,9 @@ import kotlin.test.fail import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TestName import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any @@ -185,6 +188,7 @@ @RunWith(AndroidJUnit4::class) @SmallTest class IpReachabilityMonitorTest { + @get:Rule val mTestName = TestName() private val callback = mock(IpReachabilityMonitor.Callback::class.java) private val dependencies = mock(IpReachabilityMonitor.Dependencies::class.java) private val log = mock(SharedLog::class.java) @@ -271,6 +275,19 @@ doReturn(true).`when`(dependencies).isFeatureNotChickenedOut(any(), eq(IP_REACHABILITY_ROUTER_MAC_CHANGE_FAILURE_ONLY_AFTER_ROAM_VERSION)) + val ignoreOrganicNudFailureTestList = listOf( + "testLoseProvisioning_ignoreOrganicIpv4DnsLost", + "testLoseProvisioning_ignoreOrganicIpv6DnsLost", + "testLoseProvisioning_ignoreOrganicIpv4GatewayLost", + "testLoseProvisioning_ignoreOrganicIpv6GatewayLost") + // The experiment flag: IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION is read at + // the IpReachabilityMonitor constructor, so we have to set the value before initializing + // an IpReachabilityMonitor instance. + if (ignoreOrganicNudFailureTestList.contains(mTestName.methodName)) { + doReturn(true).`when`(dependencies).isFeatureEnabled(any(), + eq(IP_REACHABILITY_IGNORE_ORGANIC_NUD_FAILURE_VERSION)) + } + val monitorFuture = CompletableFuture<IpReachabilityMonitor>() // IpReachabilityMonitor needs to be started from the handler thread handler.post { @@ -312,6 +329,15 @@ lostNeighbor: InetAddress, eventType: NudEventType ) { + runLoseProvisioningTest(newLp, lostNeighbor, eventType, true /* expectedNotifyLost */) + } + + private fun runLoseProvisioningTest( + newLp: LinkProperties, + lostNeighbor: InetAddress, + eventType: NudEventType, + expectedNotifyLost: Boolean + ) { reachabilityMonitor.updateLinkProperties(newLp) neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV4_GATEWAY, NUD_STALE)) @@ -320,8 +346,14 @@ neighborMonitor.enqueuePacket(makeNewNeighMessage(TEST_IPV6_DNS, NUD_STALE)) neighborMonitor.enqueuePacket(makeNewNeighMessage(lostNeighbor, NUD_FAILED)) - verify(callback, timeout(TEST_TIMEOUT_MS)).notifyLost(eq(lostNeighbor), anyString(), - eq(eventType)) + handlerThread.waitForIdle(TEST_TIMEOUT_MS) + + if (expectedNotifyLost) { + verify(callback, timeout(TEST_TIMEOUT_MS)).notifyLost(eq(lostNeighbor), anyString(), + eq(eventType)) + } else { + verify(callback, never()).notifyLost(eq(lostNeighbor), anyString(), any()) + } } private fun verifyNudFailureMetrics( @@ -395,6 +427,30 @@ NUD_ORGANIC_FAILED_CRITICAL) } + @Test + fun testLoseProvisioning_ignoreOrganicIpv4DnsLost() { + runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV4_DNS, NUD_ORGANIC_FAILED_CRITICAL, + false /* expectedNotifyLost */) + } + + @Test + fun testLoseProvisioning_ignoreOrganicIpv6DnsLost() { + runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV6_DNS, NUD_ORGANIC_FAILED_CRITICAL, + false /* expectedNotifyLost */) + } + + @Test + fun testLoseProvisioning_ignoreOrganicIpv4GatewayLost() { + runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV4_GATEWAY, + NUD_ORGANIC_FAILED_CRITICAL, false /* expectedNotifyLost */) + } + + @Test + fun testLoseProvisioning_ignoreOrganicIpv6GatewayLost() { + runLoseProvisioningTest(TEST_LINK_PROPERTIES, TEST_IPV6_GATEWAY, + NUD_ORGANIC_FAILED_CRITICAL, false /* expectedNotifyLost */) + } + private fun runNudProbeFailureMetricsTest( lp: LinkProperties, lostNeighbor: InetAddress,
diff --git a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java index 43bee55..d3647f5 100644 --- a/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java +++ b/tests/unit/src/com/android/server/connectivity/NetworkMonitorTest.java
@@ -154,8 +154,8 @@ import android.util.ArrayMap; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; +import com.android.internal.annotations.GuardedBy; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.SharedLog; import com.android.networkstack.NetworkStackNotifier; @@ -180,6 +180,8 @@ import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; +import com.android.testutils.DevSdkIgnoreRunner; +import com.android.testutils.FunctionalUtils.ThrowingConsumer; import com.android.testutils.HandlerUtils; import com.google.protobuf.nano.MessageNano; @@ -194,6 +196,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.mockito.invocation.InvocationOnMock; @@ -223,12 +226,15 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Supplier; import javax.net.ssl.SSLHandshakeException; -@RunWith(AndroidJUnit4.class) +@DevSdkIgnoreRunner.MonitorThreadLeak +@RunWith(DevSdkIgnoreRunner.class) @SmallTest @SuppressLint("NewApi") // Uses hidden APIs, which the linter would identify as missing APIs. public class NetworkMonitorTest { @@ -321,6 +327,7 @@ private static final NetworkAgentConfigShim TEST_AGENT_CONFIG = NetworkAgentConfigShimImpl.newInstance(null); private static final LinkProperties TEST_LINK_PROPERTIES = new LinkProperties(); + private static final int THREAD_QUIT_MAX_RETRY_COUNT = 3; // Cannot have a static member for the LinkProperties with captive portal API information, as // the initializer would crash on Q (the members in LinkProperties were introduced in R). @@ -564,6 +571,11 @@ private FakeDns mFakeDns; + @GuardedBy("mThreadsToBeCleared") + private final ArrayList<Thread> mThreadsToBeCleared = new ArrayList<>(); + @GuardedBy("mExecutorServiceToBeCleared") + private final ArrayList<ExecutorService> mExecutorServiceToBeCleared = new ArrayList<>(); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -578,6 +590,18 @@ .getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTP_URL), any()); doReturn(TEST_HTTPS_URL).when(mDependencies) .getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL), any()); + doAnswer((invocation) -> { + synchronized (mThreadsToBeCleared) { + mThreadsToBeCleared.add(invocation.getArgument(0)); + } + return null; + }).when(mDependencies).onThreadCreated(any()); + doAnswer((invocation) -> { + synchronized (mExecutorServiceToBeCleared) { + mExecutorServiceToBeCleared.add(invocation.getArgument(0)); + } + return null; + }).when(mDependencies).onExecutorServiceCreated(any()); doReturn(mCleartextDnsNetwork).when(mNetwork).getPrivateDnsBypassingCopy(); @@ -680,8 +704,52 @@ mRegisteredReceivers = new HashSet<>(); } + private static <T> void quitThreadsThat(Supplier<List<T>> supplier, ThrowingConsumer terminator) + throws Exception { + // Run it multiple times since new threads might be generated in a thread + // that is about to be terminated, e.g. each thread that runs + // isCaptivePortal could generate 2 more probing threads. + for (int retryCount = 0; retryCount < THREAD_QUIT_MAX_RETRY_COUNT; retryCount++) { + final List<T> resourcesToBeCleared = supplier.get(); + if (resourcesToBeCleared.isEmpty()) return; + for (final T resource : resourcesToBeCleared) { + terminator.accept(resource); + } + } + + assertEquals(Collections.emptyList(), supplier.get()); + } + + private void quitExecutorServices() throws Exception { + quitThreadsThat(() -> { + synchronized (mExecutorServiceToBeCleared) { + final ArrayList<ExecutorService> ret = new ArrayList<>(mExecutorServiceToBeCleared); + mExecutorServiceToBeCleared.clear(); + return ret; + } + }, (it) -> { + final ExecutorService ecs = (ExecutorService) it; + ecs.awaitTermination(HANDLER_TIMEOUT_MS, TimeUnit.MILLISECONDS); + }); + } + + private void quitThreads() throws Exception { + quitThreadsThat(() -> { + synchronized (mThreadsToBeCleared) { + final ArrayList<Thread> ret = new ArrayList<>(mThreadsToBeCleared); + mThreadsToBeCleared.clear(); + return ret; + } + }, (it) -> { + final Thread th = (Thread) it; + th.interrupt(); + th.join(HANDLER_TIMEOUT_MS); + if (th.isAlive()) fail("Threads did not terminate within timeout."); + }); + } + @After - public void tearDown() { + public void tearDown() throws Exception { mFakeDns.clearAll(); // Make a local copy of mCreatedNetworkMonitors because during the iteration below, // WrappedNetworkMonitor#onQuitting will delete elements from it on the handler threads. @@ -695,6 +763,10 @@ 0, mCreatedNetworkMonitors.size()); assertEquals("BroadcastReceiver still registered after disconnect", 0, mRegisteredReceivers.size()); + quitThreads(); + quitExecutorServices(); + // Clear mocks to prevent from stubs holding instances and cause memory leaks. + Mockito.framework().clearInlineMocks(); } private void initHttpConnection(HttpURLConnection connection) {