add method for checking if a Conversation has been gracefully shut down.
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / Conversation.java
index 6a24195..f28b8fd 100644 (file)
@@ -1,13 +1,11 @@
 package edu.uci.iotproject;
 
+import edu.uci.iotproject.util.PcapPacketUtils;
 import org.pcap4j.core.PcapPacket;
 import org.pcap4j.packet.IpV4Packet;
 import org.pcap4j.packet.TcpPacket;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
 
 /**
  * Models a (TCP) conversation/connection/session/flow (packet's belonging to the same session between a client and a
@@ -16,15 +14,51 @@ import java.util.Objects;
  * considered when determining equality of two {@code Conversation} instances in order to allow for a
  * {@code Conversation} to function as a key in data structures such as {@link java.util.Map} and {@link java.util.Set}.
  * See {@link #equals(Object)} for the definition of equality.
+ *
+ * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
+ * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
  */
 public class Conversation {
 
+    /* Begin instance properties */
+    /**
+     * The IP of the host that is considered the client (i.e. the host that initiates the conversation)
+     * in this conversation.
+     */
     private final String mClientIp;
+
+    /**
+     * The port number used by the host that is considered the client in this conversation.
+     */
     private final int mClientPort;
+
+    /**
+     * The IP of the host that is considered the server (i.e. is the responder) in this conversation.
+     */
     private final String mServerIp;
+
+    /**
+     * The port number used by the server in this conversation.
+     */
     private final int mServerPort;
+
+    /**
+     * The list of packets pertaining to this conversation.
+     */
     private final List<PcapPacket> mPackets;
 
+    /**
+     * Contains the sequence numbers seen so far for this {@code Conversation}.
+     * Used for filtering out retransmissions.
+     */
+    private final Set<Integer> mSeqNumbers;
+
+    /**
+     * List of pairs FINs and their corresponding ACKs associated with this conversation.
+     */
+    private List<FinAckPair> mFinPackets;
+    /* End instance properties */
+
     /**
      * Constructs a new {@code Conversation}.
      * @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation)
@@ -39,43 +73,32 @@ public class Conversation {
         this.mServerIp = serverIp;
         this.mServerPort = serverPort;
         this.mPackets = new ArrayList<>();
+        this.mSeqNumbers = new HashSet<>();
+        this.mFinPackets = new ArrayList<>();
     }
 
     /**
      * Add a packet to the list of packets associated with this conversation.
      * @param packet The packet that is to be added to (associated with) this conversation.
+     * @param ignoreRetransmissions Boolean value indicating if retransmissions should be ignored.
+     *                              If set to {@code true}, {@code packet} will <em>not</em> be added to the
+     *                              internal list of packets pertaining to this {@code Conversation}
+     *                              <em>iff</em> the sequence number of {@code packet} was already
+     *                              seen in a previous packet.
      */
-    public void addPacket(PcapPacket packet) {
-        // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
-        // defines the conversation.
-        // ==== Precondition: verify that packet does indeed pertain to conversation. ====
-        IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
+    public void addPacket(PcapPacket packet, boolean ignoreRetransmissions) {
+        // Precondition: verify that packet does indeed pertain to conversation.
+        onAddPrecondition(packet);
         // For now we only support TCP flows.
         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
-        String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
-        String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
-        int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
-        int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
-        String clientIp, serverIp;
-        int clientPort, serverPort;
-        if (ipSrc.equals(mClientIp)) {
-            clientIp = ipSrc;
-            clientPort = srcPort;
-            serverIp = ipDst;
-            serverPort = dstPort;
-        } else {
-            clientIp = ipDst;
-            clientPort = dstPort;
-            serverIp = ipSrc;
-            serverPort = srcPort;
-        }
-        if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
-                serverIp.equals(mServerIp) && serverPort == mServerPort)) {
-            throw new IllegalArgumentException(
-                    String.format("Attempt to add packet that does not pertain to %s",
-                            Conversation.class.getSimpleName()));
+        int seqNo = tcpPacket.getHeader().getSequenceNumber();
+        if (ignoreRetransmissions && mSeqNumbers.contains(seqNo)) {
+            // Packet is a retransmission. Ignore it.
+            return;
         }
-        // ================================================================================
+        // Update set of sequence numbers seen so far with sequence number of new packet.
+        mSeqNumbers.add(seqNo);
+        // Finally add packet to list of packets pertaining to this conversation.
         mPackets.add(packet);
     }
 
@@ -89,6 +112,39 @@ public class Conversation {
         return Collections.unmodifiableList(mPackets);
     }
 
+    /**
+     * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
+     * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
+     */
+    public void addFinPacket(PcapPacket finPacket) {
+        // Precondition: verify that packet does indeed pertain to conversation.
+        onAddPrecondition(finPacket);
+        mFinPackets.add(new FinAckPair(finPacket));
+    }
+
+    /**
+     * Attempt to ACK any FIN packets held by this conversation.
+     * @param ackPacket The ACK for a FIN previously added to this conversation.
+     */
+    public void attemptAcknowledgementOfFin(PcapPacket ackPacket) {
+        // Precondition: verify that the packet pertains to this conversation.
+        onAddPrecondition(ackPacket);
+        // Mark unack'ed FIN(s) that this ACK matches as ACK'ed (there might be more than one in case of retransmissions..?)
+        mFinPackets.replaceAll(finAckPair -> (!finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket)) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
+    }
+
+    /**
+     * Get if this {@code Conversation} is considered to have been gracefully shut down.
+     * A {@code Conversation} has been gracefully shut down if it contains a FIN+ACK pair for both directions
+     * (client to server, and server to client).
+     * @return {@code true} if the connection has been gracefully shut down, false otherwise.
+     */
+    public boolean isGracefullyShutdown() {
+        //  The conversation has been gracefully shut down if we have recorded a FIN from both the client and the server which have both been ack'ed.
+        return mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mClientIp, mClientPort)) &&
+                mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
+    }
+
     // =========================================================================================================
     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
     // in a Map.
@@ -115,4 +171,41 @@ public class Conversation {
     public String toString() {
         return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
     }
+
+    /**
+     * Invoke to verify that the precondition holds when a caller attempts to add a packet to this {@code Conversation}.
+     * An {@link IllegalArgumentException} is thrown if the precondition is violated.
+     * @param packet the packet to be added to this {@code Conversation}
+     */
+    private void onAddPrecondition(PcapPacket packet) {
+        // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
+        // defines the conversation.
+        IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
+        // For now we only support TCP flows.
+        TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
+        String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
+        String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
+        int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
+        int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
+        String clientIp, serverIp;
+        int clientPort, serverPort;
+        if (ipSrc.equals(mClientIp)) {
+            clientIp = ipSrc;
+            clientPort = srcPort;
+            serverIp = ipDst;
+            serverPort = dstPort;
+        } else {
+            clientIp = ipDst;
+            clientPort = dstPort;
+            serverIp = ipSrc;
+            serverPort = srcPort;
+        }
+        if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
+                serverIp.equals(mServerIp) && serverPort == mServerPort)) {
+            throw new IllegalArgumentException(
+                    String.format("Attempt to add packet that does not pertain to %s",
+                            Conversation.class.getSimpleName()));
+        }
+    }
+
 }
\ No newline at end of file