content: /*
   * 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);
          }
      }
  }
  