blob: e570ef1e9bf2042f122e365e36924d046370fe13 [file] [log] [blame]
/*
* 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.SocketKeepalive.DATA_RECEIVED;
import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
import static android.net.SocketKeepalive.ERROR_SOCKET_NOT_IDLE;
import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
import static android.system.OsConstants.ENOPROTOOPT;
import static android.system.OsConstants.FIONREAD;
import static android.system.OsConstants.IPPROTO_IP;
import static android.system.OsConstants.IPPROTO_TCP;
import static android.system.OsConstants.IP_TOS;
import static android.system.OsConstants.IP_TTL;
import static android.system.OsConstants.TIOCOUTQ;
import android.annotation.NonNull;
import android.net.NetworkUtils;
import android.net.SocketKeepalive.InvalidPacketException;
import android.net.SocketKeepalive.InvalidSocketException;
import android.net.TcpKeepalivePacketData;
import android.net.TcpKeepalivePacketDataParcelable;
import android.net.TcpRepairWindow;
import android.os.Handler;
import android.os.MessageQueue;
import android.os.Messenger;
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 com.android.server.connectivity.KeepaliveTracker.KeepaliveInfo;
import java.io.FileDescriptor;
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();
}
/** Build tcp keepalive packet. */
public static TcpKeepalivePacketData getTcpKeepalivePacket(@NonNull FileDescriptor fd)
throws InvalidPacketException, InvalidSocketException {
try {
final TcpKeepalivePacketDataParcelable tcpDetails = switchToRepairMode(fd);
return TcpKeepalivePacketData.tcpKeepalivePacket(tcpDetails);
} catch (InvalidPacketException | InvalidSocketException e) {
switchOutOfRepairMode(fd);
throw e;
}
}
/**
* Switch the tcp socket to repair mode and query detail tcp information.
*
* @param fd the fd of socket on which to use keepalive offload.
* @return a {@link TcpKeepalivePacketData#TcpKeepalivePacketDataParcelable} object for current
* tcp/ip information.
*/
private static TcpKeepalivePacketDataParcelable switchToRepairMode(FileDescriptor fd)
throws InvalidSocketException {
if (DBG) Log.i(TAG, "switchToRepairMode to start tcp keepalive : " + fd);
final TcpKeepalivePacketDataParcelable tcpDetails = new TcpKeepalivePacketDataParcelable();
final SocketAddress srcSockAddr;
final SocketAddress dstSockAddr;
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) {
tcpDetails.srcAddress = getAddress((InetSocketAddress) srcSockAddr);
tcpDetails.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) {
tcpDetails.dstAddress = getAddress((InetSocketAddress) dstSockAddr);
tcpDetails.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 {
// Switch to tcp repair mode.
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_ON);
// Check if socket is idle.
if (!isSocketIdle(fd)) {
Log.e(TAG, "Socket is not idle");
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);
tcpDetails.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);
tcpDetails.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 (!isReceiveQueueEmpty(fd)) {
Log.e(TAG, "Fatal: receive queue of this socket is not empty");
throw new InvalidSocketException(ERROR_INVALID_SOCKET);
}
if (!isSendQueueEmpty(fd)) {
Log.e(TAG, "Socket is not idle");
throw new InvalidSocketException(ERROR_SOCKET_NOT_IDLE);
}
// Query tcp window size.
trw = NetworkUtils.getTcpRepairWindow(fd);
tcpDetails.rcvWnd = trw.rcvWnd;
tcpDetails.rcvWndScale = trw.rcvWndScale;
if (tcpDetails.srcAddress.length == 4 /* V4 address length */) {
// Query TOS.
tcpDetails.tos = Os.getsockoptInt(fd, IPPROTO_IP, IP_TOS);
// Query TTL.
tcpDetails.ttl = Os.getsockoptInt(fd, IPPROTO_IP, IP_TTL);
}
} catch (ErrnoException e) {
Log.e(TAG, "Exception reading TCP state from socket", e);
if (e.errno == ENOPROTOOPT) {
// ENOPROTOOPT may happen in kernel version lower than 4.8.
// Treat it as ERROR_UNSUPPORTED.
throw new InvalidSocketException(ERROR_UNSUPPORTED, e);
} else {
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.
tcpDetails.seq = tcpDetails.seq - 1;
return tcpDetails;
}
/**
* Switch the tcp socket out of repair mode.
*
* @param fd the fd of socket to switch back to normal.
*/
private static void switchOutOfRepairMode(@NonNull final FileDescriptor fd) {
try {
Os.setsockoptInt(fd, IPPROTO_TCP, TCP_REPAIR, TCP_REPAIR_OFF);
} catch (ErrnoException e) {
Log.e(TAG, "Cannot switch socket out of repair mode", e);
// Well, there is not much to do here to recover
}
}
/**
* Start monitoring incoming packets.
*
* @param fd socket fd to monitor.
* @param ki a {@link KeepaliveInfo} that tracks information about a socket keepalive.
* @param slot keepalive slot.
*/
public void startSocketMonitor(@NonNull final FileDescriptor fd,
@NonNull final KeepaliveInfo ki, final int slot)
throws IllegalArgumentException, InvalidSocketException {
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))) {
Log.e(TAG, "This fd is already registered.");
throw new InvalidSocketException(ERROR_INVALID_SOCKET);
}
}
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.
final int reason;
if (0 != (events & EVENT_ERROR)) {
reason = ERROR_INVALID_SOCKET;
} else {
reason = DATA_RECEIVED;
}
ki.onFileDescriptorInitiatedStop(reason);
// 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);
if (DBG) Log.d(TAG, "Moving socket out of repair mode for stop : " + fd);
switchOutOfRepairMode(fd);
}
private static byte [] getAddress(InetSocketAddress inetAddr) {
return inetAddr.getAddress().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);
}
}
}