Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8b946b4a authored by Paul Jensen's avatar Paul Jensen Committed by Android (Google) Code Review
Browse files

Merge "Add basic packet filtering via APF program." into nyc-dev

parents 77dc205b dfd5a949
Loading
Loading
Loading
Loading
+257 −57
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import android.system.PacketSocketAddress;
import android.util.Log;
import android.util.Pair;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.HexDump;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.ConnectivityService;
@@ -52,6 +53,17 @@ import libcore.io.IoBridge;
 * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to
 * filter out redundant duplicate ones.
 *
 * Threading model:
 * A collection of RAs we've received is kept in mRas. Generating APF programs uses mRas to
 * know what RAs to filter for, thus generating APF programs is dependent on mRas.
 * mRas can be accessed by multiple threads:
 * - ReceiveThread, which listens for RAs and adds them to mRas, and generates APF programs.
 * - callers of:
 *    - setMulticastFilter(), which can cause an APF program to be generated.
 *    - dump(), which dumps mRas among other things.
 *    - shutdown(), which clears mRas.
 * So access to mRas is synchronized.
 *
 * @hide
 */
public class ApfFilter {
@@ -93,11 +105,46 @@ public class ApfFilter {
    private static final boolean DBG = true;
    private static final boolean VDBG = false;

    private static final int ETH_HEADER_LEN = 14;
    private static final int ETH_ETHERTYPE_OFFSET = 12;
    private static final byte[] ETH_BROADCAST_MAC_ADDRESS = new byte[]{
            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff };
    // TODO: Make these offsets relative to end of link-layer header; don't include ETH_HEADER_LEN.
    private static final int IPV4_FRAGMENT_OFFSET_OFFSET = ETH_HEADER_LEN + 6;
    // Endianness is not an issue for this constant because the APF interpreter always operates in
    // network byte order.
    private static final int IPV4_FRAGMENT_OFFSET_MASK = 0x1fff;
    private static final int IPV4_PROTOCOL_OFFSET = ETH_HEADER_LEN + 9;
    private static final int IPV4_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 16;

    private static final int IPV6_NEXT_HEADER_OFFSET = ETH_HEADER_LEN + 6;
    private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
    private static final int IPV6_DEST_ADDR_OFFSET = ETH_HEADER_LEN + 24;
    private static final int IPV6_HEADER_LEN = 40;
    // The IPv6 all nodes address ff02::1
    private static final byte[] IPV6_ALL_NODES_ADDRESS =
            new byte[]{ (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };

    private static final int ICMP6_TYPE_OFFSET = ETH_HEADER_LEN + IPV6_HEADER_LEN;
    private static final int ICMP6_NEIGHBOR_ANNOUNCEMENT = 136;

    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
    private static final int UDP_DESTINATION_PORT_OFFSET = ETH_HEADER_LEN + 2;
    private static final int UDP_HEADER_LEN = 8;

    private static final int DHCP_CLIENT_PORT = 68;
    // NOTE: this must be added to the IPv4 header length in IPV4_HEADER_SIZE_MEMORY_SLOT
    private static final int DHCP_CLIENT_MAC_OFFSET = ETH_HEADER_LEN + UDP_HEADER_LEN + 28;

    private final ConnectivityService mConnectivityService;
    private final NetworkAgentInfo mNai;
    private ReceiveThread mReceiveThread;
    private String mIfaceName;
    private byte[] mIfaceMac;
    @GuardedBy("this")
    private long mUniqueCounter;
    @GuardedBy("this")
    private boolean mMulticastFilter;

    private ApfFilter(ConnectivityService connectivityService, NetworkAgentInfo nai) {
        mConnectivityService = connectivityService;
@@ -109,7 +156,8 @@ public class ApfFilter {
        Log.d(TAG, "(" + mNai.network.netId + "): " + s);
    }

    private long getUniqueNumber() {
    @GuardedBy("this")
    private long getUniqueNumberLocked() {
        return mUniqueCounter++;
    }

@@ -122,13 +170,25 @@ public class ApfFilter {
        if (mIfaceName == null) return;
        FileDescriptor socket;
        try {
            NetworkInterface networkInterface = NetworkInterface.getByName(mIfaceName);
            if (networkInterface == null) {
                Log.e(TAG, "Can't find interface " + mIfaceName);
                return;
            }
            mIfaceMac = networkInterface.getHardwareAddress();

            synchronized(this) {
                // Install basic filters
                installNewProgramLocked();
            }

            socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
            PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6,
                    NetworkInterface.getByName(mIfaceName).getIndex());
                    networkInterface.getIndex());
            Os.bind(socket, addr);
            NetworkUtils.attachRaFilter(socket, mNai.networkMisc.apfPacketFormat);
        } catch(SocketException|ErrnoException e) {
            Log.e(TAG, "Error filtering raw socket", e);
            Log.e(TAG, "Error starting filter", e);
            return;
        }
        mReceiveThread = new ReceiveThread(socket);
@@ -156,12 +216,6 @@ public class ApfFilter {

    // A class to hold information about an RA.
    private class Ra {
        private static final int ETH_HEADER_LEN = 14;

        private static final int IPV6_HEADER_LEN = 40;
        private static final int IPV6_SRC_ADDR_OFFSET = ETH_HEADER_LEN + 8;
        private static final int IPV6_DST_ADDR_OFFSET = ETH_HEADER_LEN + 24;

        // From RFC4861:
        private static final int ICMP6_RA_HEADER_LEN = 16;
        private static final int ICMP6_RA_CHECKSUM_OFFSET =
@@ -250,7 +304,7 @@ public class ApfFilter {
                StringBuffer sb = new StringBuffer();
                sb.append(String.format("RA %s -> %s %d ",
                        IPv6AddresstoString(IPV6_SRC_ADDR_OFFSET),
                        IPv6AddresstoString(IPV6_DST_ADDR_OFFSET),
                        IPv6AddresstoString(IPV6_DEST_ADDR_OFFSET),
                        uint16(mPacket.getShort(ICMP6_RA_ROUTER_LIFETIME_OFFSET))));
                for (int i: mPrefixOptionOffsets) {
                    String prefix = IPv6AddresstoString(i + 16);
@@ -302,7 +356,7 @@ public class ApfFilter {
                    ICMP6_RA_ROUTER_LIFETIME_OFFSET,
                    ICMP6_RA_ROUTER_LIFETIME_LEN);

            // Parse ICMP6 options
            // Parse ICMPv6 options
            mPrefixOptionOffsets = new ArrayList<>();
            mPacket.position(ICMP6_RA_OPTION_OFFSET);
            while (mPacket.hasRemaining()) {
@@ -394,13 +448,16 @@ public class ApfFilter {
        }

        boolean isExpired() {
            return currentLifetime() < 0;
            // TODO: We may want to handle 0 lifetime RAs differently, if they are common. We'll
            // have to calculte the filter lifetime specially as a fraction of 0 is still 0.
            return currentLifetime() <= 0;
        }

        // Append a filter for this RA to {@code gen}. Jump to DROP_LABEL if it should be dropped.
        // Jump to the next filter if packet doesn't match this RA.
        long generateFilter(ApfGenerator gen) throws IllegalInstructionException {
            String nextFilterLabel = "Ra" + getUniqueNumber();
        @GuardedBy("ApfFilter.this")
        long generateFilterLocked(ApfGenerator gen) throws IllegalInstructionException {
            String nextFilterLabel = "Ra" + getUniqueNumberLocked();
            // Skip if packet is not the right size
            gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT);
            gen.addJumpIfR0NotEquals(mPacket.limit(), nextFilterLabel);
@@ -447,6 +504,8 @@ public class ApfFilter {

    // Maximum number of RAs to filter for.
    private static final int MAX_RAS = 10;

    @GuardedBy("this")
    private ArrayList<Ra> mRas = new ArrayList<Ra>();

    // There is always some marginal benefit to updating the installed APF program when an RA is
@@ -460,42 +519,167 @@ public class ApfFilter {
    private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6;

    // When did we last install a filter program? In seconds since Unix Epoch.
    @GuardedBy("this")
    private long mLastTimeInstalledProgram;
    // How long should the last installed filter program live for? In seconds.
    @GuardedBy("this")
    private long mLastInstalledProgramMinLifetime;

    // For debugging only. The length in bytes of the last program.
    // For debugging only. The last program installed.
    @GuardedBy("this")
    private byte[] mLastInstalledProgram;

    private void installNewProgram() {
        if (mRas.size() == 0) return;
    /**
     * Generate filter code to process IPv4 packets. Execution of this code ends in either the
     * DROP_LABEL or PASS_LABEL and does not fall off the end.
     * Preconditions:
     *  - Packet being filtered is IPv4
     *  - R1 is initialized to 0
     */
    @GuardedBy("this")
    private void generateIPv4FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
        // Here's a basic summary of what the IPv4 filter program does:
        //
        // if it's multicast and we're dropping multicast:
        //   drop
        // if it's not broadcast:
        //   pass
        // if it's not DHCP destined to our MAC:
        //   drop
        // pass

        if (mMulticastFilter) {
            // Check for multicast destination address range
            gen.addLoad8(Register.R0, IPV4_DEST_ADDR_OFFSET);
            gen.addAnd(0xf0);
            gen.addJumpIfR0Equals(0xe0, gen.DROP_LABEL);
        }

        // Drop all broadcasts besides DHCP addressed to us
        // If not a broadcast packet, pass
        // NOTE: Relies on R1 being initialized to 0 which is the offset of the ethernet
        //       destination MAC address
        gen.addJumpIfBytesNotEqual(Register.R1, ETH_BROADCAST_MAC_ADDRESS, gen.PASS_LABEL);
        // If not UDP, drop
        gen.addLoad8(Register.R0, IPV4_PROTOCOL_OFFSET);
        gen.addJumpIfR0NotEquals(IPPROTO_UDP, gen.DROP_LABEL);
        // If fragment, drop. This matches the BPF filter installed by the DHCP client.
        gen.addLoad16(Register.R0, IPV4_FRAGMENT_OFFSET_OFFSET);
        gen.addJumpIfR0AnyBitsSet(IPV4_FRAGMENT_OFFSET_MASK, gen.DROP_LABEL);
        // If not to DHCP client port, drop
        gen.addLoadFromMemory(Register.R1, gen.IPV4_HEADER_SIZE_MEMORY_SLOT);
        gen.addLoad16Indexed(Register.R0, UDP_DESTINATION_PORT_OFFSET);
        gen.addJumpIfR0NotEquals(DHCP_CLIENT_PORT, gen.DROP_LABEL);
        // If not DHCP to our MAC address, drop
        gen.addLoadImmediate(Register.R0, DHCP_CLIENT_MAC_OFFSET);
        // NOTE: Relies on R1 containing IPv4 header offset.
        gen.addAddR1();
        gen.addJumpIfBytesNotEqual(Register.R0, mIfaceMac, gen.DROP_LABEL);

        // Otherwise, pass
        gen.addJump(gen.PASS_LABEL);
    }


    /**
     * Generate filter code to process IPv6 packets. Execution of this code ends in either the
     * DROP_LABEL or PASS_LABEL, or falls off the end for ICMPv6 packets.
     * Preconditions:
     *  - Packet being filtered is IPv6
     *  - R1 is initialized to 0
     */
    @GuardedBy("this")
    private void generateIPv6FilterLocked(ApfGenerator gen) throws IllegalInstructionException {
        // Here's a basic summary of what the IPv6 filter program does:
        //
        // if it's not ICMPv6:
        //   pass
        // if it's ICMPv6 NA to ff02::1:
        //   drop

        // If not ICMPv6, pass
        gen.addLoad8(Register.R0, IPV6_NEXT_HEADER_OFFSET);
        // TODO: Drop multicast if the multicast filter is enabled.
        gen.addJumpIfR0NotEquals(IPPROTO_ICMPV6, gen.PASS_LABEL);
        // Add unsolicited multicast neighbor announcements filter
        String skipUnsolicitedMulticastNALabel = "skipUnsolicitedMulticastNA";
        // If not neighbor announcements, skip unsolicited multicast NA filter
        gen.addLoad8(Register.R0, ICMP6_TYPE_OFFSET);
        gen.addJumpIfR0NotEquals(ICMP6_NEIGHBOR_ANNOUNCEMENT, skipUnsolicitedMulticastNALabel);
        // If to ff02::1, drop
        // TODO: Drop only if they don't contain the address of on-link neighbours.
        gen.addLoadImmediate(Register.R0, IPV6_DEST_ADDR_OFFSET);
        gen.addJumpIfBytesNotEqual(Register.R0, IPV6_ALL_NODES_ADDRESS,
                skipUnsolicitedMulticastNALabel);
        gen.addJump(gen.DROP_LABEL);
        gen.defineLabel(skipUnsolicitedMulticastNALabel);
    }

    /**
     * Begin generating an APF program to:
     * <ul>
     * <li>Drop IPv4 broadcast packets, except DHCP destined to our MAC,
     * <li>Drop IPv4 multicast packets, if mMulticastFilter,
     * <li>Pass all other IPv4 packets,
     * <li>Pass all non-ICMPv6 IPv6 packets,
     * <li>Pass all non-IPv4 and non-IPv6 packets,
     * <li>Drop IPv6 ICMPv6 NAs to ff02::1.
     * <li>Let execution continue off the end of the program for IPv6 ICMPv6 packets. This allows
     *     insertion of RA filters here, or if there aren't any, just passes the packets.
     * </ul>
     */
    @GuardedBy("this")
    private ApfGenerator beginProgramLocked() throws IllegalInstructionException {
        ApfGenerator gen = new ApfGenerator();
        // This is guaranteed to return true because of the check in maybeCreate.
        gen.setApfVersion(mNai.networkMisc.apfVersionSupported);

        // Here's a basic summary of what the initial program does:
        //
        // if it's IPv4:
        //   insert IPv4 filter to drop or pass these appropriately
        // if it's not IPv6:
        //   pass
        // insert IPv6 filter to drop, pass, or fall off the end for ICMPv6 packets

        // Add IPv4 filters:
        String skipIPv4FiltersLabel = "skipIPv4Filters";
        // If not IPv4, skip IPv4 filters
        gen.addLoad16(Register.R0, ETH_ETHERTYPE_OFFSET);
        gen.addJumpIfR0NotEquals(ETH_P_IP, skipIPv4FiltersLabel);
        // NOTE: Relies on R1 being initialized to 0.
        generateIPv4FilterLocked(gen);
        gen.defineLabel(skipIPv4FiltersLabel);

        // Add IPv6 filters:
        // If not IPv6, pass
        // NOTE: Relies on R0 containing ethertype. This is safe because if we got here, we did not
        // execute the IPv4 filter, since that filter does not fall through, but either drops or
        // passes.
        gen.addJumpIfR0NotEquals(ETH_P_IPV6, gen.PASS_LABEL);
        generateIPv6FilterLocked(gen);
        return gen;
    }

    @GuardedBy("this")
    private void installNewProgramLocked() {
        purgeExpiredRasLocked();
        final byte[] program;
        long programMinLifetime = Long.MAX_VALUE;
        try {
            ApfGenerator gen = new ApfGenerator();
            // This is guaranteed to return true because of the check in maybeInstall.
            gen.setApfVersion(mNai.networkMisc.apfVersionSupported);
            // Step 1: Determine how many RA filters we can fit in the program.
            int ras = 0;
            ApfGenerator gen = beginProgramLocked();
            ArrayList<Ra> rasToFilter = new ArrayList<Ra>();
            for (Ra ra : mRas) {
                if (ra.isExpired()) continue;
                ra.generateFilter(gen);
                if (gen.programLengthOverEstimate() > mNai.networkMisc.maximumApfProgramSize) {
                    // We went too far.  Use prior number of RAs in "ras".
                    break;
                } else {
                    // Yay! this RA filter fits, increment "ras".
                    ras++;
                ra.generateFilterLocked(gen);
                // Stop if we get too big.
                if (gen.programLengthOverEstimate() > mNai.networkMisc.maximumApfProgramSize) break;
                rasToFilter.add(ra);
            }
            }
            // Step 2: Generate RA filters
            gen = new ApfGenerator();
            // This is guaranteed to return true because of the check in maybeInstall.
            gen.setApfVersion(mNai.networkMisc.apfVersionSupported);
            for (Ra ra : mRas) {
                if (ras-- == 0) break;
                if (ra.isExpired()) continue;
                programMinLifetime = Math.min(programMinLifetime, ra.generateFilter(gen));
            // Step 2: Actually generate the program
            gen = beginProgramLocked();
            for (Ra ra : rasToFilter) {
                programMinLifetime = Math.min(programMinLifetime, ra.generateFilterLocked(gen));
            }
            // Execution will reach the end of the program if no filters match, which will pass the
            // packet to the AP.
@@ -514,12 +698,13 @@ public class ApfFilter {
    }

    // Install a new filter program if the last installed one will die soon.
    private void maybeInstallNewProgram() {
    @GuardedBy("this")
    private void maybeInstallNewProgramLocked() {
        if (mRas.size() == 0) return;
        // If the current program doesn't expire for a while, don't bother updating.
        long expiry = mLastTimeInstalledProgram + mLastInstalledProgramMinLifetime;
        if (expiry < curTime() + MAX_PROGRAM_LIFETIME_WORTH_REFRESHING) {
            installNewProgram();
            installNewProgramLocked();
        }
    }

@@ -527,7 +712,19 @@ public class ApfFilter {
        log(msg + HexDump.toHexString(packet, 0, length, false /* lowercase */));
    }

    private void processRa(byte[] packet, int length) {
    @GuardedBy("this")
    private void purgeExpiredRasLocked() {
        for (int i = 0; i < mRas.size();) {
            if (mRas.get(i).isExpired()) {
                log("Expiring " + mRas.get(i));
                mRas.remove(i);
            } else {
                i++;
            }
        }
    }

    private synchronized void processRa(byte[] packet, int length) {
        if (VDBG) hexDump("Read packet = ", packet, length);

        // Have we seen this RA before?
@@ -549,30 +746,25 @@ public class ApfFilter {
                // Swap to front of array.
                mRas.add(0, mRas.remove(i));

                maybeInstallNewProgram();
                maybeInstallNewProgramLocked();
                return;
            }
        }
        // Purge expired RAs.
        for (int i = 0; i < mRas.size();) {
            if (mRas.get(i).isExpired()) {
                log("Expired RA " + mRas.get(i));
                mRas.remove(i);
            } else {
                i++;
            }
        }
        purgeExpiredRasLocked();
        // TODO: figure out how to proceed when we've received more then MAX_RAS RAs.
        if (mRas.size() >= MAX_RAS) return;
        final Ra ra;
        try {
            Ra ra = new Ra(packet, length);
            log("Adding " + ra);
            mRas.add(ra);
            ra = new Ra(packet, length);
        } catch (Exception e) {
            Log.e(TAG, "Error parsing RA: " + e);
            return;
        }
        installNewProgram();
        // Ignore 0 lifetime RAs.
        if (ra.isExpired()) return;
        log("Adding " + ra);
        mRas.add(ra);
        installNewProgramLocked();
    }

    /**
@@ -598,15 +790,23 @@ public class ApfFilter {
        nai.apfFilter = new ApfFilter(connectivityService, nai);
    }

    public void shutdown() {
    public synchronized void shutdown() {
        if (mReceiveThread != null) {
            log("shutting down");
            mReceiveThread.halt();  // Also closes socket.
            mReceiveThread = null;
        }
        mRas.clear();
    }

    public synchronized void setMulticastFilter(boolean enabled) {
        if (mMulticastFilter != enabled) {
            mMulticastFilter = enabled;
            installNewProgramLocked();
        }
    }

    public void dump(IndentingPrintWriter pw) {
    public synchronized void dump(IndentingPrintWriter pw) {
        pw.println("APF version: " + mNai.networkMisc.apfVersionSupported);
        pw.println("Max program size: " + mNai.networkMisc.maximumApfProgramSize);
        pw.println("Receive thread: " + (mReceiveThread != null ? "RUNNING" : "STOPPED"));