Temporary fix for router's IP as client for WAN trace and paths for new tests for...
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / Conversation.java
index f28b8fd2f3167aa64f91957b9ca766f804c09c0a..490dc4f53264ee40f9b3e7c3d995d9482902cbd0 100644 (file)
@@ -1,8 +1,10 @@
 package edu.uci.iotproject;
 
+import edu.uci.iotproject.analysis.TcpConversationUtils;
 import edu.uci.iotproject.util.PcapPacketUtils;
 import org.pcap4j.core.PcapPacket;
 import org.pcap4j.packet.IpV4Packet;
+import org.pcap4j.packet.Packet;
 import org.pcap4j.packet.TcpPacket;
 
 import java.util.*;
@@ -43,22 +45,74 @@ public class Conversation {
     private final int mServerPort;
 
     /**
-     * The list of packets pertaining to this conversation.
+     * The list of packets (with payload) pertaining to this conversation.
      */
     private final List<PcapPacket> mPackets;
 
     /**
-     * Contains the sequence numbers seen so far for this {@code Conversation}.
+     * If {@link #isTls()} is {@code true}, this list contains the subset of {@link #mPackets} which are TLS Application
+     * Data packets.
+     */
+    private final List<PcapPacket> mTlsApplicationDataPackets;
+
+    /**
+     * Contains the sequence numbers used thus far by the host that is considered the <em>client</em> in this
+     * {@code Conversation}.
+     * Used for filtering out retransmissions.
+     */
+    private final Set<Integer> mSeqNumbersClient;
+
+    /**
+     * Contains the sequence numbers used thus far by the host that is considered the <em>server</em> in this
+     * {@code Conversation}.
      * Used for filtering out retransmissions.
      */
-    private final Set<Integer> mSeqNumbers;
+    private final Set<Integer> mSeqNumbersSrv;
+
+    /**
+     * List of SYN packets pertaining to this conversation.
+     */
+    private final List<PcapPacket> mSynPackets;
 
     /**
      * List of pairs FINs and their corresponding ACKs associated with this conversation.
      */
-    private List<FinAckPair> mFinPackets;
+    private final List<FinAckPair> mFinPackets;
+
+    /**
+     * List of RST packets associated with this conversation.
+     */
+    private final List<PcapPacket> mRstPackets;
+
+    /**
+     * Boolean to mark the packet as Application Data based on the previous packet that reaches MTU
+     */
+    private boolean mApplicationData;
     /* End instance properties */
 
+    /**
+     * Factory method for creating a {@code Conversation} from a {@link PcapPacket}.
+     * @param pcapPacket The {@code PcapPacket} that wraps a TCP segment for which a {@code Conversation} is to be initiated.
+     * @param clientIsSrc If {@code true}, the source address and source port found in the IP datagram and TCP segment
+     *                    wrapped in the {@code PcapPacket} are regarded as pertaining to the client, and the destination
+     *                    address and destination port are regarded as pertaining to the server---and vice versa if set
+     *                    to {@code false}.
+     * @return A {@code Conversation} initiated with ip:port for client and server according to the direction of the packet.
+     */
+    public static Conversation fromPcapPacket(PcapPacket pcapPacket, boolean clientIsSrc) {
+        IpV4Packet ipPacket = pcapPacket.get(IpV4Packet.class);
+        TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
+        String clientIp = clientIsSrc ? ipPacket.getHeader().getSrcAddr().getHostAddress() :
+                ipPacket.getHeader().getDstAddr().getHostAddress();
+        String srvIp = clientIsSrc ? ipPacket.getHeader().getDstAddr().getHostAddress() :
+                ipPacket.getHeader().getSrcAddr().getHostAddress();
+        int clientPort = clientIsSrc ? tcpPacket.getHeader().getSrcPort().valueAsInt() :
+                tcpPacket.getHeader().getDstPort().valueAsInt();
+        int srvPort = clientIsSrc ? tcpPacket.getHeader().getDstPort().valueAsInt() :
+                tcpPacket.getHeader().getSrcPort().valueAsInt();
+        return new Conversation(clientIp, clientPort, srvIp, srvPort);
+    }
+
     /**
      * 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)
@@ -73,8 +127,13 @@ public class Conversation {
         this.mServerIp = serverIp;
         this.mServerPort = serverPort;
         this.mPackets = new ArrayList<>();
-        this.mSeqNumbers = new HashSet<>();
+        this.mTlsApplicationDataPackets = new ArrayList<>();
+        this.mSeqNumbersClient = new HashSet<>();
+        this.mSeqNumbersSrv = new HashSet<>();
+        this.mSynPackets = new ArrayList<>();
         this.mFinPackets = new ArrayList<>();
+        this.mRstPackets = new ArrayList<>();
+        this.mApplicationData = false;
     }
 
     /**
@@ -89,17 +148,58 @@ public class Conversation {
     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));
-        int seqNo = tcpPacket.getHeader().getSequenceNumber();
-        if (ignoreRetransmissions && mSeqNumbers.contains(seqNo)) {
+        if (ignoreRetransmissions && isRetransmission(packet)) {
             // Packet is a retransmission. Ignore it.
             return;
         }
-        // Update set of sequence numbers seen so far with sequence number of new packet.
-        mSeqNumbers.add(seqNo);
+        // Select direction-dependent set of sequence numbers seen so far and update it with sequence number of new packet.
+        addSeqNumber(packet);
         // Finally add packet to list of packets pertaining to this conversation.
         mPackets.add(packet);
+        // Preserve order of packets in list: sort according to timestamp.
+        if (mPackets.size() > 1 &&
+                mPackets.get(mPackets.size()-1).getTimestamp().isBefore(mPackets.get(mPackets.size()-2).getTimestamp())) {
+            Collections.sort(mPackets, (o1, o2) -> {
+                if (o1.getTimestamp().isBefore(o2.getTimestamp())) { return -1; }
+                else if (o2.getTimestamp().isBefore(o1.getTimestamp())) { return 1; }
+                else { return 0; }
+            });
+        }
+        // If TLS, inspect packet to see if it's a TLS Application Data packet, and if so add it to the list of TLS
+        // Application Data packets.
+        if (isTls()) {
+            TcpPacket tcpPacket = packet.get(TcpPacket.class);
+            Packet tcpPayload = tcpPacket.getPayload();
+            if (tcpPayload == null) {
+                return;
+            }
+            byte[] rawPayload = tcpPayload.getRawData();
+            // The SSL record header is at the front of the payload and is 5 bytes long.
+            // The SSL record header type field (the first byte) is set to 23 if it is an Application Data packet.
+            if (rawPayload != null && rawPayload.length >= 5) {
+                if (rawPayload[0] == 23) {
+                    mTlsApplicationDataPackets.add(packet);
+                    // Consider the following packet a data packet if this packet's size == MTU size 1448
+                    if (rawPayload.length >= 1448)
+                        mApplicationData = true;
+                } else if (rawPayload[0] == 20) {
+                    // Do nothing for now - CHANGE_CIPHER_SPEC
+                } else if (rawPayload[0] == 21) {
+                    // Do nothing for now - ALERT
+                } else if (rawPayload[0] == 22) {
+                    // Do nothing for now - HANDSHAKE
+                } else {
+                    // If it is TLS with payload, but rawPayload[0] != 23
+                    if (mApplicationData == true) {
+                        // It is a continuation of the previous packet if the previous packet reaches MTU size 1448 and
+                        // it is not either type 20, 21, or 22
+                        mTlsApplicationDataPackets.add(packet);
+                        if (rawPayload.length < 1448)
+                            mApplicationData = false;
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -112,6 +212,88 @@ public class Conversation {
         return Collections.unmodifiableList(mPackets);
     }
 
+    /**
+     * Records a TCP SYN packet as pertaining to this conversation (adds it to the the internal list).
+     * Attempts to add duplicate SYN packets will be ignored, and the caller is made aware of the attempt to add a
+     * duplicate by the return value being {@code false}.
+     *
+     * @param synPacket A {@link PcapPacket} wrapping a TCP SYN packet.
+     * @return {@code true} if the packet was successfully added to this {@code Conversation}, {@code false} otherwise.
+     */
+    public boolean addSynPacket(PcapPacket synPacket) {
+        onAddPrecondition(synPacket);
+        final IpV4Packet synPacketIpSection = synPacket.get(IpV4Packet.class);
+        final TcpPacket synPacketTcpSection = synPacket.get(TcpPacket.class);
+        if (synPacketTcpSection == null || !synPacketTcpSection.getHeader().getSyn()) {
+            throw new IllegalArgumentException("Not a SYN packet.");
+        }
+        // We are only interested in recording one copy of the two SYN packets (one SYN packet in each direction), i.e.,
+        // we want to discard retransmitted SYN packets.
+        if (mSynPackets.size() >= 2) {
+            return false;
+        }
+        // Check the set of recorded SYN packets to see if we have already recorded a SYN packet going in the same
+        // direction as the packet given in the argument.
+        boolean matchingPrevSyn = mSynPackets.stream().anyMatch(p -> {
+            IpV4Packet pIp = p.get(IpV4Packet.class);
+            TcpPacket pTcp = p.get(TcpPacket.class);
+            boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
+                    equals(pIp.getHeader().getSrcAddr().getHostAddress());
+            boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
+                    equals(pIp.getHeader().getDstAddr().getHostAddress());
+            boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
+                    pTcp.getHeader().getSrcPort().valueAsInt();
+            boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().valueAsInt() ==
+                    pTcp.getHeader().getDstPort().valueAsInt();
+            return srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
+        });
+        if (matchingPrevSyn) {
+            return false;
+        }
+        // Update direction-dependent set of sequence numbers and record/log packet.
+        addSeqNumber(synPacket);
+        return mSynPackets.add(synPacket);
+
+        /*
+        mSynPackets.stream().anyMatch(p -> {
+            IpV4Packet pIp = p.get(IpV4Packet.class);
+            TcpPacket pTcp = p.get(TcpPacket.class);
+            boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
+                    equals(pIp.getHeader().getSrcAddr().getHostAddress());
+            boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
+                    equals(pIp.getHeader().getDstAddr().getHostAddress());
+            boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
+                    pTcp.getHeader().getSrcPort().valueAsInt();
+            boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().value() ==
+                    pTcp.getHeader().getDstPort().value();
+
+            boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
+
+            boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() ==
+                    pTcp.getHeader().getSequenceNumber();
+
+            if (fourTupleMatch && !seqNoMatch) {
+                // If the four tuple that identifies the conversation matches, but the sequence number is different,
+                // it means that this SYN packet is, in fact, an attempt to establish a **new** connection, and hence
+                // the given packet is NOT part of this conversation, even though the ip:port combinations are (by
+                // chance) selected such that they match this conversation.
+                throw new IllegalArgumentException("Attempt to add SYN packet that belongs to a different conversation " +
+                        "(which is identified by the same four tuple as this conversation)");
+            }
+            return fourTupleMatch && seqNoMatch;
+        });
+        */
+    }
+
+    /**
+     * Get a list of SYN packets pertaining to this {@code Conversation}.
+     * The returned list is a read-only list.
+     * @return the list of SYN packets pertaining to this {@code Conversation}.
+     */
+    public List<PcapPacket> getSynPackets() {
+        return Collections.unmodifiableList(mSynPackets);
+    }
+
     /**
      * 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.
@@ -119,6 +301,8 @@ public class Conversation {
     public void addFinPacket(PcapPacket finPacket) {
         // Precondition: verify that packet does indeed pertain to conversation.
         onAddPrecondition(finPacket);
+        // TODO: should call addSeqNumber here?
+        addSeqNumber(finPacket);
         mFinPackets.add(new FinAckPair(finPacket));
     }
 
@@ -130,7 +314,15 @@ public class Conversation {
         // 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);
+        mFinPackets.replaceAll(finAckPair -> !finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
+    }
+
+    /**
+     * Retrieves an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
+     * @return an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
+     */
+    public List<FinAckPair> getFinAckPairs() {
+        return Collections.unmodifiableList(mFinPackets);
     }
 
     /**
@@ -145,6 +337,37 @@ public class Conversation {
                 mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
     }
 
+    /**
+     * Add a TCP segment for which the RST flag is set to this {@code Conversation}.
+     * @param packet A {@link PcapPacket} wrapping a TCP segment pertaining to this {@code Conversation} for which the
+     *               RST flag is set.
+     */
+    public void addRstPacket(PcapPacket packet) {
+        /*
+         * TODO:
+         * When now also keeping track of RST packets, should we also...?
+         * 1) Prevent later packets from being added once a RST segment has been added?
+         * 2) Extend 'isGracefullyShutdown()' to also consider RST segments, or add another method, 'isShutdown()' that
+         *    both considers FIN/ACK (graceful) as well as RST (abrupt/"ungraceful") shutdown?
+         * 3) Should it be impossible to associate more than one RST segment with each Conversation?
+         */
+        onAddPrecondition(packet);
+        TcpPacket tcpPacket = packet.get(TcpPacket.class);
+        if (tcpPacket == null || !tcpPacket.getHeader().getRst()) {
+            throw new IllegalArgumentException("not a RST packet");
+        }
+        mRstPackets.add(packet);
+    }
+
+    /**
+     * Get the TCP segments pertaining to this {@code Conversation} for which it was detected that the RST flag is set.
+     * @return the TCP segments pertaining to this {@code Conversation} for which it was detected that the RST flag is
+     *         set.
+     */
+    public List<PcapPacket> getRstPackets() {
+        return Collections.unmodifiableList(mRstPackets);
+    }
+
     // =========================================================================================================
     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
     // in a Map.
@@ -208,4 +431,155 @@ public class Conversation {
         }
     }
 
