Renaming root project name.
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / FlowPatternFinder.java
index 9dba468538fbeee813aae64396f893e2e60ac5ec..c384852b4c1d91d5bb4bd6024c3d9e17ad26d6d7 100644 (file)
@@ -3,6 +3,7 @@ package edu.uci.iotproject;
 import edu.uci.iotproject.comparison.ComparisonFunctions;
 import edu.uci.iotproject.comparison.CompleteMatchPatternComparisonResult;
 import edu.uci.iotproject.comparison.PatternComparisonTask;
+import edu.uci.iotproject.trafficreassembly.layer3.Conversation;
 import org.pcap4j.core.NotOpenException;
 import org.pcap4j.core.PcapHandle;
 import org.pcap4j.core.PcapNativeException;
@@ -11,8 +12,10 @@ import org.pcap4j.packet.DnsPacket;
 import org.pcap4j.packet.IpV4Packet;
 import org.pcap4j.packet.TcpPacket;
 
-import java.io.EOFException;
+import java.io.*;
 import java.net.UnknownHostException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
 import java.util.*;
 import java.util.concurrent.*;
 
@@ -27,7 +30,7 @@ import java.util.concurrent.*;
  * {@link FlowPattern} (in that one end/party of said conversations matches the hostname(s) specified by the given
  * {@code FlowPattern}).
  * These potential matches are then examined on background worker thread(s) to determine if they are indeed a (complete)
- * matches of the provided {@code FlowPattern}.
+ * match of the provided {@code FlowPattern}.
  * </p>
  *
  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
