summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/core/java/com/android/server/connectivity/ApfFilter.java314
1 files changed, 257 insertions, 57 deletions
diff --git a/services/core/java/com/android/server/connectivity/ApfFilter.java b/services/core/java/com/android/server/connectivity/ApfFilter.java
index 1ebd56c78954..35f358353cbf 100644
--- a/services/core/java/com/android/server/connectivity/ApfFilter.java
+++ b/services/core/java/com/android/server/connectivity/ApfFilter.java
@@ -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"));