From e5591ce2fa8d051b46d6ae1623b0007cabe8aae7 Mon Sep 17 00:00:00 2001 From: markchien Date: Thu, 27 Dec 2018 22:49:51 +0800 Subject: [PATCH] [KA03] Support tcp keepalive offload When offload is starting, socket will be switched to repair mode. Read and write on the socket will not be allowed until repair mode is turned off. If remote packet arrives, repair mode will be turned off automatically and a callback will be raised to indicate that socket is ready to read from. Bug: 114151147 Test: -atest FrameworksNetTests -manual Change-Id: I0c335865912e183e7ad32a8ea12188f02ccde5fd --- .../java/android/net/KeepalivePacketData.java | 2 +- .../android/net/NattKeepalivePacketData.java | 7 +- core/java/android/net/NetworkUtils.java | 22 ++ core/java/android/net/SocketKeepalive.java | 36 +- .../android/net/TcpKeepalivePacketData.java | 202 +++++++++++ core/java/android/net/TcpRepairWindow.java | 45 +++ core/jni/android_net_NetUtils.cpp | 67 ++++ .../server/connectivity/KeepaliveTracker.java | 3 +- .../connectivity/TcpKeepaliveController.java | 330 ++++++++++++++++++ .../java/android/net/LinkPropertiesTest.java | 23 +- .../net/TcpKeepalivePacketDataTest.java | 102 ++++++ 11 files changed, 811 insertions(+), 28 deletions(-) create mode 100644 core/java/android/net/TcpKeepalivePacketData.java create mode 100644 core/java/android/net/TcpRepairWindow.java create mode 100644 services/core/java/com/android/server/connectivity/TcpKeepaliveController.java create mode 100644 tests/net/java/android/net/TcpKeepalivePacketDataTest.java diff --git a/core/java/android/net/KeepalivePacketData.java b/core/java/android/net/KeepalivePacketData.java index 16555d8bd4..18726f748d 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 dd2b108cdb..bdb246f516 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 07668a9e3a..0ae29b1251 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -70,6 +70,18 @@ public class NetworkUtils { public native static void attachControlPacketFilter(FileDescriptor fd, int packetType) 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}. @@ -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 a47c11af40..7ea1bef836 100644 --- a/core/java/android/net/SocketKeepalive.java +++ b/core/java/android/net/SocketKeepalive.java @@ -109,15 +109,43 @@ public abstract class SocketKeepalive implements AutoCloseable { **/ public static final int MAX_INTERVAL_SEC = 3600; + /** + * An exception that embarks an error code. + * @hide + */ + public static class ErrorCodeException extends Exception { + public final 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 Exception { - public final int error; - public InvalidPacketException(int error) { - this.error = error; + public static class InvalidPacketException extends ErrorCodeException { + public InvalidPacketException(final int error) { + super(error); } } diff --git a/core/java/android/net/TcpKeepalivePacketData.java b/core/java/android/net/TcpKeepalivePacketData.java new file mode 100644 index 0000000000..aba4989e0b --- /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 CREATOR = + new Parcelable.Creator() { + 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 0000000000..86034f0a76 --- /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 7eddcfe425..cfb2dd199f 100644 --- a/core/jni/android_net_NetUtils.cpp +++ b/core/jni/android_net_NetUtils.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -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, "", "(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 07e28f9fb5..d872e4d428 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 0000000000..640504ff6e --- /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 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 299fbefc78..bdde096190 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 0000000000..16fc41b816 --- /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 +}