@@ -56,9 +59,23 @@ public class FlowPatternFinder {
      */
     private final Map<Conversation, Conversation> mConversations;
 
+    /**
+     * Holds a list of trigger times.
+     */
+    private final List<Long> mTriggerTimes;
+    private static int triggerListCounter;
+
     private final DnsMap mDnsMap;
     private final PcapHandle mPcap;
     private final FlowPattern mPattern;
+    private final ConversationPair mConvPair;
+    private final String FILE = "./devices/tplink_switch/datapoints.csv";
+    //private final String REF_FILE = "./devices/dlink_switch/dlink-june-26-2018.timestamps";
+    private final String REF_FILE = "./devices/tplink_switch/tplink-june-14-2018.timestamps";
+    //private final String REF_FILE = "./devices/tplink_switch/tplink-feb-13-2018.timestamps";
+    // Router time is in CET and we use PST for the trigger times
+    // Difference is 7 hours x 3600 x 1000ms = 25,200,000ms
+    private final long TIME_OFFSET = 25200000;
 
     private final List<Future<CompleteMatchPatternComparisonResult>> mPendingComparisons = new ArrayList<>();
     /* End instance properties */
@@ -70,18 +87,42 @@ public class FlowPatternFinder {
      */
     public FlowPatternFinder(PcapHandle pcap, FlowPattern pattern) {
         this.mConversations = new HashMap<>();
+        this.mTriggerTimes = readTriggerTimes(REF_FILE);
+        triggerListCounter = 0;
         this.mDnsMap = new DnsMap();
         this.mPcap = Objects.requireNonNull(pcap,
                 String.format("Argument of type '%s' cannot be null", PcapHandle.class.getSimpleName()));
         this.mPattern = Objects.requireNonNull(pattern,
                 String.format("Argument of type '%s' cannot be null", FlowPattern.class.getSimpleName()));
+        this.mConvPair = new ConversationPair(FILE, ConversationPair.Direction.DEVICE_TO_SERVER);
+    }
+
+
+    private List<Long> readTriggerTimes(String refFileName) {
+
+        List<Long> listTriggerTimes = new ArrayList<>();
+        try {
+            File file = new File(refFileName);
+            BufferedReader br = new BufferedReader(new FileReader(file));
+            String s;
+            while ((s = br.readLine()) != null) {
+                listTriggerTimes.add(timeToMillis(s, false));
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        System.out.println("List has: " + listTriggerTimes.size());
+
+        return listTriggerTimes;
     }
 
     /**
      * Starts the pattern search.
      */
     public void start() {
-        findFlowPattern();
+
+        //findFlowPattern();
+        findSignatureBasedOnTimestamp();
     }
 
     /**
@@ -90,7 +131,10 @@ public class FlowPatternFinder {
     private void findFlowPattern() {
         try {
             PcapPacket packet;
-            int patternLength = mPattern.getLength();
+//            TODO: The new comparison method is pending
+//            TODO: For now, just compare using one hostname and one list per FlowPattern
+//            List<String> hostnameList = mPattern.getHostnameList();
+//            int hostIndex = 0;
             while ((packet = mPcap.getNextPacketEx()) != null) {
                 // Let DnsMap handle DNS packets.
                 if (packet.get(DnsPacket.class) != null) {
@@ -99,11 +143,12 @@ public class FlowPatternFinder {
                     continue;
                 }
                 // For now, we only work support pattern search in TCP over IPv4.
-                IpV4Packet ipPacket = packet.get(IpV4Packet.class);
-                TcpPacket tcpPacket = packet.get(TcpPacket.class);
+                final IpV4Packet ipPacket = packet.get(IpV4Packet.class);
+                final TcpPacket tcpPacket = packet.get(TcpPacket.class);
                 if (ipPacket == null || tcpPacket == null) {
                     continue;
                 }
+
                 String srcAddress = ipPacket.getHeader().getSrcAddr().getHostAddress();
                 String dstAddress = ipPacket.getHeader().getDstAddr().getHostAddress();
                 int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
@@ -111,14 +156,14 @@ public class FlowPatternFinder {
                 // Is this packet related to the pattern; i.e. is it going to (or coming from) the cloud server?
                 boolean fromServer = mDnsMap.isRelatedToCloudServer(srcAddress, mPattern.getHostname());
                 boolean fromClient = mDnsMap.isRelatedToCloudServer(dstAddress, mPattern.getHostname());
+//                String currentHostname = hostnameList.get(hostIndex);
+//                boolean fromServer = mDnsMap.isRelatedToCloudServer(srcAddress, currentHostname);
+//                boolean fromClient = mDnsMap.isRelatedToCloudServer(dstAddress, currentHostname);
                 if (!fromServer && !fromClient) {
                     // Packet not related to pattern, skip it.
                     continue;
                 }
-                if (tcpPacket.getPayload() == null) {
-                    // We skip non-payload control packets as these are less predictable
-                    continue;
-                }
+
                 // Conversations (connections/sessions) are identified by the four-tuple
                 // (clientIp, clientPort, serverIp, serverPort) (see Conversation Javadoc).
                 // Create "dummy" conversation for looking up an existing entry.
@@ -126,30 +171,52 @@ public class FlowPatternFinder {
                         new Conversation(dstAddress, dstPort, srcAddress, srcPort);
                 // Add the packet so that the "dummy" conversation can be immediately added to the map if no entry
                 // exists for the conversation that the current packet belongs to.
-                conversation.addPacket(packet, true);
+                if (tcpPacket.getHeader().getFin()) {
+                    // Record FIN packets.
+                    conversation.addFinPacket(packet);
+                }
+                if (tcpPacket.getPayload() != null) {
+                    // Record regular payload packets.
+                    conversation.addPacket(packet, true);
+                }
+                // Note: does not make sense to call attemptAcknowledgementOfFin here as the new packet has no FINs
+                // in its list, so if this packet is an ACK, it would not be added anyway.
+                // Need to retain a final reference to get access to the packet in the lambda below.
+                final PcapPacket finalPacket = packet;
                 // Add the new conversation to the map if an equal entry is not already present.
                 // If an existing entry is already present, the current packet is simply added to that conversation.
                 mConversations.merge(conversation, conversation, (existingEntry, toMerge) -> {
-                    // toMerge only has a single packet, which is the same as referred to by 'packet' variable, but need
-                    // this hack as 'packet' is not final and hence cannot be referred to in a lambda.
-                    existingEntry.addPacket(toMerge.getPackets().get(0), true);
+                    // toMerge may not have any payload packets if the current packet is a FIN packet.
+                    if (toMerge.getPackets().size() > 0) {
+                        existingEntry.addPacket(toMerge.getPackets().get(0), true);
+                    }
+                    if (toMerge.getFinAckPairs().size() > 0) {
+                        // Add the FIN packet to the existing entry.
+                        existingEntry.addFinPacket(toMerge.getFinAckPairs().get(0).getFinPacket());
+                    }
+                    if (finalPacket.get(TcpPacket.class).getHeader().getAck()) {
+                        existingEntry.attemptAcknowledgementOfFin(finalPacket);
+                    }
                     return existingEntry;
                 });
                 // Refresh reference to point to entry in map (in case packet was added to existing entry).
                 conversation = mConversations.get(conversation);
-                if (conversation.getPackets().size() == mPattern.getLength()) {
-                    // Conversation reached a size that matches the expected size.
+                if (conversation.isGracefullyShutdown()) {
+                    // Conversation terminated gracefully, so we can now start analyzing it.
                     // Remove the Conversation from the map and start the analysis.
                     // Any future packets identified by the same four tuple will be tied to a new Conversation instance.
-                    // This might, for example, occur if the same conversation is reused for multiple events.
                     mConversations.remove(conversation);
                     // Create comparison task and send to executor service.
                     PatternComparisonTask<CompleteMatchPatternComparisonResult> comparisonTask =
-                            new PatternComparisonTask<>(conversation, mPattern, ComparisonFunctions.COMPLETE_MATCH);
+                            new PatternComparisonTask<>(conversation, mPattern, ComparisonFunctions.SUB_SEQUENCE_COMPLETE_MATCH);
                     mPendingComparisons.add(EXECUTOR_SERVICE.submit(comparisonTask));
+                    // Increment hostIndex to find the next
+
                 }
             }
         } catch (EOFException eofe) {
+            // TODO should check for leftover conversations in map here and fire tasks for those.
+            // TODO [cont'd] such tasks may be present if connections did not terminate gracefully or if there are longlived connections.
             System.out.println("[ findFlowPattern ] Finished processing entire PCAP stream!");
             System.out.println("[ findFlowPattern ] Now waiting for comparisons to finish...");
             // Wait for all comparisons to finish, then output their results to std.out.
@@ -172,45 +239,119 @@ public class FlowPatternFinder {
         }
     }
 
-//    private static class PatternComparisonTask implements Runnable {
-//
-//        private final Conversation mConversation;
-//        private final FlowPattern mFlowPattern;
-//
-//        private PatternComparisonTask(Conversation conversation, FlowPattern pattern) {
-//            this.mConversation = conversation;
-//            this.mFlowPattern = pattern;
-//        }
-//
-//        @Override
-//        public void run() {
-//            if(isCompleteMatch()) {
-//                PcapPacket firstPacketInFlow = mConversation.getPackets().get(0);
-//                System.out.println(
-//                        String.format("[ find ] Detected a complete match of pattern '%s' at %s!",
-//                                mFlowPattern.getPatternId(), firstPacketInFlow.getTimestamp().toString()));
-//            }
-//        }
-//
-//        /**
-//         * Compares the order of packet lengths present in {@link #mConversation} with those found in
-//         * {@link #mFlowPattern}.
-//         * @return {@code true} if the packet lengths matches pairwise for all indices, {@code false} otherwise.
-//         */
-//        private boolean isCompleteMatch() {
-//            List<PcapPacket> convPackets = mConversation.getPackets();
-//            if (convPackets.size() != mFlowPattern.getLength()) {
-//                return false;
-//            }
-//            for (int i = 0; i < convPackets.size(); i++) {
-//                TcpPacket tcpPacket = convPackets.get(i).get(TcpPacket.class);
-//                if (tcpPacket.getPayload().length() != mFlowPattern.getPacketOrder().get(i)) {
-//                    return false;
-//                }
-//            }
-//            return true;
-//        }
-//
-//    }
+    /**
+     * Find patterns based on the FlowPattern object (run by a thread)
+     */
+    private void findSignatureBasedOnTimestamp() {
+        try {
+            PcapPacket packet;
+//            TODO: The new comparison method is pending
+//            TODO: For now, just compare using one hostname and one list per FlowPattern
+            while ((packet = mPcap.getNextPacketEx()) != null) {
+                // Let DnsMap handle DNS packets.
+                if (packet.get(DnsPacket.class) != null) {
+                    // Check if this is a valid DNS packet
+                    mDnsMap.validateAndAddNewEntry(packet);
+                    continue;
+                }
+                // For now, we only work support pattern search in TCP over IPv4.
+                final IpV4Packet ipPacket = packet.get(IpV4Packet.class);
+                final TcpPacket tcpPacket = packet.get(TcpPacket.class);
+                if (ipPacket == null || tcpPacket == null) {
+                    continue;
+                }
+
+                String srcAddress = ipPacket.getHeader().getSrcAddr().getHostAddress();
+                String dstAddress = ipPacket.getHeader().getDstAddr().getHostAddress();
+                int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
+                int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
+                //System.out.println("Timestamp packet: " + packet.getTimestamp());
+                // Is this packet related to the pattern; i.e. is it going to (or coming from) the cloud server?
+                boolean fromServer = mDnsMap.isRelatedToCloudServer(srcAddress, mPattern.getHostname());
+                boolean fromClient = mDnsMap.isRelatedToCloudServer(dstAddress, mPattern.getHostname());
+                if (!fromServer && !fromClient) {
+                    // Packet not related to pattern, skip it.
+                    continue;
+                }
+                // Record the conversation pairs
+                if (tcpPacket.getPayload() != null && checkTimeStamp(packet)) {
+                //if (tcpPacket.getPayload() != null) {
+                    mConvPair.writeConversationPair(packet, fromClient, fromServer);
+                }
+            }
+        } catch (EOFException eofe) {
+            triggerListCounter = 0;
+            mConvPair.close();
+            System.out.println("[ findFlowPattern ] ConversationPair writer closed!");
+            System.out.println("[ findFlowPattern ] Frequencies of data points:");
+            mConvPair.printListFrequency();
+        } catch (UnknownHostException |
+                PcapNativeException  |
+                NotOpenException     |
+                TimeoutException ex) {
+            ex.printStackTrace();
+        }
+    }
+
+    private boolean checkTimeStamp(PcapPacket packet) {
+
+        // Extract time from the packet's timestamp
+        String timeStamp = packet.getTimestamp().toString();
+        String timeString = timeStamp.substring(timeStamp.indexOf("T") + 1, timeStamp.indexOf("."));
+        // Timestamps are in CET (ahead of PST) so it should be deducted by TIME_OFFSET
+        long time = timeToMillis(timeString, true) - TIME_OFFSET;
+        //long time = timeToMillis(timeString, true);
+
+        //System.out.println("Gets here: " + time + " trigger time: " + mTriggerTimes.get(triggerListCounter));
+
+        // We accept packets that are at most 3 seconds away from the trigger time
+        if ((mTriggerTimes.get(triggerListCounter) <= time) &&
+                (time <= mTriggerTimes.get(triggerListCounter) + 3000)) {
+            //System.out.println("Gets here 1: " + timeString + " index: " + triggerListCounter);
+            return true;
+        } else {
+            // Handle the case that the timestamp is > 3000, but < next timestamp
+            // in the list. We ignore these packets.
+            if (time < mTriggerTimes.get(triggerListCounter)) {
+                // Timestamp is smaller than trigger, ignore!
+                //System.out.println("Gets here 2: " + timeString + " index: " + triggerListCounter);
+                return false;
+            } else { // Timestamp is greater than trigger, increment!
+                triggerListCounter = triggerListCounter + 1;
+                //System.out.println("Gets here 3: " + timeString + " index: " + triggerListCounter);
+                //return false;
+                return checkTimeStamp(packet);
+            }
+        }
+
+        //System.out.println("Timestamp: " + timeToMillis(time, true));
+        //String time2 = "21:38:08";
+        //System.out.println("Timestamp: " + timeToMillis(time2, true));
+    }
+
+    /**
+     * A private function that returns time in milliseconds.
+     * @param time The time in the form of String.
+     * @param is24Hr If true, then this is in 24-hour format.
+     */
+    private long timeToMillis(String time, boolean is24Hr) {
+
+        String format = null;
+        if (is24Hr) {
+            format = "hh:mm:ss";
+        } else { // 12 Hr format
+            format = "hh:mm:ss aa";
+        }
+        DateFormat sdf = new SimpleDateFormat(format);
+        Date date = null;
+        try {
+            date = sdf.parse(time);
+        } catch(Exception e) {
+            e.printStackTrace();
+        }
+        if (date == null)
+            return 0;
+        return date.getTime();
+    }
 
 }