From 578a76e7de77492ac33e407fff4fb9a2f5550d8a Mon Sep 17 00:00:00 2001 From: Paul Jensen Date: Thu, 14 Jan 2016 14:54:39 -0500 Subject: [PATCH] Have ConnectivityService install packet filters when possible Listen for ICMP6 router advertisements on networks that support packet filters. Construct packet filters and install them to ignore redundant future ICMP6 router advertisements. Bug: 26238573 Change-Id: If78300b9fda257c21f3ee6533e1da7de9f897cb4 --- core/java/android/net/NetworkAgent.java | 21 + core/java/android/net/NetworkMisc.java | 25 + core/java/android/net/NetworkUtils.java | 7 + core/jni/android_net_NetUtils.cpp | 46 +- .../android/server/ConnectivityService.java | 29 + .../server/connectivity/ApfFilter.java | 499 ++++++++++++++++++ .../server/connectivity/NetworkAgentInfo.java | 4 + 7 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 services/core/java/com/android/server/connectivity/ApfFilter.java diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java index 20c216826531b..9e360e11bf8b5 100644 --- a/core/java/android/net/NetworkAgent.java +++ b/core/java/android/net/NetworkAgent.java @@ -200,6 +200,14 @@ public abstract class NetworkAgent extends Handler { */ public static final int CMD_PREVENT_AUTOMATIC_RECONNECT = BASE + 15; + /** + * Sent by ConnectivityService to the NetworkAgent to install an APF program in the network + * chipset for use to filter packets. + * + * obj = byte[] containing the APF program bytecode. + */ + public static final int CMD_PUSH_APF_PROGRAM = BASE + 16; + public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni, NetworkCapabilities nc, LinkProperties lp, int score) { this(looper, context, logTag, ni, nc, lp, score, null); @@ -319,6 +327,10 @@ public abstract class NetworkAgent extends Handler { preventAutomaticReconnect(); break; } + case CMD_PUSH_APF_PROGRAM: { + installPacketFilter((byte[]) msg.obj); + break; + } } } @@ -494,6 +506,15 @@ public abstract class NetworkAgent extends Handler { protected void preventAutomaticReconnect() { } + /** + * Install a packet filter. + * @param filter an APF program to filter incoming packets. + * @return {@code true} if filter successfully installed, {@code false} otherwise. + */ + protected boolean installPacketFilter(byte[] filter) { + return false; + } + protected void log(String s) { Log.d(LOG_TAG, "NetworkAgent: " + s); } diff --git a/core/java/android/net/NetworkMisc.java b/core/java/android/net/NetworkMisc.java index 5511a248b6aa9..748699eff4cfd 100644 --- a/core/java/android/net/NetworkMisc.java +++ b/core/java/android/net/NetworkMisc.java @@ -56,6 +56,22 @@ public class NetworkMisc implements Parcelable { */ public String subscriberId; + /** + * Version of APF instruction set supported for packet filtering. 0 indicates no support for + * packet filtering using APF programs. + */ + public int apfVersionSupported; + + /** + * Maximum size of APF program allowed. + */ + public int maximumApfProgramSize; + + /** + * Format of packets passed to APF filter. Should be one of ARPHRD_* + */ + public int apfPacketFormat; + public NetworkMisc() { } @@ -65,6 +81,9 @@ public class NetworkMisc implements Parcelable { explicitlySelected = nm.explicitlySelected; acceptUnvalidated = nm.acceptUnvalidated; subscriberId = nm.subscriberId; + apfVersionSupported = nm.apfVersionSupported; + maximumApfProgramSize = nm.maximumApfProgramSize; + apfPacketFormat = nm.apfPacketFormat; } } @@ -79,6 +98,9 @@ public class NetworkMisc implements Parcelable { out.writeInt(explicitlySelected ? 1 : 0); out.writeInt(acceptUnvalidated ? 1 : 0); out.writeString(subscriberId); + out.writeInt(apfVersionSupported); + out.writeInt(maximumApfProgramSize); + out.writeInt(apfPacketFormat); } public static final Creator CREATOR = new Creator() { @@ -89,6 +111,9 @@ public class NetworkMisc implements Parcelable { networkMisc.explicitlySelected = in.readInt() != 0; networkMisc.acceptUnvalidated = in.readInt() != 0; networkMisc.subscriberId = in.readString(); + networkMisc.apfVersionSupported = in.readInt(); + networkMisc.maximumApfProgramSize = in.readInt(); + networkMisc.apfPacketFormat = in.readInt(); return networkMisc; } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index c6d919f4d77e1..555032d522bf0 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -61,6 +61,13 @@ public class NetworkUtils { */ public native static void attachDhcpFilter(FileDescriptor fd) throws SocketException; + /** + * Attaches a socket filter that accepts ICMP6 router advertisement packets to the given socket. + * @param fd the socket's {@link FileDescriptor}. + * @param packetType the hardware address type, one of ARPHRD_*. + */ + public native static void attachRaFilter(FileDescriptor fd, int packetType) throws SocketException; + /** * Binds the current process to the network designated by {@code netId}. All sockets created * in the future (and not explicitly bound via a bound {@link SocketFactory} (see diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp index defb88a9712f9..880a79cc4f6d8 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -26,10 +26,13 @@ #include #include #include +#include #include #include #include +#include #include +#include #include #include @@ -64,10 +67,9 @@ static jint android_net_utils_resetConnections(JNIEnv* env, jobject clazz, static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd) { - int fd = jniGetFDFromFileDescriptor(env, javaFd); uint32_t ip_offset = sizeof(ether_header); uint32_t proto_offset = ip_offset + offsetof(iphdr, protocol); - uint32_t flags_offset = ip_offset + offsetof(iphdr, frag_off); + uint32_t flags_offset = ip_offset + offsetof(iphdr, frag_off); uint32_t dport_indirect_offset = ip_offset + offsetof(udphdr, dest); struct sock_filter filter_code[] = { // Check the protocol is UDP. @@ -94,6 +96,45 @@ static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobje filter_code, }; + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); + } +} + +static void android_net_utils_attachRaFilter(JNIEnv *env, jobject clazz, jobject javaFd, + jint hardwareAddressType) +{ + if (hardwareAddressType != ARPHRD_ETHER) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "attachRaFilter only supports ARPHRD_ETHER"); + return; + } + + uint32_t ipv6_offset = sizeof(ether_header); + uint32_t ipv6_next_header_offset = ipv6_offset + offsetof(ip6_hdr, ip6_nxt); + uint32_t icmp6_offset = ipv6_offset + sizeof(ip6_hdr); + uint32_t icmp6_type_offset = icmp6_offset + offsetof(icmp6_hdr, icmp6_type); + struct sock_filter filter_code[] = { + // Check IPv6 Next Header is ICMPv6. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, ipv6_next_header_offset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3), + + // Check ICMPv6 type is Router Advertisement. + BPF_STMT(BPF_LD | BPF_B | BPF_ABS, icmp6_type_offset), + BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_ROUTER_ADVERT, 0, 1), + + // Accept or reject. + BPF_STMT(BPF_RET | BPF_K, 0xffff), + BPF_STMT(BPF_RET | BPF_K, 0) + }; + struct sock_fprog filter = { + sizeof(filter_code) / sizeof(filter_code[0]), + filter_code, + }; + + int fd = jniGetFDFromFileDescriptor(env, javaFd); if (setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter)) != 0) { jniThrowExceptionFmt(env, "java/net/SocketException", "setsockopt(SO_ATTACH_FILTER): %s", strerror(errno)); @@ -148,6 +189,7 @@ static const JNINativeMethod gNetworkUtilMethods[] = { { "protectFromVpn", "(I)Z", (void*)android_net_utils_protectFromVpn }, { "queryUserAccess", "(II)Z", (void*)android_net_utils_queryUserAccess }, { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter }, + { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter }, }; int register_android_net_NetworkUtils(JNIEnv* env) diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 9c09f246c1f26..c80a82e7dc69e 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -125,6 +125,7 @@ import com.android.server.connectivity.NetworkAgentInfo; import com.android.server.connectivity.NetworkMonitor; import com.android.server.connectivity.PacManager; import com.android.server.connectivity.PermissionMonitor; +import com.android.server.connectivity.ApfFilter; import com.android.server.connectivity.Tethering; import com.android.server.connectivity.Vpn; import com.android.server.net.BaseNetworkObserver; @@ -359,6 +360,13 @@ public class ConnectivityService extends IConnectivityManager.Stub */ private static final int EVENT_REGISTER_NETWORK_LISTENER_WITH_INTENT = 31; + /** + * used to push APF program to NetworkAgent + * replyTo = NetworkAgent message handler + * obj = byte[] of APF program + */ + private static final int EVENT_PUSH_APF_PROGRAM_TO_NETWORK = 32; + /** Handler thread used for both of the handlers below. */ @VisibleForTesting protected final HandlerThread mHandlerThread; @@ -2188,6 +2196,7 @@ public class ConnectivityService extends IConnectivityManager.Stub mKeepaliveTracker.handleStopAllKeepalives(nai, ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK); nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED); + if (nai.apfFilter != null) nai.apfFilter.shutdown(); mNetworkAgentInfos.remove(msg.replyTo); updateClat(null, nai.linkProperties, nai); synchronized (mNetworkForNetId) { @@ -2402,6 +2411,13 @@ public class ConnectivityService extends IConnectivityManager.Stub accept ? 1 : 0, always ? 1: 0, network)); } + public void pushApfProgramToNetwork(NetworkAgentInfo nai, byte[] program) { + enforceConnectivityInternalPermission(); + Message msg = mHandler.obtainMessage(EVENT_PUSH_APF_PROGRAM_TO_NETWORK, program); + msg.replyTo = nai.messenger; + mHandler.sendMessage(msg); + } + private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) { if (DBG) log("handleSetAcceptUnvalidated network=" + network + " accept=" + accept + " always=" + always); @@ -2556,6 +2572,16 @@ public class ConnectivityService extends IConnectivityManager.Stub handleMobileDataAlwaysOn(); break; } + case EVENT_PUSH_APF_PROGRAM_TO_NETWORK: { + NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo); + if (nai == null) { + loge("EVENT_PUSH_APF_PROGRAM_TO_NETWORK from unknown NetworkAgent"); + } else { + nai.asyncChannel.sendMessage(NetworkAgent.CMD_PUSH_APF_PROGRAM, + (byte[]) msg.obj); + } + break; + } // Sent by KeepaliveTracker to process an app request on the state machine thread. case NetworkAgent.CMD_START_PACKET_KEEPALIVE: { mKeepaliveTracker.handleStartKeepalive(msg); @@ -3944,6 +3970,9 @@ public class ConnectivityService extends IConnectivityManager.Stub if (networkAgent.clatd != null) { networkAgent.clatd.fixupLinkProperties(oldLp); } + if (networkAgent.apfFilter != null) { + networkAgent.apfFilter.updateFilter(); + } updateInterfaces(newLp, oldLp, netId); updateMtu(newLp, oldLp); diff --git a/services/core/java/com/android/server/connectivity/ApfFilter.java b/services/core/java/com/android/server/connectivity/ApfFilter.java new file mode 100644 index 0000000000000..25c84e1328046 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/ApfFilter.java @@ -0,0 +1,499 @@ +/* + * 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 com.android.server.connectivity; + +import static android.system.OsConstants.*; + +import android.net.NetworkUtils; +import android.net.apf.ApfGenerator; +import android.net.apf.ApfGenerator.IllegalInstructionException; +import android.net.apf.ApfGenerator.Register; +import android.system.ErrnoException; +import android.system.Os; +import android.system.PacketSocketAddress; +import android.util.Log; +import android.util.Pair; + +import com.android.internal.util.HexDump; +import com.android.server.ConnectivityService; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.lang.Thread; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; + +import libcore.io.IoBridge; + +/** + * For networks that support packet filtering via APF programs, {@code ApfFilter} + * listens for IPv6 ICMPv6 router advertisements (RAs) and generates APF programs to + * filter out redundant duplicate ones. + * + * @hide + */ +public class ApfFilter { + // Thread to listen for RAs. + private class ReceiveThread extends Thread { + private final byte[] mPacket = new byte[1514]; + private final FileDescriptor mSocket; + private volatile boolean mStopped; + + public ReceiveThread(FileDescriptor socket) { + mSocket = socket; + } + + public void halt() { + mStopped = true; + try { + // Interrupts the read() call the thread is blocked in. + IoBridge.closeAndSignalBlockedThreads(mSocket); + } catch (IOException ignored) {} + } + + @Override + public void run() { + log("begin monitoring"); + while (!mStopped) { + try { + int length = Os.read(mSocket, mPacket, 0, mPacket.length); + processRa(mPacket, length); + } catch (IOException|ErrnoException e) { + if (!mStopped) { + Log.e(TAG, "Read error", e); + } + } + } + } + } + + private static final String TAG = "ApfFilter"; + + private final ConnectivityService mConnectivityService; + private final NetworkAgentInfo mNai; + private ReceiveThread mReceiveThread; + private String mIfaceName; + private long mUniqueCounter; + + private ApfFilter(ConnectivityService connectivityService, NetworkAgentInfo nai) { + mConnectivityService = connectivityService; + mNai = nai; + maybeStartFilter(); + } + + private void log(String s) { + Log.d(TAG, "(" + mNai.network.netId + "): " + s); + } + + private long getUniqueNumber() { + return mUniqueCounter++; + } + + /** + * Attempt to start listening for RAs and, if RAs are received, generating and installing + * filters to ignore useless RAs. + */ + private void maybeStartFilter() { + mIfaceName = mNai.linkProperties.getInterfaceName(); + if (mIfaceName == null) return; + FileDescriptor socket; + try { + socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6); + PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6, + NetworkInterface.getByName(mIfaceName).getIndex()); + Os.bind(socket, addr); + NetworkUtils.attachRaFilter(socket, mNai.networkMisc.apfPacketFormat); + } catch(SocketException|ErrnoException e) { + Log.e(TAG, "Error filtering raw socket", e); + return; + } + mReceiveThread = new ReceiveThread(socket); + mReceiveThread.start(); + } + + /** + * mNai's LinkProperties may have changed, take appropriate action. + */ + public void updateFilter() { + // If we're not listening for RAs, try starting. + if (mReceiveThread == null) { + maybeStartFilter(); + // If interface name has changed, restart. + } else if (!mIfaceName.equals(mNai.linkProperties.getInterfaceName())) { + shutdown(); + maybeStartFilter(); + } + } + + // Returns seconds since Unix Epoch. + private static long curTime() { + return System.currentTimeMillis() / 1000L; + } + + // 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; + + // From RFC4861: + private static final int ICMP6_RA_HEADER_LEN = 16; + private static final int ICMP6_RA_OPTION_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_LEN + ICMP6_RA_HEADER_LEN; + private static final int ICMP6_RA_ROUTER_LIFETIME_OFFSET = + ETH_HEADER_LEN + IPV6_HEADER_LEN + 6; + private static final int ICMP6_RA_ROUTER_LIFETIME_LEN = 2; + // Prefix information option. + private static final int ICMP6_PREFIX_OPTION_TYPE = 3; + private static final int ICMP6_PREFIX_OPTION_LEN = 32; + private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET = 4; + private static final int ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN = 4; + private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET = 8; + private static final int ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN = 4; + + // From RFC6106: Recursive DNS Server option + private static final int ICMP6_RDNSS_OPTION_TYPE = 25; + // From RFC6106: DNS Search List option + private static final int ICMP6_DNSSL_OPTION_TYPE = 31; + + // From RFC4191: Route Information option + private static final int ICMP6_ROUTE_INFO_OPTION_TYPE = 24; + // Above three options all have the same format: + private static final int ICMP6_4_BYTE_LIFETIME_OFFSET = 4; + private static final int ICMP6_4_BYTE_LIFETIME_LEN = 4; + + private final ByteBuffer mPacket; + // List of binary ranges that include the whole packet except the lifetimes. + // Pairs consist of offset and length. + private final ArrayList> mNonLifetimes = + new ArrayList>(); + // Minimum lifetime in packet + long mMinLifetime; + // When the packet was last captured, in seconds since Unix Epoch + long mLastSeen; + + /** + * Add a binary range of the packet that does not include a lifetime to mNonLifetimes. + * Assumes mPacket.position() is as far as we've parsed the packet. + * @param lastNonLifetimeStart offset within packet of where the last binary range of + * data not including a lifetime. + * @param lifetimeOffset offset from mPacket.position() to the next lifetime data. + * @param lifetimeLength length of the next lifetime data. + * @return offset within packet of where the next binary range of data not including + * a lifetime. This can be passed into the next invocation of this function + * via {@code lastNonLifetimeStart}. + */ + private int addNonLifetime(int lastNonLifetimeStart, int lifetimeOffset, + int lifetimeLength) { + lifetimeOffset += mPacket.position(); + mNonLifetimes.add(new Pair(lastNonLifetimeStart, + lifetimeOffset - lastNonLifetimeStart)); + return lifetimeOffset + lifetimeLength; + } + + // Note that this parses RA and may throw IllegalArgumentException (from + // Buffer.position(int) ) or IndexOutOfBoundsException (from ByteBuffer.get(int) ) if + // parsing encounters something non-compliant with specifications. + Ra(byte[] packet, int length) { + mPacket = ByteBuffer.allocate(length).put(ByteBuffer.wrap(packet, 0, length)); + mPacket.clear(); + mLastSeen = curTime(); + + // Parse router lifetime + int lastNonLifetimeStart = addNonLifetime(0, ICMP6_RA_ROUTER_LIFETIME_OFFSET, + ICMP6_RA_ROUTER_LIFETIME_LEN); + // Parse ICMP6 options + mPacket.position(ICMP6_RA_OPTION_OFFSET); + while (mPacket.hasRemaining()) { + int optionType = ((int)mPacket.get(mPacket.position())) & 0xff; + int optionLength = (((int)mPacket.get(mPacket.position() + 1)) & 0xff) * 8; + switch (optionType) { + case ICMP6_PREFIX_OPTION_TYPE: + // Parse valid lifetime + lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart, + ICMP6_PREFIX_OPTION_VALID_LIFETIME_OFFSET, + ICMP6_PREFIX_OPTION_VALID_LIFETIME_LEN); + // Parse preferred lifetime + lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart, + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_OFFSET, + ICMP6_PREFIX_OPTION_PREFERRED_LIFETIME_LEN); + break; + // These three options have the same lifetime offset and size, so process + // together: + case ICMP6_ROUTE_INFO_OPTION_TYPE: + case ICMP6_RDNSS_OPTION_TYPE: + case ICMP6_DNSSL_OPTION_TYPE: + // Parse lifetime + lastNonLifetimeStart = addNonLifetime(lastNonLifetimeStart, + ICMP6_4_BYTE_LIFETIME_OFFSET, + ICMP6_4_BYTE_LIFETIME_LEN); + break; + default: + // RFC4861 section 4.2 dictates we ignore unknown options for fowards + // compatibility. + break; + } + mPacket.position(mPacket.position() + optionLength); + } + // Mark non-lifetime bytes since last lifetime. + addNonLifetime(lastNonLifetimeStart, 0, 0); + mMinLifetime = minLifetime(packet, length); + } + + // Ignoring lifetimes (which may change) does {@code packet} match this RA? + boolean matches(byte[] packet, int length) { + if (length != mPacket.limit()) return false; + ByteBuffer a = ByteBuffer.wrap(packet); + ByteBuffer b = mPacket; + for (Pair nonLifetime : mNonLifetimes) { + a.clear(); + b.clear(); + a.position(nonLifetime.first); + b.position(nonLifetime.first); + a.limit(nonLifetime.first + nonLifetime.second); + b.limit(nonLifetime.first + nonLifetime.second); + if (a.compareTo(b) != 0) return false; + } + return true; + } + + // What is the minimum of all lifetimes within {@code packet} in seconds? + // Precondition: matches(packet, length) already returned true. + long minLifetime(byte[] packet, int length) { + long minLifetime = Long.MAX_VALUE; + // Wrap packet in ByteBuffer so we can read big-endian values easily + ByteBuffer byteBuffer = ByteBuffer.wrap(packet); + for (int i = 0; (i + 1) < mNonLifetimes.size(); i++) { + int offset = mNonLifetimes.get(i).first + mNonLifetimes.get(i).second; + int lifetimeLength = mNonLifetimes.get(i+1).first - offset; + long val; + switch (lifetimeLength) { + case 2: val = byteBuffer.getShort(offset); break; + case 4: val = byteBuffer.getInt(offset); break; + default: throw new IllegalStateException("bogus lifetime size " + length); + } + // Mask to size, converting signed to unsigned + val &= (1L << (lifetimeLength * 8)) - 1; + minLifetime = Math.min(minLifetime, val); + } + return minLifetime; + } + + // How many seconds does this RA's have to live, taking into account the fact + // that we might have seen it a while ago. + long currentLifetime() { + return mMinLifetime - (curTime() - mLastSeen); + } + + boolean isExpired() { + 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(); + // Skip if packet is not the right size + gen.addLoadFromMemory(Register.R0, gen.PACKET_SIZE_MEMORY_SLOT); + gen.addJumpIfR0NotEquals(mPacket.limit(), nextFilterLabel); + int filterLifetime = (int)(currentLifetime() / FRACTION_OF_LIFETIME_TO_FILTER); + // Skip filter if expired + gen.addLoadFromMemory(Register.R0, gen.FILTER_AGE_MEMORY_SLOT); + gen.addJumpIfR0GreaterThan(filterLifetime, nextFilterLabel); + for (int i = 0; i < mNonLifetimes.size(); i++) { + // Generate code to match the packet bytes + Pair nonLifetime = mNonLifetimes.get(i); + gen.addLoadImmediate(Register.R0, nonLifetime.first); + gen.addJumpIfBytesNotEqual(Register.R0, + Arrays.copyOfRange(mPacket.array(), nonLifetime.first, + nonLifetime.first + nonLifetime.second), + nextFilterLabel); + // Generate code to test the lifetimes haven't gone down too far + if ((i + 1) < mNonLifetimes.size()) { + Pair nextNonLifetime = mNonLifetimes.get(i + 1); + int offset = nonLifetime.first + nonLifetime.second; + int length = nextNonLifetime.first - offset; + switch (length) { + case 4: gen.addLoad32(Register.R0, offset); break; + case 2: gen.addLoad16(Register.R0, offset); break; + default: throw new IllegalStateException("bogus lifetime size " + length); + } + gen.addJumpIfR0LessThan(filterLifetime, nextFilterLabel); + } + } + gen.addJump(gen.DROP_LABEL); + gen.defineLabel(nextFilterLabel); + return filterLifetime; + } + } + + // Maximum number of RAs to filter for. + private static final int MAX_RAS = 10; + private ArrayList mRas = new ArrayList(); + + // There is always some marginal benefit to updating the installed APF program when an RA is + // seen because we can extend the program's lifetime slightly, but there is some cost to + // updating the program, so don't bother unless the program is going to expire soon. This + // constant defines "soon" in seconds. + private static final long MAX_PROGRAM_LIFETIME_WORTH_REFRESHING = 30; + // We don't want to filter an RA for it's whole lifetime as it'll be expired by the time we ever + // see a refresh. Using half the lifetime might be a good idea except for the fact that + // packets may be dropped, so let's use 6. + private static final int FRACTION_OF_LIFETIME_TO_FILTER = 6; + + // When did we last install a filter program? In seconds since Unix Epoch. + private long mLastTimeInstalledProgram; + // How long should the last installed filter program live for? In seconds. + private long mLastInstalledProgramMinLifetime; + + private void installNewProgram() { + if (mRas.size() == 0) return; + 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; + 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++; + } + } + // 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)); + } + // Execution will reach the end of the program if no filters match, which will pass the + // packet to the AP. + program = gen.generate(); + } catch (IllegalInstructionException e) { + Log.e(TAG, "Program failed to generate: ", e); + return; + } + mLastTimeInstalledProgram = curTime(); + mLastInstalledProgramMinLifetime = programMinLifetime; + hexDump("Installing filter: ", program, program.length); + mConnectivityService.pushApfProgramToNetwork(mNai, program); + } + + // Install a new filter program if the last installed one will die soon. + private void maybeInstallNewProgram() { + 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(); + } + } + + private void hexDump(String msg, byte[] packet, int length) { + log(msg + HexDump.toHexString(packet, 0, length)); + } + + private void processRa(byte[] packet, int length) { + hexDump("Read packet = ", packet, length); + + // Have we seen this RA before? + for (int i = 0; i < mRas.size(); i++) { + Ra ra = mRas.get(i); + if (ra.matches(packet, length)) { + log("matched RA"); + // Update lifetimes. + ra.mLastSeen = curTime(); + ra.mMinLifetime = ra.minLifetime(packet, length); + + // Keep mRas in LRU order so as to prioritize generating filters for recently seen + // RAs. LRU prioritizes this because RA filters are generated in order from mRas + // until the filter program exceeds the maximum filter program size allowed by the + // chipset, so RAs appearing earlier in mRas are more likely to make it into the + // filter program. + // TODO: consider sorting the RAs in order of increasing expiry time as well. + // Swap to front of array. + mRas.add(0, mRas.remove(i)); + + maybeInstallNewProgram(); + return; + } + } + // Purge expired RAs. + for (int i = 0; i < mRas.size();) { + if (mRas.get(i).isExpired()) { + log("expired RA"); + mRas.remove(i); + } else { + i++; + } + } + // TODO: figure out how to proceed when we've received more then MAX_RAS RAs. + if (mRas.size() >= MAX_RAS) return; + try { + log("adding RA"); + mRas.add(new Ra(packet, length)); + } catch (Exception e) { + Log.e(TAG, "Error parsing RA: " + e); + return; + } + installNewProgram(); + } + + /** + * Install an {@link ApfFilter} on {@code nai} if {@code nai} supports packet + * filtering using APF programs. + */ + public static void maybeInstall(ConnectivityService connectivityService, NetworkAgentInfo nai) { + if (nai.networkMisc == null) return; + if (nai.networkMisc.apfVersionSupported == 0) return; + if (nai.networkMisc.maximumApfProgramSize < 200) { + Log.e(TAG, "Uselessly small APF size limit: " + nai.networkMisc.maximumApfProgramSize); + return; + } + // For now only support generating programs for Ethernet frames. If this restriction is + // lifted: + // 1. the program generator will need its offsets adjusted. + // 2. the packet filter attached to our packet socket will need its offset adjusted. + if (nai.networkMisc.apfPacketFormat != ARPHRD_ETHER) return; + if (!new ApfGenerator().setApfVersion(nai.networkMisc.apfVersionSupported)) { + Log.e(TAG, "Unsupported APF version: " + nai.networkMisc.apfVersionSupported); + return; + } + nai.apfFilter = new ApfFilter(connectivityService, nai); + } + + public void shutdown() { + if (mReceiveThread != null) { + log("shuting down"); + mReceiveThread.halt(); // Also closes socket. + mReceiveThread = null; + } + } +} diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java index 00292793256c6..384c620112fc8 100644 --- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java +++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java @@ -32,6 +32,7 @@ import android.util.SparseArray; import com.android.internal.util.AsyncChannel; import com.android.server.ConnectivityService; import com.android.server.connectivity.NetworkMonitor; +import com.android.server.connectivity.ApfFilter; import java.util.ArrayList; import java.util.Comparator; @@ -163,6 +164,8 @@ public class NetworkAgentInfo implements Comparable { // Used by ConnectivityService to keep track of 464xlat. public Nat464Xlat clatd; + public ApfFilter apfFilter; + public NetworkAgentInfo(Messenger messenger, AsyncChannel ac, Network net, NetworkInfo info, LinkProperties lp, NetworkCapabilities nc, int score, Context context, Handler handler, NetworkMisc misc, NetworkRequest defaultRequest, ConnectivityService connService) { @@ -175,6 +178,7 @@ public class NetworkAgentInfo implements Comparable { currentScore = score; networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest); networkMisc = misc; + apfFilter.maybeInstall(connService, this); } /**