-}
\ No newline at end of file
+    /**
+     * <p>
+     *      Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
+     *      packet.
+     * </p>
+     *
+     * <b>
+     *     TODO:
+     *     the current implementation, which uses a set of previously seen sequence numbers, will consider a segment
+     *     with a reused sequence number---occurring as a result of sequence number wrap around for a very long-lived
+     *     connection---as a retransmission (and may therefore end up discarding it even though it is in fact NOT a
+     *     retransmission). Ideas?
+     * </b>
+     *
+     * @param packet The packet.
+     * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
+     */
+    public boolean isRetransmission(PcapPacket packet) {
+        // Extract sequence number.
+        int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
+        switch (getDirection(packet)) {
+            case CLIENT_TO_SERVER:
+                return mSeqNumbersClient.contains(seqNo);
+            case SERVER_TO_CLIENT:
+                return mSeqNumbersSrv.contains(seqNo);
+            default:
+                throw new AssertionError(String.format("Unexpected value of enum '%s'",
+                        Direction.class.getSimpleName()));
+        }
+    }
+
+    /**
+     * <p>
+     *     Is this {@code Conversation} a TLS session?
+     * </p>
+     *
+     * <em>Note: the current implementation simply examines the port number(s) for 443; it does <b>not</b> verify if the
+     * application data is indeed encrypted.</em>
+     *
+     * @return {@code true} if this {@code Conversation} is interpreted as a TLS session, {@code false} otherwise.
+     */
+    public boolean isTls() {
+        /*
+         * TODO:
+         * - may want to change this to be "return mServerPort == 443 || mClientPort == 443;" in order to also detect
+         *   TLS in those cases where it is not possible to correctly label who is the client and who is the server,
+         *   i.e., when the trace does not contain the SYN/SYNACK exchange.
+         * - current implementation relies on the server using the conventional TLS port number; may instead want to
+         *   inspect the first 4 bytes of each potential TLS packet to see if they match the SSL record header.
+         *
+         * 08/31/18: Added unconvetional TLS ports used by WeMo plugs and LiFX bulb.
+         * 09/20/18: Moved hardcoded ports to other class to allow other classes to query the set of TLS ports.
+         */
+        return TcpConversationUtils.isTlsPort(mServerPort);
+    }
+
+    /**
+     * If this {@code Conversation} is backing a TLS session (i.e., if the value of {@link #isTls()} is {@code true}),
+     * get the packets labeled as TLS Application Data packets. This is a subset of the full set of payload-carrying
+     * packets (as returned by {@link #getPackets()}). An exception is thrown if this method is invoked on a
+     * {@code Conversation} for which {@link #isTls()} returns {@code false}.
+     *
+     * @return A list containing exactly those packets that could be identified as TLS Application Data packets (through
+     *         inspecting of the SSL record header). The list may be empty, if no TLS application data packets have been
+     *         recorded for this {@code Conversation}.
+     */
+    public List<PcapPacket> getTlsApplicationDataPackets() {
+        if (!isTls()) {
+            throw new NoSuchElementException("cannot get TLS Application Data packets for non-TLS TCP conversation");
+        }
+        return Collections.unmodifiableList(mTlsApplicationDataPackets);
+    }
+
+    /**
+     * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by
+     * analyzing the direction of the packet.
+     * @param packet A TCP packet (wrapped in a {@code PcapPacket}) that was added to this conversation and whose
+     *               sequence number is to be recorded as seen.
+     */
+    private void addSeqNumber(PcapPacket packet) {
+        // Note: below check is redundant if client code is correct as the call to check the precondition should already
+        // have been made by the addXPacket method that invokes this method. As such, the call below may be removed in
+        // favor of speed, but the improvement will be minor, hence the added safety may be worth it.
+        onAddPrecondition(packet);
+        // Extract sequence number.
+        int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
+        // Determine direction of packet and add packet's sequence number to corresponding set of sequence numbers.
+        switch (getDirection(packet)) {
+            case CLIENT_TO_SERVER:
+                // Client to server packet.
+                mSeqNumbersClient.add(seqNo);
+                break;
+            case SERVER_TO_CLIENT:
+                // Server to client packet.
+                mSeqNumbersSrv.add(seqNo);
+                break;
+            default:
+                throw new AssertionError(String.format("Unexpected value of enum '%s'",
+                        Direction.class.getSimpleName()));
+        }
+    }
+
+    /**
+     * Determine the direction of {@code packet}. An {@link IllegalArgumentException} is thrown if {@code packet} does
+     * not pertain to this conversation.
+     *
+     * @param packet The packet whose direction is to be determined.
+     * @return A {@link Direction} indicating the direction of the packet.
+     */
+    public Direction getDirection(PcapPacket packet) {
+        IpV4Packet ipPacket = packet.get(IpV4Packet.class);
+        String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
+        String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
+        // Determine direction of packet.
+        if (ipSrc.equals(mClientIp) && ipDst.equals(mServerIp)) {
+            // Client to server packet.
+            return Direction.CLIENT_TO_SERVER;
+        } else if (ipSrc.equals(mServerIp) && ipDst.equals(mClientIp)) {
+            // Server to client packet.
+            return Direction.SERVER_TO_CLIENT;
+        } else {
+            throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
+        }
+    }
+
+    /**
+     * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
+     */
+    public enum Direction {
+
+        CLIENT_TO_SERVER {
+            @Override
+            public String toCompactString() {
+                return "*";
+            }
+        },
+        SERVER_TO_CLIENT {
+            @Override
+            public String toCompactString() {
+                return "";
+            }
+        };
+
+        /**
+         * Get a compact string representation of this {@code Direction}.
+         * @return a compact string representation of this {@code Direction}.
+         */
+        abstract public String toCompactString();
+
+    }
+
+}