From d38fb7662d0e525eee57062ee8c14f661b1aee2d Mon Sep 17 00:00:00 2001 From: Paul Jensen Date: Thu, 7 Jan 2016 23:13:19 -0500 Subject: [PATCH] Android packet filtering program interpreter test & program generator Change-Id: I17951bd6320b9eb3b6c43f2ae37f62c2025705c1 --- .../net/java/android/net/apf/ApfGenerator.java | 883 +++++++++++++++++++++ services/tests/servicestests/Android.mk | 35 + services/tests/servicestests/jni/apf_jni.cpp | 182 +++++ services/tests/servicestests/res/raw/apf.pcap | Bin 0 -> 5702 bytes .../src/com/android/server/ApfTest.java | 560 +++++++++++++ .../src/com/android/server/Bpf2Apf.java | 327 ++++++++ 6 files changed, 1987 insertions(+) create mode 100644 services/net/java/android/net/apf/ApfGenerator.java create mode 100644 services/tests/servicestests/jni/apf_jni.cpp create mode 100644 services/tests/servicestests/res/raw/apf.pcap create mode 100644 services/tests/servicestests/src/com/android/server/ApfTest.java create mode 100644 services/tests/servicestests/src/com/android/server/Bpf2Apf.java diff --git a/services/net/java/android/net/apf/ApfGenerator.java b/services/net/java/android/net/apf/ApfGenerator.java new file mode 100644 index 000000000000..96c2ba535dd1 --- /dev/null +++ b/services/net/java/android/net/apf/ApfGenerator.java @@ -0,0 +1,883 @@ +/* + * Copyright (C) 2016 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 java.util.ArrayList; +import java.util.HashMap; + +/** + * 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. + * + * @hide + */ +public class ApfGenerator { + /** + * This exception is thrown when an attempt is made to generate an illegal instruction. + */ + public static class IllegalInstructionException extends Exception { + IllegalInstructionException(String msg) { + super(msg); + } + } + private enum Opcodes { + LABEL(-1), + 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]" + LDBX(4), // Load 1 byte from immediate offset plus register, e.g. "ldbx R0, [5]R0" + LDHX(5), // Load 2 byte from immediate offset plus register, e.g. "ldhx R0, [5]R0" + LDWX(6), // Load 4 byte from immediate offset plus register, e.g. "ldwx R0, [5]R0" + ADD(7), // Add, e.g. "add R0,5" + MUL(8), // Multiply, e.g. "mul R0,5" + DIV(9), // Divide, e.g. "div R0,5" + AND(10), // And, e.g. "and R0,5" + 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" + 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" + JLT(18), // Compare less than and branch, e.g. "jlt R0,5,label" + JSET(19), // Compare any bits set and branch, e.g. "jset R0,5,label" + JNEBS(20), // Compare not equal byte sequence, e.g. "jnebs R0,5,label,0x1122334455" + EXT(21); // Followed by immediate indicating ExtendedOpcodes. + + final int value; + + private Opcodes(int value) { + this.value = value; + } + } + // Extended opcodes. Primary opcode is Opcodes.EXT. ExtendedOpcodes are encoded in the immediate + // field. + private 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" + NEG(33), // Negate, e.g. "neg R0" + SWAP(34), // Swap, e.g. "swap R0,R1" + MOVE(35); // Move, e.g. "move R0,R1" + + final int value; + + private ExtendedOpcodes(int value) { + this.value = value; + } + } + public enum Register { + R0(0), + R1(1); + + final int value; + + private Register(int value) { + this.value = value; + } + } + private class Instruction { + private final byte mOpcode; // A "Opcode" value. + private final byte mRegister; // A "Register" value. + private boolean mHasImm; + private byte mImmSize; + private boolean mImmSigned; + private int mImm; + // When mOpcode is a jump: + private byte mTargetLabelSize; + private String mTargetLabel; + // When mOpcode == Opcodes.LABEL: + private String mLabel; + // When mOpcode == Opcodes.JNEBS: + private byte[] mCompareBytes; + // Offset in bytes from the begining of this program. Set by {@link ApfGenerator#generate}. + int offset; + + Instruction(Opcodes opcode, Register register) { + mOpcode = (byte)opcode.value; + mRegister = (byte)register.value; + } + + Instruction(Opcodes opcode) { + this(opcode, Register.R0); + } + + void setImm(int imm, boolean signed) { + mHasImm = true; + mImm = imm; + mImmSigned = signed; + mImmSize = calculateImmSize(imm, signed); + } + + void setUnsignedImm(int imm) { + setImm(imm, false); + } + + void setSignedImm(int imm) { + setImm(imm, true); + } + + void setLabel(String label) throws IllegalInstructionException { + if (mLabels.containsKey(label)) { + throw new IllegalInstructionException("duplicate label " + label); + } + if (mOpcode != Opcodes.LABEL.value) { + throw new IllegalStateException("adding label to non-label instruction"); + } + mLabel = label; + mLabels.put(label, this); + } + + void setTargetLabel(String label) { + mTargetLabel = label; + mTargetLabelSize = 4; // May shrink later on in generate(). + } + + void setCompareBytes(byte[] bytes) { + if (mOpcode != Opcodes.JNEBS.value) { + throw new IllegalStateException("adding compare bytes to non-JNEBS instruction"); + } + mCompareBytes = bytes; + } + + /** + * @return size of instruction in bytes. + */ + int size() { + if (mOpcode == Opcodes.LABEL.value) { + return 0; + } + int size = 1; + if (mHasImm) { + size += generatedImmSize(); + } + if (mTargetLabel != null) { + size += generatedImmSize(); + } + if (mCompareBytes != null) { + size += mCompareBytes.length; + } + return size; + } + + /** + * Resize immediate value field so that it's only as big as required to + * contain the offset of the jump destination. + * @return {@code true} if shrunk. + */ + boolean shrink() throws IllegalInstructionException { + 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; + } + + /** + * Assemble value for instruction size field. + */ + private byte generateImmSizeField() { + byte immSize = generatedImmSize(); + // Encode size field to fit in 2 bits: 0->0, 1->1, 2->2, 3->4. + return immSize == 4 ? 3 : immSize; + } + + /** + * Assemble first byte of generated instruction. + */ + private byte generateInstructionByte() { + byte sizeField = generateImmSizeField(); + return (byte)((mOpcode << 3) | (sizeField << 1) | mRegister); + } + + /** + * Write {@code value} at offset {@code writingOffset} into {@code bytecode}. + * {@link generatedImmSize} bytes are written. {@code value} is truncated to + * {@code generatedImmSize} bytes. {@code value} is treated simply as a + * 32-bit value, so unsigned values should be zero extended and the truncation + * should simply throw away their zero-ed upper bits, and signed values should + * be sign extended and the truncation should simply throw away their signed + * upper bits. + */ + private int writeValue(int value, byte[] bytecode, int writingOffset) { + for (int i = generatedImmSize() - 1; i >= 0; i--) { + bytecode[writingOffset++] = (byte)((value >> (i * 8)) & 255); + } + return writingOffset; + } + + /** + * Generate bytecode for this instruction at offset {@link offset}. + */ + void generate(byte[] bytecode) throws IllegalInstructionException { + if (mOpcode == Opcodes.LABEL.value) { + return; + } + int writingOffset = offset; + bytecode[writingOffset++] = generateInstructionByte(); + if (mTargetLabel != null) { + writingOffset = writeValue(calculateTargetLabelOffset(), bytecode, writingOffset); + } + if (mHasImm) { + writingOffset = writeValue(mImm, bytecode, writingOffset); + } + if (mCompareBytes != null) { + System.arraycopy(mCompareBytes, 0, bytecode, writingOffset, mCompareBytes.length); + writingOffset += mCompareBytes.length; + } + if ((writingOffset - offset) != size()) { + throw new IllegalStateException("wrote " + (writingOffset - offset) + + " but should have written " + size()); + } + } + + /** + * Calculate the size of either the immediate field or the target label field, if either is + * present. Most instructions have either an immediate 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 field, because there is only one length field in the instruction + * byte, hence why this function simply takes the maximum of the two sizes, so neither is + * truncated. + */ + private byte generatedImmSize() { + return mImmSize > mTargetLabelSize ? mImmSize : mTargetLabelSize; + } + + private int calculateTargetLabelOffset() throws IllegalInstructionException { + Instruction targetLabelInstruction; + if (mTargetLabel == DROP_LABEL) { + targetLabelInstruction = mDropLabel; + } else if (mTargetLabel == PASS_LABEL) { + targetLabelInstruction = mPassLabel; + } else { + targetLabelInstruction = mLabels.get(mTargetLabel); + } + if (targetLabelInstruction == null) { + throw new IllegalInstructionException("label not found: " + mTargetLabel); + } + // Calculate distance from end of this instruction to instruction.offset. + final int targetLabelOffset = targetLabelInstruction.offset - (offset + size()); + if (targetLabelOffset < 0) { + throw new IllegalInstructionException("backward branches disallowed; label: " + + mTargetLabel); + } + return targetLabelOffset; + } + + private byte calculateImmSize(int imm, boolean signed) { + if (imm == 0) { + return 0; + } + if (signed && (imm >= -128 && imm <= 127) || + !signed && (imm >= 0 && imm <= 255)) { + return 1; + } + if (signed && (imm >= -32768 && imm <= 32767) || + !signed && (imm >= 0 && imm <= 65535)) { + return 2; + } + return 4; + } + } + + /** + * Jump to this label to terminate the program and indicate the packet + * should be dropped. + */ + public static final String DROP_LABEL = "__DROP__"; + + /** + * Jump to this label to terminate the program and indicate the packet + * should be passed to the AP. + */ + public static final String PASS_LABEL = "__PASS__"; + + /** + * Number of memory slots available for access via APF stores to memory and loads from memory. + * The memory slots are numbered 0 to {@code MEMORY_SLOTS} - 1. This must be kept in sync with + * the APF interpreter. + */ + public static final int MEMORY_SLOTS = 16; + + /** + * Memory slot number that is prefilled with the IPv4 header length. + * Note that this memory slot may be overwritten by a program that + * executes stores to this memory slot. This must be kept in sync with + * the APF interpreter. + */ + public static final int IPV4_HEADER_SIZE_MEMORY_SLOT = 13; + + /** + * Memory slot number that is prefilled with the size of the packet being filtered in bytes. + * Note that this memory slot may be overwritten by a program that + * executes stores to this memory slot. This must be kept in sync with the APF interpreter. + */ + public static final int PACKET_SIZE_MEMORY_SLOT = 14; + + /** + * Memory slot number that is prefilled with the age of the filter in seconds. The age of the + * filter is the time since the filter was installed until now. + * Note that this memory slot may be overwritten by a program that + * executes stores to this memory slot. This must be kept in sync with the APF interpreter. + */ + public static final int FILTER_AGE_MEMORY_SLOT = 15; + + /** + * First memory slot containing prefilled values. Can be used in range comparisons to determine + * if memory slot index is within prefilled slots. + */ + public static final int FIRST_PREFILLED_MEMORY_SLOT = IPV4_HEADER_SIZE_MEMORY_SLOT; + + /** + * Last memory slot containing prefilled values. Can be used in range comparisons to determine + * if memory slot index is within prefilled slots. + */ + public static final int LAST_PREFILLED_MEMORY_SLOT = FILTER_AGE_MEMORY_SLOT; + + private final ArrayList mInstructions = new ArrayList(); + private final HashMap mLabels = new HashMap(); + private final Instruction mDropLabel = new Instruction(Opcodes.LABEL); + private final Instruction mPassLabel = new Instruction(Opcodes.LABEL); + private boolean mGenerated; + + /** + * Set version of APF instruction set to generate instructions for. Returns {@code true} + * if generating for this version is supported, {@code false} otherwise. + */ + public boolean setApfVersion(int version) { + // This version number syncs up with APF_VERSION in hardware/google/apf/apf_interpreter.h + return version == 2; + } + + private void addInstruction(Instruction instruction) { + if (mGenerated) { + throw new IllegalStateException("Program already generated"); + } + mInstructions.add(instruction); + } + + /** + * Define a label at the current end of the program. Jumps can jump to this label. Labels are + * their own separate instructions, though with size 0. This facilitates having labels with + * no corresponding code to execute, for example a label at the end of a program. For example + * an {@link ApfGenerator} might be passed to a function that adds a filter like so: + *
+     *   load from packet
+     *   compare loaded data, jump if not equal to "next_filter"
+     *   load from packet
+     *   compare loaded data, jump if not equal to "next_filter"
+     *   jump to drop label
+     *   define "next_filter" here
+     * 
+ * 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; + } + + /** + * 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; + } + + /** + * Add an instruction to the end of the program to load the byte at offset {@code offset} + * bytes from the begining of the packet into {@code register}. + */ + public ApfGenerator addLoad8(Register register, int offset) { + Instruction instruction = new Instruction(Opcodes.LDB, register); + instruction.setUnsignedImm(offset); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to load 16-bits at offset {@code offset} + * bytes from the begining of the packet into {@code register}. + */ + public ApfGenerator addLoad16(Register register, int offset) { + Instruction instruction = new Instruction(Opcodes.LDH, register); + instruction.setUnsignedImm(offset); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to load 32-bits at offset {@code offset} + * bytes from the begining of the packet into {@code register}. + */ + public ApfGenerator addLoad32(Register register, int offset) { + Instruction instruction = new Instruction(Opcodes.LDW, register); + instruction.setUnsignedImm(offset); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to load a byte from the packet into + * {@code register}. The offset of the loaded byte from the begining 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.setUnsignedImm(offset); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to load 16-bits from the packet into + * {@code register}. The offset of the loaded 16-bits from the begining 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.setUnsignedImm(offset); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to load 32-bits from the packet into + * {@code register}. The offset of the loaded 32-bits from the begining 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.setUnsignedImm(offset); + addInstruction(instruction); + return this; + } + + /** + * 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.setSignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setSignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setSignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setSignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setSignedImm(-value); + addInstruction(instruction); + return this; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * Add an instruction to the end of the program to logically and register R0 with register R1 + * and store the result back into register R0. + */ + public ApfGenerator addAndR1() { + Instruction instruction = new Instruction(Opcodes.AND, Register.R1); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to logically or register R0 with register R1 + * and store the result back into register R0. + */ + public ApfGenerator addOrR1() { + Instruction instruction = new Instruction(Opcodes.OR, Register.R1); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to the end of the program to shift register R0 left by the value in + * register R1. + */ + public ApfGenerator addLeftShiftR1() { + Instruction instruction = new Instruction(Opcodes.SH, Register.R1); + addInstruction(instruction); + return this; + } + + /** + * 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.setSignedImm(value); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + instruction.setTargetLabel(target); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + instruction.setTargetLabel(target); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + instruction.setTargetLabel(target); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + instruction.setTargetLabel(target); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(value); + instruction.setTargetLabel(target); + addInstruction(instruction); + return this; + } + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * Add an instruction to the end of the program to jump to {@code target} if register R0's + * 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; + } + + /** + * 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; + } + + /** + * 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}, match {@code bytes}. + */ + public ApfGenerator addJumpIfBytesNotEqual(Register register, byte[] bytes, String target) { + Instruction instruction = new Instruction(Opcodes.JNEBS, register); + instruction.setUnsignedImm(bytes.length); + instruction.setTargetLabel(target); + instruction.setCompareBytes(bytes); + addInstruction(instruction); + return this; + } + + /** + * 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) + 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.setUnsignedImm(ExtendedOpcodes.LDM.value + slot); + addInstruction(instruction); + return this; + } + + /** + * 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) + 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.setUnsignedImm(ExtendedOpcodes.STM.value + slot); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(ExtendedOpcodes.NOT.value); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(ExtendedOpcodes.NEG.value); + addInstruction(instruction); + return this; + } + + /** + * Add an instruction to swap the values in register R0 and register R1. + */ + public ApfGenerator addSwap() { + Instruction instruction = new Instruction(Opcodes.EXT); + instruction.setUnsignedImm(ExtendedOpcodes.SWAP.value); + addInstruction(instruction); + return this; + } + + /** + * 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.setUnsignedImm(ExtendedOpcodes.MOVE.value); + addInstruction(instruction); + return this; + } + + /** + * Updates instruction offset fields using latest instruction sizes. + * @return current program length in bytes. + */ + private int updateInstructionOffsets() { + int offset = 0; + for (Instruction instruction : mInstructions) { + instruction.offset = offset; + offset += instruction.size(); + } + return offset; + } + + /** + * Returns an overestimate of the size of the generated program. {@link #generate} may return + * a program that is smaller. + */ + public int programLengthOverEstimate() { + return updateInstructionOffsets(); + } + + /** + * Generate the bytecode for the APF program. + * @return the bytecode. + * @throws IllegalStateException if a label is referenced but not defined. + */ + public byte[] generate() throws IllegalInstructionException { + // Enforce that we can only generate once because we cannot unshrink instructions and + // PASS/DROP labels may move further away requiring unshrinking if we add further + // instructions. + if (mGenerated) { + throw new IllegalStateException("Can only generate() once!"); + } + mGenerated = true; + int total_size; + boolean shrunk; + // Shrink the immediate value fields of instructions. + // As we shrink the instructions some branch offset + // fields may shrink also, thereby shrinking the + // instructions further. Loop until we've reached the + // minimum size. Rarely will this loop more than a few times. + // Limit iterations to avoid O(n^2) behavior. + int iterations_remaining = 10; + do { + total_size = updateInstructionOffsets(); + // Update drop and pass label offsets. + mDropLabel.offset = total_size + 1; + mPassLabel.offset = total_size; + // Limit run-time in aberant circumstances. + if (iterations_remaining-- == 0) break; + // Attempt to shrink instructions. + shrunk = false; + for (Instruction instruction : mInstructions) { + if (instruction.shrink()) { + shrunk = true; + } + } + } while (shrunk); + // Generate bytecode for instructions. + byte[] bytecode = new byte[total_size]; + for (Instruction instruction : mInstructions) { + instruction.generate(bytecode); + } + return bytecode; + } +} + diff --git a/services/tests/servicestests/Android.mk b/services/tests/servicestests/Android.mk index 33979b11d7a7..f933d10e5843 100644 --- a/services/tests/servicestests/Android.mk +++ b/services/tests/servicestests/Android.mk @@ -1,3 +1,7 @@ +######################################################################### +# Build FrameworksServicesTests package +######################################################################### + LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) @@ -21,5 +25,36 @@ LOCAL_PACKAGE_NAME := FrameworksServicesTests LOCAL_CERTIFICATE := platform +LOCAL_JNI_SHARED_LIBRARIES := libapfjni + include $(BUILD_PACKAGE) +######################################################################### +# Build JNI Shared Library +######################################################################### + +LOCAL_PATH:= $(LOCAL_PATH)/jni + +include $(CLEAR_VARS) + +LOCAL_MODULE_TAGS := tests + +LOCAL_CFLAGS := -Wall -Werror + +LOCAL_C_INCLUDES := \ + libpcap \ + hardware/google/apf + +LOCAL_SRC_FILES := apf_jni.cpp + +LOCAL_SHARED_LIBRARIES := \ + libnativehelper \ + liblog + +LOCAL_STATIC_LIBRARIES := \ + libpcap \ + libapf + +LOCAL_MODULE := libapfjni + +include $(BUILD_SHARED_LIBRARY) diff --git a/services/tests/servicestests/jni/apf_jni.cpp b/services/tests/servicestests/jni/apf_jni.cpp new file mode 100644 index 000000000000..7d142eb76b95 --- /dev/null +++ b/services/tests/servicestests/jni/apf_jni.cpp @@ -0,0 +1,182 @@ +/* + * Copyright 2016, 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. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "apf_interpreter.h" + +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) + +// JNI function acting as simply call-through to native APF interpreter. +static jint com_android_server_ApfTest_apfSimulate( + JNIEnv* env, jclass, jbyteArray program, jbyteArray packet, jint filter_age) { + return accept_packet( + (uint8_t*)env->GetByteArrayElements(program, NULL), + env->GetArrayLength(program), + (uint8_t*)env->GetByteArrayElements(packet, NULL), + env->GetArrayLength(packet), + filter_age); +} + +class ScopedPcap { + public: + ScopedPcap(pcap_t* pcap) : pcap_ptr(pcap) {} + ~ScopedPcap() { + pcap_close(pcap_ptr); + } + + pcap_t* get() const { return pcap_ptr; }; + private: + pcap_t* const pcap_ptr; +}; + +class ScopedFILE { + public: + ScopedFILE(FILE* fp) : file(fp) {} + ~ScopedFILE() { + fclose(file); + } + + FILE* get() const { return file; }; + private: + FILE* const file; +}; + +static void throwException(JNIEnv* env, const std::string& error) { + jclass newExcCls = env->FindClass("java/lang/IllegalStateException"); + if (newExcCls == 0) { + abort(); + return; + } + env->ThrowNew(newExcCls, error.c_str()); +} + +static jstring com_android_server_ApfTest_compileToBpf(JNIEnv* env, jclass, jstring jfilter) { + ScopedUtfChars filter(env, jfilter); + std::string bpf_string; + ScopedPcap pcap(pcap_open_dead(DLT_EN10MB, 65535)); + if (pcap.get() == NULL) { + throwException(env, "pcap_open_dead failed"); + return NULL; + } + + // Compile "filter" to a BPF program + bpf_program bpf; + if (pcap_compile(pcap.get(), &bpf, filter.c_str(), 0, PCAP_NETMASK_UNKNOWN)) { + throwException(env, "pcap_compile failed"); + return NULL; + } + + // Translate BPF program to human-readable format + const struct bpf_insn* insn = bpf.bf_insns; + for (uint32_t i = 0; i < bpf.bf_len; i++) { + bpf_string += bpf_image(insn++, i); + bpf_string += "\n"; + } + + return env->NewStringUTF(bpf_string.c_str()); +} + +static jboolean com_android_server_ApfTest_compareBpfApf(JNIEnv* env, jclass, jstring jfilter, + jstring jpcap_filename, jbyteArray japf_program) { + ScopedUtfChars filter(env, jfilter); + ScopedUtfChars pcap_filename(env, jpcap_filename); + const uint8_t* apf_program = (uint8_t*)env->GetByteArrayElements(japf_program, NULL); + const uint32_t apf_program_len = env->GetArrayLength(japf_program); + + // Open pcap file for BPF filtering + ScopedFILE bpf_fp(fopen(pcap_filename.c_str(), "rb")); + char pcap_error[PCAP_ERRBUF_SIZE]; + ScopedPcap bpf_pcap(pcap_fopen_offline(bpf_fp.get(), pcap_error)); + if (bpf_pcap.get() == NULL) { + throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error)); + return false; + } + + // Open pcap file for APF filtering + ScopedFILE apf_fp(fopen(pcap_filename.c_str(), "rb")); + ScopedPcap apf_pcap(pcap_fopen_offline(apf_fp.get(), pcap_error)); + if (apf_pcap.get() == NULL) { + throwException(env, "pcap_fopen_offline failed: " + std::string(pcap_error)); + return false; + } + + // Compile "filter" to a BPF program + bpf_program bpf; + if (pcap_compile(bpf_pcap.get(), &bpf, filter.c_str(), 0, PCAP_NETMASK_UNKNOWN)) { + throwException(env, "pcap_compile failed"); + return false; + } + + // Install BPF filter on bpf_pcap + if (pcap_setfilter(bpf_pcap.get(), &bpf)) { + throwException(env, "pcap_setfilter failed"); + return false; + } + + while (1) { + pcap_pkthdr bpf_header, apf_header; + // Run BPF filter to the next matching packet. + const uint8_t* bpf_packet = pcap_next(bpf_pcap.get(), &bpf_header); + + // Run APF filter to the next matching packet. + const uint8_t* apf_packet; + do { + apf_packet = pcap_next(apf_pcap.get(), &apf_header); + } while (apf_packet != NULL && !accept_packet( + apf_program, apf_program_len, apf_packet, apf_header.len, 0)); + + // Make sure both filters matched the same packet. + if (apf_packet == NULL && bpf_packet == NULL) + break; + if (apf_packet == NULL || bpf_packet == NULL) + return false; + if (apf_header.len != bpf_header.len || + apf_header.ts.tv_sec != bpf_header.ts.tv_sec || + apf_header.ts.tv_usec != bpf_header.ts.tv_usec || + memcmp(apf_packet, bpf_packet, apf_header.len)) + return false; + } + return true; +} + +extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + ALOGE("ERROR: GetEnv failed"); + return -1; + } + + static JNINativeMethod gMethods[] = { + { "apfSimulate", "([B[BI)I", + (void*)com_android_server_ApfTest_apfSimulate }, + { "compileToBpf", "(Ljava/lang/String;)Ljava/lang/String;", + (void*)com_android_server_ApfTest_compileToBpf }, + { "compareBpfApf", "(Ljava/lang/String;Ljava/lang/String;[B)Z", + (void*)com_android_server_ApfTest_compareBpfApf }, + }; + + jniRegisterNativeMethods(env, "com/android/server/ApfTest", + gMethods, ARRAY_SIZE(gMethods)); + + return JNI_VERSION_1_6; +} diff --git a/services/tests/servicestests/res/raw/apf.pcap b/services/tests/servicestests/res/raw/apf.pcap new file mode 100644 index 0000000000000000000000000000000000000000..963165f19f7382f95f5db8f914ac632f51c86c5b GIT binary patch literal 5702 zcmeHLdrVVj6#s640)=96Mlo~l`WPk!N((KIJ4GHs3S$Mzb5WF13KaVwv@nP-W^|gU z_{x}nOr6e%Q4yxuoUZ}XxGgT*Okl=W#>QNRBF0QNW{~cjdnpA94elRV!cET2ef9S{ z=X~d!@AR}^K7EKs+=>0L*$AQFCU{+a&MOHXrcIpjsUV~4M)&u(`{YIwU07g{7cO73_o+I$dcAR5``qBN$A3|0ym~I) zsmWPNeYbg_r(;Fald~T$dM=ONW}_ZRcPn$>h+k+*c~l>)`Q+h6)cPQ3T?}L>PFm*$ z`m$QDAF1{Hg+p4WJ83O4wpV>Qcc84!P(1hPv4Tyr;-mIIe?94&SYLI(0-xU3@{l$6 zfCq^OMikCV95?uiCURj~ERpm5($P=I+*CqlB4bAX{Drk5H@DvlIact~GV~WBo^y?u z&LXCbfv98=V@4u|0HWW0Z;}f7p^WkjAu-eC;2HlT&8Qy;b?^pJ5b-8{*Exs>3<)?q ziZpsl4ZpIm(qyf4)0&O=hC=Kf*VqacyUC6%t>>^C5?o@>;jmkKyomwWQAYWekd=fW z_UBhnXT&E8&^%>_vFqG>wV)kue7#$JQG0m*6LDXDX+`v?`$)7))a&q>6F2yn<^IjV z4VH<6yc-f&ZloT|XJD5Eup1xoNICGMP-Nu>+p~I6%#ltN#0;SzB4UZV|78teZ+T?1 zJ=<>eG*GIx)2g(ruhrqhOop~tipm*{L0<(AG85G~iE4&U z)tf4-L&GEylJJP2U^+Y9gepse>2!02Ra0ot(V5HMqKnNH>?@kXv%n|eXjfiHwnbMF z5?7)#SsA)QS7b4p^je`HI}=o{tkf4VVNp?1lo0kL!z&G*qUADaxK5+cN##0*ane^P zK>vTS(S6GQFVX*{0en&ev5(>u|EmY%KW{|*=Pv4>4DmnF(3|9$Arx?l@YN~LsfBo% zCCa`K8xy@rGh_pbQ$`#f%!qj-GNSIDZZc# z!f;ic$xVt|#_kskl=jKKW4x$6Tp~^DcXSJ5bXXDCEgU%o>&JFjb20`yEG4_#<8=$r zqu_4U>l!OYiGN}9_&OO4!l^01)8V$mvkv7++lUF9aiK}hsE7Tqg`P{ zFw~V!q{9Ns#BrC_&Ynnz)qGnpl~0qR1N(3Jlw$EE=OKVmoa*7p!Fo8$UJp;KhYoq4 zRx#P@A@sD^He*4|O^s6}m`%m{lK*>A9M>qWGKPEkO|9hQ`9tI?9bK$sDAa3`;X`s@ zuRW4X0=No0P(f&yf@^0zlx3~(sJS-kj!@*;e9LUF4E zRT8Qw9@ryrSN8MuoIroX^6c{_Vt9x$d~JuYcpL*FI8f#A%f?(sR1DX`IV#3muhCIq zUq@G@C%6^>T0g0&d>tA*l4<8q`{AoJyY+A|or+&xVx3>JS$j$0``0?zr zKaf?J>|h;k6FX(i3lu_M$M0T;9!F+w$~s>O)5GiRlyzucd9ntXk@bdm!whD7Y0FY^ z-Dlw+=CnUii1oL+`EE^RZ|)KuD2_qa&Z92pm1@@3w@2A}9p;q=oG$(d@q^LMYpder I7o4p57c$AvCIA2c literal 0 HcmV?d00001 diff --git a/services/tests/servicestests/src/com/android/server/ApfTest.java b/services/tests/servicestests/src/com/android/server/ApfTest.java new file mode 100644 index 000000000000..640a6c927ed3 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/ApfTest.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2012 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 com.android.server; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.LargeTest; + +import com.android.frameworks.servicestests.R; +import android.net.apf.ApfGenerator; +import android.net.apf.ApfGenerator.IllegalInstructionException; +import android.net.apf.ApfGenerator.Register; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +import libcore.io.IoUtils; +import libcore.io.Streams; + +/** + * Tests for APF program generator and interpreter. + * + * Build, install and run with: + * runtest frameworks-services -c com.android.server.ApfTest + */ +public class ApfTest extends AndroidTestCase { + @Override + public void setUp() throws Exception { + super.setUp(); + // Load up native shared library containing APF interpreter exposed via JNI. + System.loadLibrary("apfjni"); + } + + // Expected return codes from APF interpreter. + private final static int PASS = 1; + private final static int DROP = 0; + // Interpreter will just accept packets without link layer headers, so pad fake packet to at + // least the minimum packet size. + private final static int MIN_PKT_SIZE = 15; + + private void assertVerdict(int expected, byte[] program, byte[] packet, int filterAge) { + assertEquals(expected, apfSimulate(program, packet, filterAge)); + } + + private void assertPass(byte[] program, byte[] packet, int filterAge) { + assertVerdict(PASS, program, packet, filterAge); + } + + private void assertDrop(byte[] program, byte[] packet, int filterAge) { + assertVerdict(DROP, program, packet, filterAge); + } + + private void assertVerdict(int expected, ApfGenerator gen, byte[] packet, int filterAge) + throws IllegalInstructionException { + assertEquals(expected, apfSimulate(gen.generate(), packet, filterAge)); + } + + private void assertPass(ApfGenerator gen, byte[] packet, int filterAge) + throws IllegalInstructionException { + assertVerdict(PASS, gen, packet, filterAge); + } + + private void assertDrop(ApfGenerator gen, byte[] packet, int filterAge) + throws IllegalInstructionException { + assertVerdict(DROP, gen, packet, filterAge); + } + + private void assertPass(ApfGenerator gen) + throws IllegalInstructionException { + assertVerdict(PASS, gen, new byte[MIN_PKT_SIZE], 0); + } + + private void assertDrop(ApfGenerator gen) + throws IllegalInstructionException { + assertVerdict(DROP, gen, new byte[MIN_PKT_SIZE], 0); + } + + /** + * Test each instruction by generating a program containing the instruction, + * generating bytecode for that program and running it through the + * interpreter to verify it functions correctly. + */ + @LargeTest + public void testApfInstructions() throws IllegalInstructionException { + // Empty program should pass because having the program counter reach the + // location immediately after the program indicates the packet should be + // passed to the AP. + ApfGenerator gen = new ApfGenerator(); + assertPass(gen); + + // Test jumping to pass label. + gen = new ApfGenerator(); + gen.addJump(gen.PASS_LABEL); + byte[] program = gen.generate(); + assertEquals(1, program.length); + assertEquals((14 << 3) | (0 << 1) | 0, program[0]); + assertPass(program, new byte[MIN_PKT_SIZE], 0); + + // Test jumping to drop label. + gen = new ApfGenerator(); + gen.addJump(gen.DROP_LABEL); + program = gen.generate(); + assertEquals(2, program.length); + assertEquals((14 << 3) | (1 << 1) | 0, program[0]); + assertEquals(1, program[1]); + assertDrop(program, new byte[15], 15); + + // Test jumping if equal to 0. + gen = new ApfGenerator(); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if not equal to 0. + gen = new ApfGenerator(); + gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0NotEquals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if registers equal. + gen = new ApfGenerator(); + gen.addJumpIfR0EqualsR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if registers not equal. + gen = new ApfGenerator(); + gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0NotEqualsR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test load immediate. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test add. + gen = new ApfGenerator(); + gen.addAdd(1234567890); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test subtract. + gen = new ApfGenerator(); + gen.addAdd(-1234567890); + gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test or. + gen = new ApfGenerator(); + gen.addOr(1234567890); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test and. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addAnd(123456789); + gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL); + assertDrop(gen); + + // Test left shift. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLeftShift(1); + gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test right shift. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addRightShift(1); + gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test multiply. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addMul(2); + gen.addJumpIfR0Equals(1234567890 * 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addDiv(2); + gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide by zero. + gen = new ApfGenerator(); + gen.addDiv(0); + gen.addJump(gen.DROP_LABEL); + assertPass(gen); + + // Test add. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addAddR1(); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test subtract. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, -1234567890); + gen.addAddR1(); + gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test or. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addOrR1(); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test and. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 123456789); + gen.addAndR1(); + gen.addJumpIfR0Equals(1234567890 & 123456789, gen.DROP_LABEL); + assertDrop(gen); + + // Test left shift. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 1); + gen.addLeftShiftR1(); + gen.addJumpIfR0Equals(1234567890 << 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test right shift. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, -1); + gen.addLeftShiftR1(); + gen.addJumpIfR0Equals(1234567890 >> 1, gen.DROP_LABEL); + assertDrop(gen); + + // Test multiply. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 2); + gen.addMulR1(); + gen.addJumpIfR0Equals(1234567890 * 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addLoadImmediate(Register.R1, 2); + gen.addDivR1(); + gen.addJumpIfR0Equals(1234567890 / 2, gen.DROP_LABEL); + assertDrop(gen); + + // Test divide by zero. + gen = new ApfGenerator(); + gen.addDivR1(); + gen.addJump(gen.DROP_LABEL); + assertPass(gen); + + // Test byte load. + gen = new ApfGenerator(); + gen.addLoad8(Register.R0, 1); + gen.addJumpIfR0Equals(45, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test out of bounds load. + gen = new ApfGenerator(); + gen.addLoad8(Register.R0, 16); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test half-word load. + gen = new ApfGenerator(); + gen.addLoad16(Register.R0, 1); + gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test word load. + gen = new ApfGenerator(); + gen.addLoad32(Register.R0, 1); + gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test byte indexed load. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1); + gen.addLoad8Indexed(Register.R0, 0); + gen.addJumpIfR0Equals(45, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test out of bounds indexed load. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 8); + gen.addLoad8Indexed(Register.R0, 8); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertPass(gen, new byte[]{123,45,0,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test half-word indexed load. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1); + gen.addLoad16Indexed(Register.R0, 0); + gen.addJumpIfR0Equals((45 << 8) | 67, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,0,0,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test word indexed load. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1); + gen.addLoad32Indexed(Register.R0, 0); + gen.addJumpIfR0Equals((45 << 24) | (67 << 16) | (89 << 8) | 12, gen.DROP_LABEL); + assertDrop(gen, new byte[]{123,45,67,89,12,0,0,0,0,0,0,0,0,0,0}, 0); + + // Test jumping if greater than. + gen = new ApfGenerator(); + gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0GreaterThan(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if less than. + gen = new ApfGenerator(); + gen.addJumpIfR0LessThan(0, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addJumpIfR0LessThan(1, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if any bits set. + gen = new ApfGenerator(); + gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 3); + gen.addJumpIfR0AnyBitsSet(3, gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if register greater than. + gen = new ApfGenerator(); + gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 2); + gen.addLoadImmediate(Register.R1, 1); + gen.addJumpIfR0GreaterThanR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if register less than. + gen = new ApfGenerator(); + gen.addJumpIfR0LessThanR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1); + gen.addJumpIfR0LessThanR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test jumping if any bits set in register. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 3); + gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL); + assertPass(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 3); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 3); + gen.addLoadImmediate(Register.R0, 3); + gen.addJumpIfR0AnyBitsSetR1(gen.DROP_LABEL); + assertDrop(gen); + + // Test load from memory. + gen = new ApfGenerator(); + gen.addLoadFromMemory(Register.R0, 0); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test store to memory. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addStoreToMemory(Register.R1, 12); + gen.addLoadFromMemory(Register.R0, 12); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test filter age pre-filled memory. + gen = new ApfGenerator(); + gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen, new byte[MIN_PKT_SIZE], 1234567890); + + // Test packet size pre-filled memory. + gen = new ApfGenerator(); + gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT); + gen.addJumpIfR0Equals(MIN_PKT_SIZE, gen.DROP_LABEL); + assertDrop(gen); + + // Test IPv4 header size pre-filled memory. + gen = new ApfGenerator(); + gen.addLoadFromMemory(Register.R0, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + gen.addJumpIfR0Equals(20, gen.DROP_LABEL); + assertDrop(gen, new byte[]{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x45}, 0); + + // Test not. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addNot(Register.R0); + gen.addJumpIfR0Equals(~1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test negate. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addNeg(Register.R0); + gen.addJumpIfR0Equals(-1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test move. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addMove(Register.R0); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addMove(Register.R1); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + + // Test swap. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R1, 1234567890); + gen.addSwap(); + gen.addJumpIfR0Equals(1234567890, gen.DROP_LABEL); + assertDrop(gen); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1234567890); + gen.addSwap(); + gen.addJumpIfR0Equals(0, gen.DROP_LABEL); + assertDrop(gen); + + // Test jump if bytes not equal. + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL); + program = gen.generate(); + assertEquals(6, program.length); + assertEquals((13 << 3) | (1 << 1) | 0, program[0]); + assertEquals(1, program[1]); + assertEquals(((20 << 3) | (1 << 1) | 0) - 256, program[2]); + assertEquals(1, program[3]); + assertEquals(1, program[4]); + assertEquals(123, program[5]); + assertDrop(program, new byte[MIN_PKT_SIZE], 0); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL); + byte[] packet123 = new byte[]{0,123,0,0,0,0,0,0,0,0,0,0,0,0,0}; + assertPass(gen, packet123, 0); + gen = new ApfGenerator(); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{123}, gen.DROP_LABEL); + assertDrop(gen, packet123, 0); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,30,4,5}, gen.DROP_LABEL); + byte[] packet12345 = new byte[]{0,1,2,3,4,5,0,0,0,0,0,0,0,0,0}; + assertDrop(gen, packet12345, 0); + gen = new ApfGenerator(); + gen.addLoadImmediate(Register.R0, 1); + gen.addJumpIfBytesNotEqual(Register.R0, new byte[]{1,2,3,4,5}, gen.DROP_LABEL); + assertPass(gen, packet12345, 0); + } + + /** + * Generate some BPF programs, translate them to APF, then run APF and BPF programs + * over packet traces and verify both programs filter out the same packets. + */ + @LargeTest + public void testApfAgainstBpf() throws Exception { + String[] tcpdump_filters = new String[]{ "udp", "tcp", "icmp", "icmp6", "udp port 53", + "arp", "dst 239.255.255.250", "arp or tcp or udp port 53", "net 192.168.1.0/24", + "arp or icmp6 or portrange 53-54", "portrange 53-54 or portrange 100-50000", + "tcp[tcpflags] & (tcp-ack|tcp-fin) != 0 and (ip[2:2] > 57 or icmp)" }; + String pcap_filename = stageFile(R.raw.apf); + for (String tcpdump_filter : tcpdump_filters) { + byte[] apf_program = Bpf2Apf.convert(compileToBpf(tcpdump_filter)); + assertTrue("Failed to match for filter: " + tcpdump_filter, + compareBpfApf(tcpdump_filter, pcap_filename, apf_program)); + } + } + + /** + * Stage a file for testing, i.e. make it native accessible. Given a resource ID, + * copy that resource into the app's data directory and return the path to it. + */ + private String stageFile(int rawId) throws Exception { + File file = new File(getContext().getFilesDir(), "staged_file"); + new File(file.getParent()).mkdirs(); + InputStream in = null; + OutputStream out = null; + try { + in = getContext().getResources().openRawResource(rawId); + out = new FileOutputStream(file); + Streams.copy(in, out); + } finally { + if (in != null) in.close(); + if (out != null) out.close(); + } + return file.getAbsolutePath(); + } + + /** + * Call the APF interpreter the run {@code program} on {@code packet} pretending the + * filter was installed {@code filter_age} seconds ago. + */ + private native static int apfSimulate(byte[] program, byte[] packet, int filter_age); + + /** + * Compile a tcpdump human-readable filter (e.g. "icmp" or "tcp port 54") into a BPF + * prorgam and return a human-readable dump of the BPF program identical to "tcpdump -d". + */ + private native static String compileToBpf(String filter); + + /** + * Open packet capture file {@code pcap_filename} and filter the packets using tcpdump + * human-readable filter (e.g. "icmp" or "tcp port 54") compiled to a BPF program and + * at the same time using APF program {@code apf_program}. Return {@code true} if + * both APF and BPF programs filter out exactly the same packets. + */ + private native static boolean compareBpfApf(String filter, String pcap_filename, + byte[] apf_program); +} diff --git a/services/tests/servicestests/src/com/android/server/Bpf2Apf.java b/services/tests/servicestests/src/com/android/server/Bpf2Apf.java new file mode 100644 index 000000000000..29594a84fc48 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/Bpf2Apf.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2015 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 com.android.server; + +import android.net.apf.ApfGenerator; +import android.net.apf.ApfGenerator.IllegalInstructionException; +import android.net.apf.ApfGenerator.Register; + +import java.io.BufferedReader; +import java.io.InputStreamReader; + +/** + * BPF to APF translator. + * + * Note: This is for testing purposes only and is not guaranteed to support + * translation of all BPF programs. + * + * Example usage: + * javac net/java/android/net/apf/ApfGenerator.java \ + * tests/servicestests/src/com/android/server/Bpf2Apf.java + * sudo tcpdump -i em1 -d icmp | java -classpath tests/servicestests/src:net/java \ + * com.android.server.Bpf2Apf + */ +public class Bpf2Apf { + private static int parseImm(String line, String arg) { + if (!arg.startsWith("#0x")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + final long val_long = Long.parseLong(arg.substring(3), 16); + if (val_long < 0 || val_long > Long.parseLong("ffffffff", 16)) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + return new Long((val_long << 32) >> 32).intValue(); + } + + /** + * Convert a single line of "tcpdump -d" (human readable BPF program dump) {@code line} into + * APF instruction(s) and append them to {@code gen}. Here's an example line: + * (001) jeq #0x86dd jt 2 jf 7 + */ + private static void convertLine(String line, ApfGenerator gen) + throws IllegalInstructionException { + if (line.indexOf("(") != 0 || line.indexOf(")") != 4 || line.indexOf(" ") != 5) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + int label = Integer.parseInt(line.substring(1, 4)); + gen.defineLabel(Integer.toString(label)); + String opcode = line.substring(6, 10).trim(); + String arg = line.substring(15, Math.min(32, line.length())).trim(); + switch (opcode) { + case "ld": + case "ldh": + case "ldb": + case "ldx": + case "ldxb": + case "ldxh": + Register dest = opcode.contains("x") ? Register.R1 : Register.R0; + if (arg.equals("4*([14]&0xf)")) { + if (!opcode.equals("ldxb")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadFromMemory(dest, gen.IPV4_HEADER_SIZE_MEMORY_SLOT); + break; + } + if (arg.equals("#pktlen")) { + if (!opcode.equals("ld")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadFromMemory(dest, gen.PACKET_SIZE_MEMORY_SLOT); + break; + } + if (arg.startsWith("#0x")) { + if (!opcode.equals("ld")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadImmediate(dest, parseImm(line, arg)); + break; + } + if (arg.startsWith("M[")) { + if (!opcode.startsWith("ld")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1)); + if (memory_slot < 0 || memory_slot >= gen.MEMORY_SLOTS || + // Disallow use of pre-filled slots as BPF programs might + // wrongfully assume they're initialized to 0. + (memory_slot >= gen.FIRST_PREFILLED_MEMORY_SLOT && + memory_slot <= gen.LAST_PREFILLED_MEMORY_SLOT)) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addLoadFromMemory(dest, memory_slot); + break; + } + if (arg.startsWith("[x + ")) { + int offset = Integer.parseInt(arg.substring(5, arg.length() - 1)); + switch (opcode) { + case "ld": + case "ldx": + gen.addLoad32Indexed(dest, offset); + break; + case "ldh": + case "ldxh": + gen.addLoad16Indexed(dest, offset); + break; + case "ldb": + case "ldxb": + gen.addLoad8Indexed(dest, offset); + break; + } + } else { + int offset = Integer.parseInt(arg.substring(1, arg.length() - 1)); + switch (opcode) { + case "ld": + case "ldx": + gen.addLoad32(dest, offset); + break; + case "ldh": + case "ldxh": + gen.addLoad16(dest, offset); + break; + case "ldb": + case "ldxb": + gen.addLoad8(dest, offset); + break; + } + } + break; + case "st": + case "stx": + Register src = opcode.contains("x") ? Register.R1 : Register.R0; + if (!arg.startsWith("M[")) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + int memory_slot = Integer.parseInt(arg.substring(2, arg.length() - 1)); + if (memory_slot < 0 || memory_slot >= gen.MEMORY_SLOTS || + // Disallow overwriting pre-filled slots + (memory_slot >= gen.FIRST_PREFILLED_MEMORY_SLOT && + memory_slot <= gen.LAST_PREFILLED_MEMORY_SLOT)) { + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + gen.addStoreToMemory(src, memory_slot); + break; + case "add": + case "and": + case "or": + case "sub": + if (arg.equals("x")) { + switch(opcode) { + case "add": + gen.addAddR1(); + break; + case "and": + gen.addAndR1(); + break; + case "or": + gen.addOrR1(); + break; + case "sub": + gen.addNeg(Register.R1); + gen.addAddR1(); + gen.addNeg(Register.R1); + break; + } + } else { + int imm = parseImm(line, arg); + switch(opcode) { + case "add": + gen.addAdd(imm); + break; + case "and": + gen.addAnd(imm); + break; + case "or": + gen.addOr(imm); + break; + case "sub": + gen.addAdd(-imm); + break; + } + } + break; + case "jeq": + case "jset": + case "jgt": + case "jge": + int val = 0; + boolean reg_compare; + if (arg.startsWith("x")) { + reg_compare = true; + } else { + reg_compare = false; + val = parseImm(line, arg); + } + int jt_offset = line.indexOf("jt"); + int jf_offset = line.indexOf("jf"); + String true_label = line.substring(jt_offset + 2, jf_offset).trim(); + String false_label = line.substring(jf_offset + 2).trim(); + boolean true_label_is_fallthrough = Integer.parseInt(true_label) == label + 1; + boolean false_label_is_fallthrough = Integer.parseInt(false_label) == label + 1; + if (true_label_is_fallthrough && false_label_is_fallthrough) + break; + switch (opcode) { + case "jeq": + if (!true_label_is_fallthrough) { + if (reg_compare) { + gen.addJumpIfR0EqualsR1(true_label); + } else { + gen.addJumpIfR0Equals(val, true_label); + } + } + if (!false_label_is_fallthrough) { + if (!true_label_is_fallthrough) { + gen.addJump(false_label); + } else if (reg_compare) { + gen.addJumpIfR0NotEqualsR1(false_label); + } else { + gen.addJumpIfR0NotEquals(val, false_label); + } + } + break; + case "jset": + if (reg_compare) { + gen.addJumpIfR0AnyBitsSetR1(true_label); + } else { + gen.addJumpIfR0AnyBitsSet(val, true_label); + } + if (!false_label_is_fallthrough) { + gen.addJump(false_label); + } + break; + case "jgt": + if (!true_label_is_fallthrough || + // We have no less-than-or-equal-to register to register + // comparison instruction, so in this case we'll jump + // around an unconditional jump. + (!false_label_is_fallthrough && reg_compare)) { + if (reg_compare) { + gen.addJumpIfR0GreaterThanR1(true_label); + } else { + gen.addJumpIfR0GreaterThan(val, true_label); + } + } + if (!false_label_is_fallthrough) { + if (!true_label_is_fallthrough || reg_compare) { + gen.addJump(false_label); + } else { + gen.addJumpIfR0LessThan(val + 1, false_label); + } + } + break; + case "jge": + if (!false_label_is_fallthrough || + // We have no greater-than-or-equal-to register to register + // comparison instruction, so in this case we'll jump + // around an unconditional jump. + (!true_label_is_fallthrough && reg_compare)) { + if (reg_compare) { + gen.addJumpIfR0LessThanR1(false_label); + } else { + gen.addJumpIfR0LessThan(val, false_label); + } + } + if (!true_label_is_fallthrough) { + if (!false_label_is_fallthrough || reg_compare) { + gen.addJump(true_label); + } else { + gen.addJumpIfR0GreaterThan(val - 1, true_label); + } + } + break; + } + break; + case "ret": + if (arg.equals("#0")) { + gen.addJump(gen.DROP_LABEL); + } else { + gen.addJump(gen.PASS_LABEL); + } + break; + case "tax": + gen.addMove(Register.R1); + break; + case "txa": + gen.addMove(Register.R0); + break; + default: + throw new IllegalArgumentException("Unhandled instruction: " + line); + } + } + + /** + * Convert the output of "tcpdump -d" (human readable BPF program dump) {@code bpf} into an APF + * program and return it. + */ + public static byte[] convert(String bpf) throws IllegalInstructionException { + ApfGenerator gen = new ApfGenerator(); + for (String line : bpf.split("\\n")) convertLine(line, gen); + return gen.generate(); + } + + /** + * Convert the output of "tcpdump -d" (human readable BPF program dump) piped in stdin into an + * APF program and output it via stdout. + */ + public static void main(String[] args) throws Exception { + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + String line = null; + StringBuilder responseData = new StringBuilder(); + ApfGenerator gen = new ApfGenerator(); + while ((line = in.readLine()) != null) convertLine(line, gen); + System.out.write(gen.generate()); + } +} -- 2.11.0