summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/net/NetworkAgent.java21
-rw-r--r--core/java/android/net/NetworkMisc.java25
-rw-r--r--core/java/android/net/NetworkUtils.java7
-rw-r--r--core/java/android/text/method/BaseKeyListener.java32
-rw-r--r--core/jni/android_net_NetUtils.cpp46
-rw-r--r--core/tests/coretests/src/android/text/method/ForwardDeleteTest.java472
-rw-r--r--services/core/java/com/android/server/ConnectivityService.java29
-rw-r--r--services/core/java/com/android/server/connectivity/ApfFilter.java499
-rw-r--r--services/core/java/com/android/server/connectivity/NetworkAgentInfo.java4
9 files changed, 1124 insertions, 11 deletions
diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java
index 20c216826531..9e360e11bf8b 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 5511a248b6aa..748699eff4cf 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<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
@@ -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 c6d919f4d77e..555032d522bf 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -62,6 +62,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
* {@link Network#getSocketFactory}) will be bound to this network. Note that if this
diff --git a/core/java/android/text/method/BaseKeyListener.java b/core/java/android/text/method/BaseKeyListener.java
index e79dfcacac29..3564e11f5b13 100644
--- a/core/java/android/text/method/BaseKeyListener.java
+++ b/core/java/android/text/method/BaseKeyListener.java
@@ -16,6 +16,7 @@
package android.text.method;
+import android.graphics.Paint;
import android.icu.lang.UCharacter;
import android.icu.lang.UProperty;
import android.view.KeyEvent;
@@ -25,6 +26,8 @@ import android.text.method.TextKeyListener.Capitalize;
import android.text.style.ReplacementSpan;
import android.widget.TextView;
+import com.android.internal.annotations.GuardedBy;
+
import java.text.BreakIterator;
import java.util.Arrays;
import java.util.Collections;
@@ -45,6 +48,11 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener
implements KeyListener {
/* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete();
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ static Paint sCachedPaint = null;
+
/**
* Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in
* a {@link TextView}. If there is a selection, deletes the selection; otherwise,
@@ -258,20 +266,15 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener
}
// Returns the end offset to be deleted by a forward delete key from the given offset.
- private static int getOffsetForForwardDeleteKey(CharSequence text, int offset) {
+ private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) {
final int len = text.length();
if (offset >= len - 1) {
return len;
}
- int codePoint = Character.codePointAt(text, offset);
- offset += Character.charCount(codePoint);
- if (offset == len) {
- return len;
- }
-
- // TODO: Handle emoji, combining chars, etc.
+ offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */,
+ offset, Paint.CURSOR_AFTER);
return adjustReplacementSpan(text, offset, false /* move to the end */);
}
@@ -311,7 +314,18 @@ public abstract class BaseKeyListener extends MetaKeyKeyListener
final int start = Selection.getSelectionEnd(content);
final int end;
if (isForwardDelete) {
- end = getOffsetForForwardDeleteKey(content, start);
+ final Paint paint;
+ if (view instanceof TextView) {
+ paint = ((TextView)view).getPaint();
+ } else {
+ synchronized (mLock) {
+ if (sCachedPaint == null) {
+ sCachedPaint = new Paint();
+ }
+ paint = sCachedPaint;
+ }
+ }
+ end = getOffsetForForwardDeleteKey(content, start, paint);
} else {
end = getOffsetForBackspaceKey(content, start);
}
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index defb88a9712f..880a79cc4f6d 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -26,10 +26,13 @@
#include <net/if.h>
#include <linux/filter.h>
#include <linux/if.h>
+#include <linux/if_arp.h>
#include <linux/if_ether.h>
#include <linux/if_packet.h>
#include <net/if_ether.h>
+#include <netinet/icmp6.h>
#include <netinet/ip.h>
+#include <netinet/ip6.h>
#include <netinet/udp.h>
#include <cutils/properties.h>
@@ -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/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java b/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java
new file mode 100644
index 000000000000..da17045d0855
--- /dev/null
+++ b/core/tests/coretests/src/android/text/method/ForwardDeleteTest.java
@@ -0,0 +1,472 @@
+/*
+ * 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.text.method;
+
+import android.app.Activity;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.text.InputType;
+import android.text.method.BaseKeyListener;
+import android.text.method.KeyListenerTestCase;
+import android.view.KeyEvent;
+import android.widget.EditText;
+import android.widget.TextView.BufferType;
+
+/**
+ * Test forward delete key handling of {@link android.text.method.BaseKeyListener}.
+ *
+ * TODO: Move some of test cases to the CTS.
+ */
+public class ForwardDeleteTest extends KeyListenerTestCase {
+ private static final BaseKeyListener mKeyListener = new BaseKeyListener() {
+ public int getInputType() {
+ return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+ }
+ };
+
+ // Sync the state to the TextView and call onKeyDown with KEYCODE_FORWARD_DEL key event.
+ // Then update the state to the result of TextView.
+ private void forwardDelete(final EditorState state, int modifiers) {
+ mActivity.runOnUiThread(new Runnable() {
+ public void run() {
+ mTextView.setText(state.mText, BufferType.EDITABLE);
+ mTextView.setKeyListener(mKeyListener);
+ mTextView.setSelection(state.mSelectionStart, state.mSelectionEnd);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+ assertTrue(mTextView.hasWindowFocus());
+
+ final KeyEvent keyEvent = getKey(KeyEvent.KEYCODE_FORWARD_DEL, modifiers);
+ mActivity.runOnUiThread(new Runnable() {
+ public void run() {
+ mTextView.onKeyDown(keyEvent.getKeyCode(), keyEvent);
+ }
+ });
+ mInstrumentation.waitForIdleSync();
+
+ state.mText = mTextView.getText();
+ state.mSelectionStart = mTextView.getSelectionStart();
+ state.mSelectionEnd = mTextView.getSelectionEnd();
+ }
+
+ @SmallTest
+ public void testSurrogatePairs() {
+ EditorState state = new EditorState();
+
+ // U+1F441 is EYE
+ state.setByString("| U+1F441");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // U+1F5E8 is LEFT SPEECH BUBBLE
+ state.setByString("| U+1F441 U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // TODO: introduce edge cases.
+ }
+
+ @SmallTest
+ public void testReplacementSpan() {
+ EditorState state = new EditorState();
+
+ state.setByString("| 'abc' ( 'de' ) 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("| 'bc' ( 'de' ) 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("| 'c' ( 'de' ) 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("| ( 'de' ) 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("| 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("| 'g'");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("'abc' [ ( 'de' ) ] 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("'abc' | 'fg'");
+ forwardDelete(state, 0);
+ state.assertEquals("'abc' | 'g'");
+ forwardDelete(state, 0);
+ state.assertEquals("'abc' |");
+ forwardDelete(state, 0);
+ state.assertEquals("'abc' |");
+
+ state.setByString("'ab' [ 'c' ( 'de' ) 'f' ] 'g'");
+ forwardDelete(state, 0);
+ state.assertEquals("'ab' | 'g'");
+ forwardDelete(state, 0);
+ state.assertEquals("'ab' |");
+ forwardDelete(state, 0);
+ state.assertEquals("'ab' |");
+
+ // TODO: introduce edge cases.
+ }
+
+ @SmallTest
+ public void testCombiningEnclosingKeycaps() {
+ EditorState state = new EditorState();
+
+ // U+20E3 is COMBINING ENCLOSING KEYCAP.
+ state.setByString("| '1' U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Edge cases
+ // multiple COMBINING ENCLOSING KEYCAP
+ state.setByString("| '1' U+20E3 U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Isolated COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Isolated multiple COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+20E3 U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+ }
+
+ @SmallTest
+ public void testVariationSelector() {
+ EditorState state = new EditorState();
+
+ // U+FE0F is VARIATION SELECTOR-16.
+ state.setByString("| '#' U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // U+E0100 is VARIATION SELECTOR-17.
+ state.setByString("| U+845B U+E0100");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Edge cases
+ // Isolated variation selectors
+ state.setByString("| U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+E0100");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Isolated multiple variation selectors
+ state.setByString("| U+FE0F U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+FE0F U+E0100");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+E0100 U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+E0100 U+E0100");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Multiple variation selectors
+ state.setByString("| '#' U+FE0F U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| '#' U+FE0F U+E0100");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+845B U+E0100 U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+845B U+E0100 U+E0100");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+ }
+
+ @SmallTest
+ public void testEmojiZeroWidthJoinerSequence() {
+ EditorState state = new EditorState();
+
+ // U+200D is ZERO WIDTH JOINER.
+ state.setByString("| U+1F441 U+200D U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+1F468 U+200D U+2764 U+FE0F U+200D U+1F48B U+200D U+1F468");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Edge cases
+ // End with ZERO WIDTH JOINER
+ state.setByString("| U+1F441 U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Start with ZERO WIDTH JOINER
+ state.setByString("| U+200D U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Multiple ZERO WIDTH JOINER
+ state.setByString("| U+1F441 U+200D U+200D U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Isolated ZERO WIDTH JOINER
+ state.setByString("| U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Isolated multiple ZERO WIDTH JOINER
+ state.setByString("| U+200D U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+ }
+
+ @SmallTest
+ public void testFlags() {
+ EditorState state = new EditorState();
+
+ // U+1F1FA is REGIONAL INDICATOR SYMBOL LETTER U.
+ // U+1F1F8 is REGIONAL INDICATOR SYMBOL LETTER S.
+ state.setByString("| U+1F1FA U+1F1F8");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ state.setByString("| U+1F1FA U+1F1F8 U+1F1FA U+1F1F8");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA U+1F1F8");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Edge cases
+ // Isolated regional indicator symbol
+ state.setByString("| U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Odd numbered regional indicator symbols
+ state.setByString("| U+1F1FA U+1F1F8 U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+ }
+
+ @SmallTest
+ public void testEmojiModifier() {
+ EditorState state = new EditorState();
+
+ // U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2.
+ state.setByString("| U+1F466 U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Edge cases
+ // Isolated emoji modifier
+ state.setByString("| U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Isolated multiple emoji modifier
+ state.setByString("| U+1F3FB U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Multiple emoji modifiers
+ state.setByString("| U+1F466 U+1F3FB U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+ }
+
+ @SmallTest
+ public void testMixedEdgeCases() {
+ EditorState state = new EditorState();
+
+ // COMBINING ENCLOSING KEYCAP + variation selector
+ state.setByString("| '1' U+20E3 U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Variation selector + COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+2665 U+FE0F U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // COMBINING ENCLOSING KEYCAP + ending with ZERO WIDTH JOINER
+ state.setByString("| '1' U+20E3 U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // COMBINING ENCLOSING KEYCAP + ZERO WIDTH JOINER
+ state.setByString("| '1' U+20E3 U+200D U+1F5E8");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F5E8 ");
+
+ // Start with ZERO WIDTH JOINER + COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+200D U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // ZERO WIDTH JOINER + COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+1F441 U+200D U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // COMBINING ENCLOSING KEYCAP + regional indicator symbol
+ state.setByString("| '1' U+20E3 U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Regional indicator symbol + COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+1F1FA U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // COMBINING ENCLOSING KEYCAP + emoji modifier
+ state.setByString("| '1' U+20E3 U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+
+ // Emoji modifier + COMBINING ENCLOSING KEYCAP
+ state.setByString("| U+1F466 U+1F3FB U+20E3");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Variation selector + end with ZERO WIDTH JOINER
+ state.setByString("| U+2665 U+FE0F U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Variation selector + ZERO WIDTH JOINER
+ state.setByString("| U+1F469 U+200D U+2764 U+FE0F U+200D U+1F469");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Start with ZERO WIDTH JOINER + variation selector
+ state.setByString("| U+200D U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // ZERO WIDTH JOINER + variation selector
+ state.setByString("| U+1F469 U+200D U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Variation selector + regional indicator symbol
+ state.setByString("| U+2665 U+FE0F U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Regional indicator symbol + variation selector
+ state.setByString("| U+1F1FA U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Variation selector + emoji modifier
+ state.setByString("| U+2665 U+FE0F U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+
+ // Emoji modifier + variation selector
+ state.setByString("| U+1F466 U+1F3FB U+FE0F");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Start with ZERO WIDTH JOINER + regional indicator symbol
+ state.setByString("| U+200D U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // ZERO WIDTH JOINER + regional indicator symbol
+ state.setByString("| U+1F469 U+200D U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA");
+
+ // Regional indicator symbol + end with ZERO WIDTH JOINER
+ state.setByString("| U+1F1FA U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Regional indicator symbol + ZERO WIDTH JOINER
+ state.setByString("| U+1F1FA U+200D U+1F469");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F469");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Start with ZERO WIDTH JOINER + emoji modifier
+ state.setByString("| U+200D U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+
+ // ZERO WIDTH JOINER + emoji modifier
+ state.setByString("| U+1F469 U+200D U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+
+ // Emoji modifier + end with ZERO WIDTH JOINER
+ state.setByString("| U+1F466 U+1F3FB U+200D");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Emoji modifier + ZERO WIDTH JOINER
+ state.setByString("| U+1F466 U+1F3FB U+200D U+1F469");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F469");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Regional indicator symbol + emoji modifier
+ state.setByString("| U+1F1FA U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F3FB");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+
+ // Emoji modifier + regional indicator symbol
+ state.setByString("| U+1F466 U+1F3FB U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("| U+1F1FA");
+ forwardDelete(state, 0);
+ state.assertEquals("|");
+ }
+}
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 25b6fdd4c350..079b2f28ce47 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;
@@ -353,6 +354,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;
@@ -2190,6 +2198,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) {
@@ -2404,6 +2413,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);
@@ -2553,6 +2569,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);
@@ -4068,6 +4094,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 000000000000..25c84e132804
--- /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<Pair<Integer, Integer>> mNonLifetimes =
+ new ArrayList<Pair<Integer, Integer>>();
+ // 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<Integer, Integer>(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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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<Ra> mRas = new ArrayList<Ra>();
+
+ // 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 c5d38cb73d04..b4c71c13a7e7 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<NetworkAgentInfo> {
// 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<NetworkAgentInfo> {
currentScore = score;
networkMonitor = connService.createNetworkMonitor(context, handler, this, defaultRequest);
networkMisc = misc;
+ apfFilter.maybeInstall(connService, this);
}
/**