diff options
| -rw-r--r-- | core/java/android/net/KeepalivePacketData.java | 2 | ||||
| -rw-r--r-- | core/java/android/net/NattKeepalivePacketData.java | 7 | ||||
| -rw-r--r-- | core/java/android/net/NetworkUtils.java | 22 | ||||
| -rw-r--r-- | core/java/android/net/SocketKeepalive.java | 36 | ||||
| -rw-r--r-- | core/java/android/net/TcpKeepalivePacketData.aidl | 20 | ||||
| -rw-r--r-- | core/java/android/net/TcpKeepalivePacketData.java | 202 | ||||
| -rw-r--r-- | core/java/android/net/TcpRepairWindow.java | 45 | ||||
| -rw-r--r-- | core/jni/android_net_NetUtils.cpp | 67 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/KeepaliveTracker.java | 3 | ||||
| -rw-r--r-- | services/core/java/com/android/server/connectivity/TcpKeepaliveController.java | 330 | ||||
| -rw-r--r-- | tests/net/java/android/net/LinkPropertiesTest.java | 23 | ||||
| -rw-r--r-- | tests/net/java/android/net/TcpKeepalivePacketDataTest.java | 102 | ||||
| -rw-r--r-- | tests/net/java/com/android/internal/util/TestUtils.java | 17 |
13 files changed, 848 insertions, 28 deletions
diff --git a/core/java/android/net/KeepalivePacketData.java b/core/java/android/net/KeepalivePacketData.java index 16555d8bd488..18726f748d30 100644 --- a/core/java/android/net/KeepalivePacketData.java +++ b/core/java/android/net/KeepalivePacketData.java @@ -96,7 +96,7 @@ public class KeepalivePacketData implements Parcelable { out.writeByteArray(mPacket); } - private KeepalivePacketData(Parcel in) { + protected KeepalivePacketData(Parcel in) { srcAddress = NetworkUtils.numericToInetAddress(in.readString()); dstAddress = NetworkUtils.numericToInetAddress(in.readString()); srcPort = in.readInt(); diff --git a/core/java/android/net/NattKeepalivePacketData.java b/core/java/android/net/NattKeepalivePacketData.java index dd2b108cdbfd..bdb246f516a2 100644 --- a/core/java/android/net/NattKeepalivePacketData.java +++ b/core/java/android/net/NattKeepalivePacketData.java @@ -16,6 +16,9 @@ package android.net; +import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; +import static android.net.SocketKeepalive.ERROR_INVALID_PORT; + import android.net.SocketKeepalive.InvalidPacketException; import android.net.util.IpUtils; import android.system.OsConstants; @@ -44,11 +47,11 @@ public final class NattKeepalivePacketData extends KeepalivePacketData { throws InvalidPacketException { if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) { - throw new InvalidPacketException(SocketKeepalive.ERROR_INVALID_IP_ADDRESS); + throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS); } if (dstPort != NattSocketKeepalive.NATT_PORT) { - throw new InvalidPacketException(SocketKeepalive.ERROR_INVALID_PORT); + throw new InvalidPacketException(ERROR_INVALID_PORT); } int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1; diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index 07668a9e3a82..0ae29b125149 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -71,6 +71,18 @@ public class NetworkUtils { throws SocketException; /** + * Attaches a socket filter that drops all of incoming packets. + * @param fd the socket's {@link FileDescriptor}. + */ + public static native void attachDropAllBPFFilter(FileDescriptor fd) throws SocketException; + + /** + * Detaches a socket filter. + * @param fd the socket's {@link FileDescriptor}. + */ + public static native void detachBPFFilter(FileDescriptor fd) throws SocketException; + + /** * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements. * @param fd the socket's {@link FileDescriptor}. * @param ifIndex the interface index. @@ -170,6 +182,16 @@ public class NetworkUtils { private static native void addArpEntry(byte[] ethAddr, byte[] netAddr, String ifname, FileDescriptor fd) throws IOException; + + /** + * Get the tcp repair window associated with the {@code fd}. + * + * @param fd the tcp socket's {@link FileDescriptor}. + * @return a {@link TcpRepairWindow} object indicates tcp window size. + */ + public static native TcpRepairWindow getTcpRepairWindow(FileDescriptor fd) + throws ErrnoException; + /** * @see Inet4AddressUtils#intToInet4AddressHTL(int) * @deprecated Use either {@link Inet4AddressUtils#intToInet4AddressHTH(int)} diff --git a/core/java/android/net/SocketKeepalive.java b/core/java/android/net/SocketKeepalive.java index a47c11af40dc..7ea1bef83669 100644 --- a/core/java/android/net/SocketKeepalive.java +++ b/core/java/android/net/SocketKeepalive.java @@ -110,15 +110,43 @@ public abstract class SocketKeepalive implements AutoCloseable { public static final int MAX_INTERVAL_SEC = 3600; /** - * This packet is invalid. - * See the error code for details. + * An exception that embarks an error code. * @hide */ - public static class InvalidPacketException extends Exception { + public static class ErrorCodeException extends Exception { public final int error; - public InvalidPacketException(int error) { + public ErrorCodeException(final int error, final Throwable e) { + super(e); this.error = error; } + public ErrorCodeException(final int error) { + this.error = error; + } + } + + /** + * This socket is invalid. + * See the error code for details, and the optional cause. + * @hide + */ + public static class InvalidSocketException extends ErrorCodeException { + public InvalidSocketException(final int error, final Throwable e) { + super(error, e); + } + public InvalidSocketException(final int error) { + super(error); + } + } + + /** + * This packet is invalid. + * See the error code for details. + * @hide + */ + public static class InvalidPacketException extends ErrorCodeException { + public InvalidPacketException(final int error) { + super(error); + } } @NonNull final IConnectivityManager mService; diff --git a/core/java/android/net/TcpKeepalivePacketData.aidl b/core/java/android/net/TcpKeepalivePacketData.aidl new file mode 100644 index 000000000000..c91eb03bd598 --- /dev/null +++ b/core/java/android/net/TcpKeepalivePacketData.aidl @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2019 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.net; + +parcelable TcpKeepalivePacketData; + diff --git a/core/java/android/net/TcpKeepalivePacketData.java b/core/java/android/net/TcpKeepalivePacketData.java new file mode 100644 index 000000000000..aba4989e0bf3 --- /dev/null +++ b/core/java/android/net/TcpKeepalivePacketData.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2019 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.net; + +import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS; + +import android.annotation.Nullable; +import android.net.SocketKeepalive.InvalidPacketException; +import android.net.util.IpUtils; +import android.os.Parcel; +import android.os.Parcelable; +import android.system.OsConstants; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Objects; + +/** + * Represents the actual tcp keep alive packets which will be used for hardware offload. + * @hide + */ +public class TcpKeepalivePacketData extends KeepalivePacketData implements Parcelable { + private static final String TAG = "TcpKeepalivePacketData"; + + /** TCP sequence number. */ + public final int tcpSeq; + + /** TCP ACK number. */ + public final int tcpAck; + + /** TCP RCV window. */ + public final int tcpWnd; + + /** TCP RCV window scale. */ + public final int tcpWndScale; + + private static final int IPV4_HEADER_LENGTH = 20; + private static final int IPV6_HEADER_LENGTH = 40; + private static final int TCP_HEADER_LENGTH = 20; + + // This should only be constructed via static factory methods, such as + // tcpKeepalivePacket. + private TcpKeepalivePacketData(TcpSocketInfo tcpDetails, byte[] data) + throws InvalidPacketException { + super(tcpDetails.srcAddress, tcpDetails.srcPort, tcpDetails.dstAddress, + tcpDetails.dstPort, data); + tcpSeq = tcpDetails.seq; + tcpAck = tcpDetails.ack; + // In the packet, the window is shifted right by the window scale. + tcpWnd = tcpDetails.rcvWnd; + tcpWndScale = tcpDetails.rcvWndScale; + } + + /** + * Factory method to create tcp keepalive packet structure. + */ + public static TcpKeepalivePacketData tcpKeepalivePacket( + TcpSocketInfo tcpDetails) throws InvalidPacketException { + final byte[] packet; + if ((tcpDetails.srcAddress instanceof Inet4Address) + && (tcpDetails.dstAddress instanceof Inet4Address)) { + packet = buildV4Packet(tcpDetails); + } else { + // TODO: support ipv6 + throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS); + } + + return new TcpKeepalivePacketData(tcpDetails, packet); + } + + /** + * Build ipv4 tcp keepalive packet, not including the link-layer header. + */ + // TODO : if this code is ever moved to the network stack, factorize constants with the ones + // over there. + private static byte[] buildV4Packet(TcpSocketInfo tcpDetails) { + final int length = IPV4_HEADER_LENGTH + TCP_HEADER_LENGTH; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.BIG_ENDIAN); + // IP version and TOS. TODO : fetch this from getsockopt(SOL_IP, IP_TOS) + buf.putShort((short) 0x4500); + buf.putShort((short) length); + buf.putInt(0x4000); // ID, flags=DF, offset + // TODO : fetch TTL from getsockopt(SOL_IP, IP_TTL) + buf.put((byte) 64); + buf.put((byte) OsConstants.IPPROTO_TCP); + final int ipChecksumOffset = buf.position(); + buf.putShort((short) 0); // IP checksum + buf.put(tcpDetails.srcAddress.getAddress()); + buf.put(tcpDetails.dstAddress.getAddress()); + buf.putShort((short) tcpDetails.srcPort); + buf.putShort((short) tcpDetails.dstPort); + buf.putInt(tcpDetails.seq); // Sequence Number + buf.putInt(tcpDetails.ack); // ACK + buf.putShort((short) 0x5010); // TCP length=5, flags=ACK + buf.putShort((short) (tcpDetails.rcvWnd >> tcpDetails.rcvWndScale)); // Window size + final int tcpChecksumOffset = buf.position(); + buf.putShort((short) 0); // TCP checksum + // URG is not set therefore the urgent pointer is not included + buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0)); + buf.putShort(tcpChecksumOffset, IpUtils.tcpChecksum( + buf, 0, IPV4_HEADER_LENGTH, TCP_HEADER_LENGTH)); + + return buf.array(); + } + + // TODO: add buildV6Packet. + + /** Represents tcp/ip information. */ + public static class TcpSocketInfo { + public final InetAddress srcAddress; + public final InetAddress dstAddress; + public final int srcPort; + public final int dstPort; + public final int seq; + public final int ack; + public final int rcvWnd; + public final int rcvWndScale; + + public TcpSocketInfo(InetAddress sAddr, int sPort, InetAddress dAddr, + int dPort, int writeSeq, int readSeq, int rWnd, int rWndScale) { + srcAddress = sAddr; + dstAddress = dAddr; + srcPort = sPort; + dstPort = dPort; + seq = writeSeq; + ack = readSeq; + rcvWnd = rWnd; + rcvWndScale = rWndScale; + } + } + + @Override + public boolean equals(@Nullable final Object o) { + if (!(o instanceof TcpKeepalivePacketData)) return false; + final TcpKeepalivePacketData other = (TcpKeepalivePacketData) o; + return this.srcAddress.equals(other.srcAddress) + && this.dstAddress.equals(other.dstAddress) + && this.srcPort == other.srcPort + && this.dstPort == other.dstPort + && this.tcpAck == other.tcpAck + && this.tcpSeq == other.tcpSeq + && this.tcpWnd == other.tcpWnd + && this.tcpWndScale == other.tcpWndScale; + } + + @Override + public int hashCode() { + return Objects.hash(srcAddress, dstAddress, srcPort, dstPort, tcpAck, tcpSeq, tcpWnd, + tcpWndScale); + } + + /* Parcelable Implementation. */ + /** No special parcel contents. */ + public int describeContents() { + return 0; + } + + /** Write to parcel. */ + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(tcpSeq); + out.writeInt(tcpAck); + out.writeInt(tcpWnd); + out.writeInt(tcpWndScale); + } + + private TcpKeepalivePacketData(Parcel in) { + super(in); + tcpSeq = in.readInt(); + tcpAck = in.readInt(); + tcpWnd = in.readInt(); + tcpWndScale = in.readInt(); + } + + /** Parcelable Creator. */ + public static final Parcelable.Creator<TcpKeepalivePacketData> CREATOR = + new Parcelable.Creator<TcpKeepalivePacketData>() { + public TcpKeepalivePacketData createFromParcel(Parcel in) { + return new TcpKeepalivePacketData(in); + } + + public TcpKeepalivePacketData[] newArray(int size) { + return new TcpKeepalivePacketData[size]; + } + }; +} diff --git a/core/java/android/net/TcpRepairWindow.java b/core/java/android/net/TcpRepairWindow.java new file mode 100644 index 000000000000..86034f0a76ed --- /dev/null +++ b/core/java/android/net/TcpRepairWindow.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 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.net; + +/** + * Corresponds to C's {@code struct tcp_repair_window} from + * include/uapi/linux/tcp.h + * + * @hide + */ +public final class TcpRepairWindow { + public final int sndWl1; + public final int sndWnd; + public final int maxWindow; + public final int rcvWnd; + public final int rcvWup; + public final int rcvWndScale; + + /** + * Constructs an instance with the given field values. + */ + public TcpRepairWindow(final int sndWl1, final int sndWnd, final int maxWindow, + final int rcvWnd, final int rcvWup, final int rcvWndScale) { + this.sndWl1 = sndWl1; + this.sndWnd = sndWnd; + this.maxWindow = maxWindow; + this.rcvWnd = rcvWnd; + this.rcvWup = rcvWup; + this.rcvWndScale = rcvWndScale; + } +} diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp index 7eddcfe425d3..cfb2dd199f39 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -29,6 +29,7 @@ #include <net/if.h> #include <linux/filter.h> #include <linux/if_arp.h> +#include <linux/tcp.h> #include <netinet/ether.h> #include <netinet/icmp6.h> #include <netinet/ip.h> @@ -226,6 +227,34 @@ static void android_net_utils_attachControlPacketFilter( } } +static void android_net_utils_attachDropAllBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd) +{ + struct sock_filter filter_code[] = { + // Reject all. + 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)); + } +} + +static void android_net_utils_detachBPFFilter(JNIEnv *env, jobject clazz, jobject javaFd) +{ + int dummy = 0; + int fd = jniGetFDFromFileDescriptor(env, javaFd); + if (setsockopt(fd, SOL_SOCKET, SO_DETACH_FILTER, &dummy, sizeof(dummy)) != 0) { + jniThrowExceptionFmt(env, "java/net/SocketException", + "setsockopt(SO_DETACH_FILTER): %s", strerror(errno)); + } + +} static void android_net_utils_setupRaSocket(JNIEnv *env, jobject clazz, jobject javaFd, jint ifIndex) { @@ -458,6 +487,41 @@ static jbyteArray android_net_utils_resNetworkResult(JNIEnv *env, jobject thiz, return answer; } +static jobject android_net_utils_getTcpRepairWindow(JNIEnv *env, jobject thiz, jobject javaFd) { + if (javaFd == NULL) { + jniThrowNullPointerException(env, NULL); + return NULL; + } + + int fd = jniGetFDFromFileDescriptor(env, javaFd); + struct tcp_repair_window trw = {}; + socklen_t size = sizeof(trw); + + // Obtain the parameters of the TCP repair window. + int rc = getsockopt(fd, IPPROTO_TCP, TCP_REPAIR_WINDOW, &trw, &size); + if (rc == -1) { + throwErrnoException(env, "getsockopt : TCP_REPAIR_WINDOW", errno); + return NULL; + } + + struct tcp_info tcpinfo = {}; + socklen_t tcpinfo_size = sizeof(tcp_info); + + // Obtain the window scale from the tcp info structure. This contains a scale factor that + // should be applied to the window size. + rc = getsockopt(fd, IPPROTO_TCP, TCP_INFO, &tcpinfo, &tcpinfo_size); + if (rc == -1) { + throwErrnoException(env, "getsockopt : TCP_INFO", errno); + return NULL; + } + + jclass class_TcpRepairWindow = env->FindClass("android/net/TcpRepairWindow"); + jmethodID ctor = env->GetMethodID(class_TcpRepairWindow, "<init>", "(IIIIII)V"); + + return env->NewObject(class_TcpRepairWindow, ctor, trw.snd_wl1, trw.snd_wnd, trw.max_window, + trw.rcv_wnd, trw.rcv_wup, tcpinfo.tcpi_rcv_wscale); +} + // ---------------------------------------------------------------------------- /* @@ -475,6 +539,9 @@ static const JNINativeMethod gNetworkUtilMethods[] = { { "attachDhcpFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDhcpFilter }, { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter }, { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter }, + { "attachDropAllBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_attachDropAllBPFFilter }, + { "detachBPFFilter", "(Ljava/io/FileDescriptor;)V", (void*) android_net_utils_detachBPFFilter }, + { "getTcpRepairWindow", "(Ljava/io/FileDescriptor;)Landroid/net/TcpRepairWindow;", (void*) android_net_utils_getTcpRepairWindow }, { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket }, { "resNetworkSend", "(I[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend }, { "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery }, diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java index 07e28f9fb543..d872e4d428ab 100644 --- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java +++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java @@ -381,8 +381,7 @@ public class KeepaliveTracker { } KeepaliveInfo ki = new KeepaliveInfo(messenger, binder, nai, packet, intervalSeconds); Log.d(TAG, "Created keepalive: " + ki.toString()); - mConnectivityServiceHandler.obtainMessage( - CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget(); + mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget(); } /** diff --git a/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java b/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java new file mode 100644 index 000000000000..640504ff6e29 --- /dev/null +++ b/services/core/java/com/android/server/connectivity/TcpKeepaliveController.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2019 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.net.NetworkAgent.EVENT_SOCKET_KEEPALIVE; +import static android.net.SocketKeepalive.DATA_RECEIVED; +import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET; +import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR; +import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT; +import static android.system.OsConstants.FIONREAD; +import static android.system.OsConstants.IPPROTO_TCP; +import static android.system.OsConstants.TIOCOUTQ; + +import android.annotation.NonNull; +import android.net.NetworkUtils; +import android.net.SocketKeepalive.InvalidSocketException; +import android.net.TcpKeepalivePacketData.TcpSocketInfo; +import android.net.TcpRepairWindow; +import android.os.Handler; +import android.os.Message; +import android.os.MessageQueue; +import android.os.Messenger; +import android.os.RemoteException; +import android.system.ErrnoException; +import android.system.Int32Ref; +import android.system.Os; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; + +import java.io.FileDescriptor; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.SocketException; + +/** + * Manage tcp socket which offloads tcp keepalive. + * + * The input socket will be changed to repair mode and the application + * will not have permission to read/write data. If the application wants + * to write data, it must stop tcp keepalive offload to leave repair mode + * first. If a remote packet arrives, repair mode will be turned off and + * offload will be stopped. The application will receive a callback to know + * it can start reading data. + * + * {start,stop}SocketMonitor are thread-safe, but care must be taken in the + * order in which they are called. Please note that while calling + * {@link #startSocketMonitor(FileDescriptor, Messenger, int)} multiple times + * with either the same slot or the same FileDescriptor without stopping it in + * between will result in an exception, calling {@link #stopSocketMonitor(int)} + * multiple times with the same int is explicitly a no-op. + * Please also note that switching the socket to repair mode is not synchronized + * with either of these operations and has to be done in an orderly fashion + * with stopSocketMonitor. Take care in calling these in the right order. + * @hide + */ +public class TcpKeepaliveController { + private static final String TAG = "TcpKeepaliveController"; + private static final boolean DBG = false; + + private final MessageQueue mFdHandlerQueue; + + private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR; + + // Reference include/uapi/linux/tcp.h + private static final int TCP_REPAIR = 19; + private static final int TCP_REPAIR_QUEUE = 20; + private static final int TCP_QUEUE_SEQ = 21; + private static final int TCP_NO_QUEUE = 0; + private static final int TCP_RECV_QUEUE = 1; + private static final int TCP_SEND_QUEUE = 2; + private static final int TCP_REPAIR_OFF = 0; + private static final int TCP_REPAIR_ON = 1; + // Reference include/uapi/linux/sockios.h + private static final int SIOCINQ = FIONREAD; + private static final int SIOCOUTQ = TIOCOUTQ; + + /** + * Keeps track of packet listeners. + * Key: slot number of keepalive offload. + * Value: {@link FileDescriptor} being listened to. + */ + @GuardedBy("mListeners") + private final SparseArray<FileDescriptor> mListeners = new SparseArray<>(); + + public TcpKeepaliveController(final Handler connectivityServiceHandler) { + mFdHandlerQueue = connectivityServiceHandler.getLooper().getQueue(); + } + + /** + * Switch the tcp socket to repair mode and query tcp socket information. + * + * @param fd the fd of socket on which to use keepalive offload + * @return a {@link TcpKeepalivePacketData#TcpSocketInfo} object for current + * tcp/ip information. + */ + // TODO : make this private. It's far too confusing that this gets called from outside + // at a time that nobody can understand, but the switch out is in this class only. + public static TcpSocketInfo switchToRepairMode(FileDescriptor fd) + throws InvalidSocketException { + if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd); + final SocketAddress srcSockAddr; + final SocketAddress dstSockAddr; + final InetAddress srcAddress; + final InetAddress dstAddress; + final int srcPort; + final int dstPort; + int seq; + final int ack; + final TcpRepairWindow trw; + + // Query source address and port. + try { + srcSockAddr = Os.getsockname(fd); + } catch (ErrnoException e) { + Log.e(TAG, "Get sockname fail: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + if (srcSockAddr instanceof InetSocketAddress) { + srcAddress = getAddress((InetSocketAddress) srcSockAddr); + srcPort = getPort((InetSocketAddress) srcSockAddr); + } else { + Log.e(TAG, "Invalid or mismatched SocketAddress"); + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + // Query destination address and port. + try { + dstSockAddr = Os.getpeername(fd); + } catch (ErrnoException e) { + Log.e(TAG, "Get peername fail: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + if (dstSockAddr instanceof InetSocketAddress) { + dstAddress = getAddress((InetSocketAddress) dstSockAddr); + dstPort = getPort((InetSocketAddress) dstSockAddr); + } else { + Log.e(TAG, "Invalid or mismatched peer SocketAddress"); + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + + // Query sequence and ack number + dropAllIncomingPackets(fd, true); + try { + // Enter tcp repair mode. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON); + // Check if socket is idle. + if (!isSocketIdle(fd)) { + throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE); + } + // Query write sequence number from SEND_QUEUE. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_SEND_QUEUE); + seq = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ); + // Query read sequence number from RECV_QUEUE. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_RECV_QUEUE); + ack = Os.getsockoptInt(fd, IPPROTO_TCP, TCP_QUEUE_SEQ); + // Switch to NO_QUEUE to prevent illegal socket read/write in repair mode. + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR_QUEUE, TCP_NO_QUEUE); + // Finally, check if socket is still idle. TODO : this check needs to move to + // after starting polling to prevent a race. + if (!isSocketIdle(fd)) { + throw new InvalidSocketException(ERROR_INVALID_SOCKET); + } + + // Query tcp window size. + trw = NetworkUtils.getTcpRepairWindow(fd); + } catch (ErrnoException e) { + Log.e(TAG, "Exception reading TCP state from socket", e); + try { + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF); + } catch (ErrnoException ex) { + Log.e(TAG, "Exception while turning off repair mode due to exception", ex); + } + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } finally { + dropAllIncomingPackets(fd, false); + } + + // Keepalive sequence number is last sequence number - 1. If it couldn't be retrieved, + // then it must be set to -1, so decrement in all cases. + seq = seq - 1; + + return new TcpSocketInfo(srcAddress, srcPort, dstAddress, dstPort, seq, ack, trw.rcvWnd, + trw.rcvWndScale); + } + + private static void switchOutOfRepairMode(@NonNull final FileDescriptor fd) + throws ErrnoException { + Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF); + } + + /** + * Start monitoring incoming packets. + * + * @param fd socket fd to monitor. + * @param messenger a callback to notify socket status. + * @param slot keepalive slot. + */ + public void startSocketMonitor(@NonNull final FileDescriptor fd, + @NonNull final Messenger messenger, final int slot) { + synchronized (mListeners) { + if (null != mListeners.get(slot)) { + throw new IllegalArgumentException("This slot is already taken"); + } + for (int i = 0; i < mListeners.size(); ++i) { + if (fd.equals(mListeners.valueAt(i))) { + throw new IllegalArgumentException("This fd is already registered"); + } + } + mFdHandlerQueue.addOnFileDescriptorEventListener(fd, FD_EVENTS, (readyFd, events) -> { + // This can't be called twice because the queue guarantees that once the listener + // is unregistered it can't be called again, even for a message that arrived + // before it was unregistered. + int result; + try { + // First move the socket out of repair mode. + if (DBG) Log.d(TAG, "Moving socket out of repair mode for event : " + readyFd); + switchOutOfRepairMode(readyFd); + result = (0 != (events & EVENT_ERROR)) ? ERROR_INVALID_SOCKET : DATA_RECEIVED; + } catch (ErrnoException e) { + // Could not move the socket out of repair mode. Still continue with notifying + // the client + Log.e(TAG, "Cannot switch socket out of repair mode", e); + result = ERROR_INVALID_SOCKET; + } + // Prepare and send the message to the receiver. + final Message message = Message.obtain(); + message.what = EVENT_SOCKET_KEEPALIVE; + message.arg1 = slot; + message.arg2 = result; + try { + messenger.send(message); + } catch (RemoteException e) { + // Remote process died + } + synchronized (mListeners) { + mListeners.remove(slot); + } + // The listener returns the new set of events to listen to. Because 0 means no + // event, the listener gets unregistered. + return 0; + }); + mListeners.put(slot, fd); + } + } + + /** Stop socket monitor */ + // This slot may have been stopped automatically already because the socket received data, + // was closed on the other end or otherwise suffered some error. In this case, this function + // is a no-op. + public void stopSocketMonitor(final int slot) { + final FileDescriptor fd; + synchronized (mListeners) { + fd = mListeners.get(slot); + if (null == fd) return; + mListeners.remove(slot); + } + mFdHandlerQueue.removeOnFileDescriptorEventListener(fd); + try { + if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd); + switchOutOfRepairMode(fd); + } catch (ErrnoException e) { + Log.e(TAG, "Cannot switch socket out of repair mode", e); + // Well, there is not much to do here to recover + } + } + + private static InetAddress getAddress(InetSocketAddress inetAddr) { + return inetAddr.getAddress(); + } + + private static int getPort(InetSocketAddress inetAddr) { + return inetAddr.getPort(); + } + + private static boolean isSocketIdle(FileDescriptor fd) throws ErrnoException { + return isReceiveQueueEmpty(fd) && isSendQueueEmpty(fd); + } + + private static boolean isReceiveQueueEmpty(FileDescriptor fd) + throws ErrnoException { + Int32Ref result = new Int32Ref(-1); + Os.ioctlInt(fd, SIOCINQ, result); + if (result.value != 0) { + Log.e(TAG, "Read queue has data"); + return false; + } + return true; + } + + private static boolean isSendQueueEmpty(FileDescriptor fd) + throws ErrnoException { + Int32Ref result = new Int32Ref(-1); + Os.ioctlInt(fd, SIOCOUTQ, result); + if (result.value != 0) { + Log.e(TAG, "Write queue has data"); + return false; + } + return true; + } + + private static void dropAllIncomingPackets(FileDescriptor fd, boolean enable) + throws InvalidSocketException { + try { + if (enable) { + NetworkUtils.attachDropAllBPFFilter(fd); + } else { + NetworkUtils.detachBPFFilter(fd); + } + } catch (SocketException e) { + Log.e(TAG, "Socket Exception: ", e); + throw new InvalidSocketException(ERROR_INVALID_SOCKET, e); + } + } +} diff --git a/tests/net/java/android/net/LinkPropertiesTest.java b/tests/net/java/android/net/LinkPropertiesTest.java index 299fbefc78e4..bdde0961909d 100644 --- a/tests/net/java/android/net/LinkPropertiesTest.java +++ b/tests/net/java/android/net/LinkPropertiesTest.java @@ -22,18 +22,15 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.net.IpPrefix; -import android.net.LinkAddress; -import android.net.LinkProperties; import android.net.LinkProperties.CompareResult; import android.net.LinkProperties.ProvisioningChange; -import android.net.RouteInfo; -import android.os.Parcel; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; import android.system.OsConstants; import android.util.ArraySet; +import com.android.internal.util.TestUtils; + import org.junit.Test; import org.junit.runner.RunWith; @@ -849,18 +846,6 @@ public class LinkPropertiesTest { assertEquals(new ArraySet<>(expectRemoved), (new ArraySet<>(result.removed))); } - private void assertParcelingIsLossless(LinkProperties source) { - Parcel p = Parcel.obtain(); - source.writeToParcel(p, /* flags */ 0); - p.setDataPosition(0); - final byte[] marshalled = p.marshall(); - p = Parcel.obtain(); - p.unmarshall(marshalled, 0, marshalled.length); - p.setDataPosition(0); - LinkProperties dest = LinkProperties.CREATOR.createFromParcel(p); - assertEquals(source, dest); - } - @Test public void testLinkPropertiesParcelable() throws Exception { LinkProperties source = new LinkProperties(); @@ -882,12 +867,12 @@ public class LinkPropertiesTest { source.setNat64Prefix(new IpPrefix("2001:db8:1:2:64:64::/96")); - assertParcelingIsLossless(source); + TestUtils.assertParcelingIsLossless(source, LinkProperties.CREATOR); } @Test public void testParcelUninitialized() throws Exception { LinkProperties empty = new LinkProperties(); - assertParcelingIsLossless(empty); + TestUtils.assertParcelingIsLossless(empty, LinkProperties.CREATOR); } } diff --git a/tests/net/java/android/net/TcpKeepalivePacketDataTest.java b/tests/net/java/android/net/TcpKeepalivePacketDataTest.java new file mode 100644 index 000000000000..16fc41b816e6 --- /dev/null +++ b/tests/net/java/android/net/TcpKeepalivePacketDataTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2019 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.net; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.net.SocketKeepalive.InvalidPacketException; +import android.net.TcpKeepalivePacketData.TcpSocketInfo; + +import com.android.internal.util.TestUtils; + +import libcore.net.InetAddressUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.net.InetAddress; +import java.nio.ByteBuffer; + +@RunWith(JUnit4.class) +public final class TcpKeepalivePacketDataTest { + + @Before + public void setUp() {} + + @Test + public void testV4TcpKeepalivePacket() { + final InetAddress srcAddr = InetAddressUtils.parseNumericAddress("192.168.0.1"); + final InetAddress dstAddr = InetAddressUtils.parseNumericAddress("192.168.0.10"); + final int srcPort = 1234; + final int dstPort = 4321; + final int seq = 0x11111111; + final int ack = 0x22222222; + final int wnd = 8000; + final int wndScale = 2; + TcpKeepalivePacketData resultData = null; + TcpSocketInfo testInfo = new TcpSocketInfo( + srcAddr, srcPort, dstAddr, dstPort, seq, ack, wnd, wndScale); + try { + resultData = TcpKeepalivePacketData.tcpKeepalivePacket(testInfo); + } catch (InvalidPacketException e) { + fail("InvalidPacketException: " + e); + } + + assertEquals(testInfo.srcAddress, resultData.srcAddress); + assertEquals(testInfo.dstAddress, resultData.dstAddress); + assertEquals(testInfo.srcPort, resultData.srcPort); + assertEquals(testInfo.dstPort, resultData.dstPort); + assertEquals(testInfo.seq, resultData.tcpSeq); + assertEquals(testInfo.ack, resultData.tcpAck); + assertEquals(testInfo.rcvWndScale, resultData.tcpWndScale); + + TestUtils.assertParcelingIsLossless(resultData, TcpKeepalivePacketData.CREATOR); + + final byte[] packet = resultData.getPacket(); + // IP version and TOS. + ByteBuffer buf = ByteBuffer.wrap(packet); + assertEquals(buf.getShort(), 0x4500); + // Source IP address. + byte[] ip = new byte[4]; + buf = ByteBuffer.wrap(packet, 12, 4); + buf.get(ip); + assertArrayEquals(ip, srcAddr.getAddress()); + // Destination IP address. + buf = ByteBuffer.wrap(packet, 16, 4); + buf.get(ip); + assertArrayEquals(ip, dstAddr.getAddress()); + + buf = ByteBuffer.wrap(packet, 20, 12); + // Source port. + assertEquals(buf.getShort(), srcPort); + // Destination port. + assertEquals(buf.getShort(), dstPort); + // Sequence number. + assertEquals(buf.getInt(), seq); + // Ack. + assertEquals(buf.getInt(), ack); + // Window size. + buf = ByteBuffer.wrap(packet, 34, 2); + assertEquals(buf.getShort(), wnd >> wndScale); + } + + //TODO: add ipv6 test when ipv6 supported +} diff --git a/tests/net/java/com/android/internal/util/TestUtils.java b/tests/net/java/com/android/internal/util/TestUtils.java index 6db01d343756..7e5a1d3ad4d3 100644 --- a/tests/net/java/com/android/internal/util/TestUtils.java +++ b/tests/net/java/com/android/internal/util/TestUtils.java @@ -16,12 +16,15 @@ package com.android.internal.util; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import android.os.ConditionVariable; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; public final class TestUtils { private TestUtils() { } @@ -50,4 +53,18 @@ public final class TestUtils { fail(handler.toString() + " did not become idle after " + timeoutMs + " ms"); } } + + // TODO : fetch the creator through reflection or something instead of passing it + public static <T extends Parcelable, C extends Parcelable.Creator<T>> + void assertParcelingIsLossless(T source, C creator) { + Parcel p = Parcel.obtain(); + source.writeToParcel(p, /* flags */ 0); + p.setDataPosition(0); + final byte[] marshalled = p.marshall(); + p = Parcel.obtain(); + p.unmarshall(marshalled, 0, marshalled.length); + p.setDataPosition(0); + T dest = creator.createFromParcel(p); + assertEquals(source, dest); + } } |