diff options
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); } /** |