Merge branch 'master' of https://github.uci.edu/rtrimana/smart_home_traffic
authorrtrimana <rtrimana@uci.edu>
Mon, 14 Jan 2019 18:03:15 +0000 (10:03 -0800)
committerrtrimana <rtrimana@uci.edu>
Mon, 14 Jan 2019 18:03:15 +0000 (10:03 -0800)
1  2 
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/Main.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/detection/layer3/Layer3ClusterMatcher.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/detection/layer3/SignatureDetector.java
Code/Projects/SmartPlugDetector/src/main/java/edu/uci/iotproject/util/PcapPacketUtils.java

index 5b184d2857780a28ad04cf437b59880d25689425,a7e9a6c7b64e9f13e8fdeca43c6b51f18773d05c..81eb3b0b9a96c833ae2db2870a4cca21316938da
@@@ -3,10 -3,9 +3,9 @@@ package edu.uci.iotproject
  import static edu.uci.iotproject.analysis.UserAction.Type;
  
  import edu.uci.iotproject.analysis.*;
- import edu.uci.iotproject.comparison.seqalignment.ExtractedSequence;
- import edu.uci.iotproject.comparison.seqalignment.SequenceAlignment;
- import edu.uci.iotproject.comparison.seqalignment.SequenceExtraction;
  import edu.uci.iotproject.io.TriggerTimesFileReader;
+ import edu.uci.iotproject.trafficreassembly.layer3.Conversation;
+ import edu.uci.iotproject.trafficreassembly.layer3.TcpReassembler;
  import edu.uci.iotproject.util.PcapPacketUtils;
  import edu.uci.iotproject.util.PrintUtils;
  import org.apache.commons.math3.stat.clustering.Cluster;
@@@ -15,8 -14,6 +14,6 @@@ import org.pcap4j.core.*
  import org.pcap4j.packet.namednumber.DataLinkType;
  
  import java.io.EOFException;
- import java.io.File;
- import java.io.PrintWriter;
  import java.net.UnknownHostException;
  import java.time.Instant;
  import java.util.*;
@@@ -233,11 -230,10 +230,11 @@@ public class Main 
  //        final String outputPcapFile = path + "/2018-10/blossom-sprinkler/blossom-sprinkler-processed.pcap";
  //        final String triggerTimesFile = path + "/2018-10/blossom-sprinkler/blossom-sprinkler-nov-2-2018.timestamps";
  //        final String deviceIp = "192.168.1.229"; // .246 == phone; .229 == sprinkler
 -        // January 9
 +        // January 9, 11
          final String inputPcapFile = path + "/experimental_result/standalone/blossom-sprinkler/wlan1/blossom-sprinkler.wlan1.local.pcap";
          final String outputPcapFile = path + "/experimental_result/standalone/blossom-sprinkler/wlan1/blossom-sprinkler-processed.pcap";
 -        final String triggerTimesFile = path + "/experimental_result/standalone/blossom-sprinkler/timestamps/blossom-sprinkler-standalone-jan-9-2018.timestamps";
 +        final String triggerTimesFile = path + "/experimental_result/standalone/blossom-sprinkler/timestamps/blossom-sprinkler-standalone-jan-9-2019.timestamps";
 +//        final String triggerTimesFile = path + "/experimental_result/standalone/blossom-sprinkler/timestamps/blossom-sprinkler-standalone-jan-11-2019.timestamps";
          final String deviceIp = "192.168.1.246"; // .246 == phone; .229 == sprinkler
  
  //        // 13) DLink siren August 14 experiment
          PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 0);
          PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 1);
          // TODO: Need to remove sequence numbers 0 for Blossom device side since it is not a good signature!
 -        //PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 0);
 -        //PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 0);
 +//        PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 0);
 +//        PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 0);
          // TODO: Need to remove sequence number 2 for ST plug since it is not a good signature!
          //PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, 2);
          // TODO: Need to remove sequence number 0 for Arlo Camera since it is not a good signature!
          }
          // TODO: Merging test
          ppListOfListListOff = PcapPacketUtils.mergeSignatures(ppListOfListListOff, sortedAllConversation);
 -        //ppListOfListListOff = PcapPacketUtils.sortSignatures(ppListOfListListOff);
 +        ppListOfListListOff = PcapPacketUtils.sortSignatures(ppListOfListListOff);
          // TODO: Need to remove sequence numbers 0,2 for Blossom device side since it is not a good signature!
 -        //PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, 0);
 -        //PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, 2);
 +//        PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, 0);
 +//        PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, 0);
          // TODO: Need to remove sequence number 1 for Nest Thermostat since it is not a good signature!
          //PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, 1);
          // TODO: Need to remove sequence number 0 for Arlo Camera since it is not a good signature!
index 0000000000000000000000000000000000000000,b5e30772d3f79893f10ee134786b6fd8e09e9d0e..f0e3bb63b03343aba86f8757b244956f1718bbdc
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,353 +1,353 @@@
 -                throw new IllegalArgumentException("no local IP or router WAN port IP found, can't detect direction");
+ package edu.uci.iotproject.detection.layer3;
+ import edu.uci.iotproject.detection.AbstractClusterMatcher;
+ import edu.uci.iotproject.trafficreassembly.layer3.Conversation;
+ import edu.uci.iotproject.trafficreassembly.layer3.TcpReassembler;
+ import edu.uci.iotproject.analysis.TcpConversationUtils;
+ import edu.uci.iotproject.io.PcapHandleReader;
+ import edu.uci.iotproject.util.PrintUtils;
+ import org.pcap4j.core.*;
+ import java.time.ZoneId;
+ import java.util.*;
+ import java.util.stream.Collectors;
+ import static edu.uci.iotproject.util.PcapPacketUtils.*;
+ /**
+  * Searches a traffic trace for sequences of packets "belong to" a given cluster (in other words, attempts to classify
+  * traffic as pertaining to a given cluster).
+  *
+  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
+  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
+  */
+ public class Layer3ClusterMatcher extends AbstractClusterMatcher implements PacketListener {
+     // Test client
+     public static void main(String[] args) throws PcapNativeException, NotOpenException {
+ //        String path = "/scratch/July-2018"; // Rahmadi
+         String path = "/Users/varmarken/temp/UCI IoT Project/experiments"; // Janus
+         final String inputPcapFile = path + "/2018-07/dlink/dlink.wlan1.local.pcap";
+         final String signatureFile = path + "/2018-07/dlink/offSignature1.sig";
+         List<List<PcapPacket>> signature = PrintUtils.deserializeClustersFromFile(signatureFile);
+         Layer3ClusterMatcher clusterMatcher = new Layer3ClusterMatcher(signature, null,
+                 (sig, match) -> System.out.println(
+                         String.format("[ !!! SIGNATURE DETECTED AT %s !!! ]",
+                                 match.get(0).getTimestamp().atZone(ZoneId.of("America/Los_Angeles")))
+                 )
+         );
+         PcapHandle handle;
+         try {
+             handle = Pcaps.openOffline(inputPcapFile, PcapHandle.TimestampPrecision.NANO);
+         } catch (PcapNativeException pne) {
+             handle = Pcaps.openOffline(inputPcapFile);
+         }
+         PcapHandleReader reader = new PcapHandleReader(handle, p -> true, clusterMatcher);
+         reader.readFromHandle();
+         clusterMatcher.performDetection();
+     }
+     /**
+      * The ordered directions of packets in the sequences that make up {@link #mCluster}.
+      */
+     private final Conversation.Direction[] mClusterMemberDirections;
+     /**
+      * For reassembling the observed traffic into TCP connections.
+      */
+     private final TcpReassembler mTcpReassembler = new TcpReassembler();
+     /**
+      * IP of the router's WAN port (if analyzed traffic is captured at the ISP's point of view).
+      */
+     private final String mRouterWanIp;
+     private final ClusterMatchObserver[] mObservers;
+     /**
+      * Create a {@link Layer3ClusterMatcher}.
+      * @param cluster The cluster that traffic is matched against.
+      * @param routerWanIp The router's WAN IP if examining traffic captured at the ISP's point of view (used for
+      *                    determining the direction of packets).
+      * @param detectionObservers Client code that wants to get notified whenever the {@link Layer3ClusterMatcher} detects that
+      *                          (a subset of) the examined traffic is similar to the traffic that makes up
+      *                          {@code cluster}, i.e., when the examined traffic is classified as pertaining to
+      *                          {@code cluster}.
+      */
+     public Layer3ClusterMatcher(List<List<PcapPacket>> cluster, String routerWanIp, ClusterMatchObserver... detectionObservers) {
+         super(cluster);
+         // ===================== PRECONDITION SECTION =====================
+         mObservers = Objects.requireNonNull(detectionObservers, "detectionObservers cannot be null");
+         if (mObservers.length == 0) {
+             throw new IllegalArgumentException("no detectionObservers provided");
+         }
+         // Build the cluster members' direction sequence.
+         // Note: assumes that the provided cluster was captured within the local network (routerWanIp is set to null).
+         mClusterMemberDirections = getPacketDirections(cluster.get(0), null);
+         /*
+          * Enforce restriction on cluster members: all representatives must exhibit the same direction pattern and
+          * contain the same number of packets. Note that this is a somewhat heavy operation, so it may be disabled later
+          * on in favor of performance. However, it is only run once (at instantiation), so the overhead may be warranted
+          * in order to ensure correctness, especially during the development/debugging phase.
+          */
+         if (mCluster.stream().
+                 anyMatch(inner -> !Arrays.equals(mClusterMemberDirections, getPacketDirections(inner, null)))) {
+             throw new IllegalArgumentException(
+                     "cluster members must contain the same number of packets and exhibit the same packet direction " +
+                             "pattern"
+             );
+         }
+         // ================================================================
+         mRouterWanIp = routerWanIp;
+     }
+     @Override
+     public void gotPacket(PcapPacket packet) {
+         // Present packet to TCP reassembler so that it can be mapped to a connection (if it is a TCP packet).
+         mTcpReassembler.gotPacket(packet);
+     }
+     /**
+      * Get the cluster that describes the packet sequence that this {@link Layer3ClusterMatcher} is searching for.
+      * @return the cluster that describes the packet sequence that this {@link Layer3ClusterMatcher} is searching for.
+      */
+     public List<List<PcapPacket>> getCluster() {
+         return mCluster;
+     }
+     public void performDetection() {
+         /*
+          * Let's start out simple by building a version that only works for signatures that do not span across multiple
+          * TCP conversations...
+          */
+         for (Conversation c : mTcpReassembler.getTcpConversations()) {
+             if (c.isTls() && c.getTlsApplicationDataPackets().isEmpty() || !c.isTls() && c.getPackets().isEmpty()) {
+                 // Skip empty conversations.
+                 continue;
+             }
+             for (List<PcapPacket> signatureSequence : mCluster) {
+                 if (isTlsSequence(signatureSequence) != c.isTls()) {
+                     // We consider it a mismatch if one is a TLS application data sequence and the other is not.
+                     continue;
+                 }
+                 // Fetch set of packets to examine based on TLS or not.
+                 List<PcapPacket> cPkts = c.isTls() ? c.getTlsApplicationDataPackets() : c.getPackets();
+                 /*
+                  * Note: we embed the attempt to detect the signature sequence in a loop in order to capture those cases
+                  * where the same signature sequence appears multiple times in one Conversation.
+                  *
+                  * Note: since we expect all sequences that together make up the signature to exhibit the same direction
+                  * pattern, we can simply pass the precomputed direction array for the signature sequence so that it
+                  * won't have to be recomputed internally in each call to findSubsequenceInSequence().
+                  */
+                 Optional<List<PcapPacket>> match;
+                 while ((match = findSubsequenceInSequence(signatureSequence, cPkts, mClusterMemberDirections, null)).
+                         isPresent()) {
+                     List<PcapPacket> matchSeq = match.get();
+                     // Notify observers about the match.
+                     Arrays.stream(mObservers).forEach(o -> o.onMatch(Layer3ClusterMatcher.this, matchSeq));
+                     /*
+                      * Get the index in cPkts of the last packet in the sequence of packets that matches the searched
+                      * signature sequence.
+                      */
+                     int matchSeqEndIdx = cPkts.indexOf(matchSeq.get(matchSeq.size()-1));
+                     // We restart the search for the signature sequence immediately after that index, so truncate cPkts.
+                     cPkts = cPkts.stream().skip(matchSeqEndIdx + 1).collect(Collectors.toList());
+                 }
+             }
+             /*
+              * TODO:
+              * if no item in cluster matches, also perform a distance-based matching to cover those cases where we did
+              * not manage to capture every single mutation of the sequence during training.
+              *
+              * Need to compute average/centroid of cluster to do so...? Compute within-cluster variance, then check if
+              * distance between input conversation and cluster average/centroid is smaller than or equal to the computed
+              * variance?
+              */
+         }
+     }
+     /**
+      * Checks if {@code sequence} is a sequence of TLS packets. Note: the current implementation relies on inspection
+      * of the port numbers when deciding between TLS vs. non-TLS. Therefore, only the first packet of {@code sequence}
+      * is examined as it is assumed that all packets in {@code sequence} pertain to the same {@link Conversation} and
+      * hence share the same set of two src/dst port numbers (albeit possibly alternating between which one is the src
+      * and which one is the dst, as packets in {@code sequence} may be in alternating directions).
+      * @param sequence The sequence of packets for which it is to be determined if it is a sequence of TLS packets or
+      *                 non-TLS packets.
+      * @return {@code true} if {@code sequence} is a sequence of TLS packets, {@code false} otherwise.
+      */
+     private boolean isTlsSequence(List<PcapPacket> sequence) {
+         // NOTE: Assumes ALL packets in sequence pertain to the same TCP connection!
+         PcapPacket firstPkt = sequence.get(0);
+         int srcPort = getSourcePort(firstPkt);
+         int dstPort = getDestinationPort(firstPkt);
+         return TcpConversationUtils.isTlsPort(srcPort) || TcpConversationUtils.isTlsPort(dstPort);
+     }
+     /**
+      * Examine if a given sequence of packets ({@code sequence}) contains a given shorter sequence of packets
+      * ({@code subsequence}). Note: the current implementation actually searches for a substring as it does not allow
+      * for interleaving packets in {@code sequence} that are not in {@code subsequence}; for example, if
+      * {@code subsequence} consists of packet lengths [2, 3, 5] and {@code sequence} consists of  packet lengths
+      * [2, 3, 4, 5], the result will be that there is no match (because of the interleaving 4). If we are to allow
+      * interleaving packets, we need a modified version of
+      * <a href="https://stackoverflow.com/a/20545604/1214974">this</a>.
+      *
+      * @param subsequence The sequence to search for.
+      * @param sequence The sequence to search.
+      * @param subsequenceDirections The directions of packets in {@code subsequence} such that for all {@code i},
+      *                              {@code subsequenceDirections[i]} is the direction of the packet returned by
+      *                              {@code subsequence.get(i)}. May be set to {@code null}, in which this call will
+      *                              internally compute the packet directions.
+      * @param sequenceDirections The directions of packets in {@code sequence} such that for all {@code i},
+      *                           {@code sequenceDirections[i]} is the direction of the packet returned by
+      *                           {@code sequence.get(i)}. May be set to {@code null}, in which this call will internally
+      *                           compute the packet directions.
+      *
+      * @return An {@link Optional} containing the part of {@code sequence} that matches {@code subsequence}, or an empty
+      *         {@link Optional} if no part of {@code sequence} matches {@code subsequence}.
+      */
+     private Optional<List<PcapPacket>> findSubsequenceInSequence(List<PcapPacket> subsequence,
+                                                                  List<PcapPacket> sequence,
+                                                                  Conversation.Direction[] subsequenceDirections,
+                                                                  Conversation.Direction[] sequenceDirections) {
+         if (sequence.size() < subsequence.size()) {
+             // If subsequence is longer, it cannot be contained in sequence.
+             return Optional.empty();
+         }
+         if (isTlsSequence(subsequence) != isTlsSequence(sequence)) {
+             // We consider it a mismatch if one is a TLS application data sequence and the other is not.
+             return Optional.empty();
+         }
+         // If packet directions have not been precomputed by calling code, we need to construct them.
+         if (subsequenceDirections == null) {
+             subsequenceDirections = getPacketDirections(subsequence, mRouterWanIp);
+         }
+         if (sequenceDirections == null) {
+             sequenceDirections = getPacketDirections(sequence, mRouterWanIp);
+         }
+         int subseqIdx = 0;
+         int seqIdx = 0;
+         while (seqIdx < sequence.size()) {
+             PcapPacket subseqPkt = subsequence.get(subseqIdx);
+             PcapPacket seqPkt = sequence.get(seqIdx);
+             // We only have a match if packet lengths and directions match.
+             if (subseqPkt.getOriginalLength() == seqPkt.getOriginalLength() &&
+                     subsequenceDirections[subseqIdx] == sequenceDirections[seqIdx]) {
+                 // A match; advance both indices to consider next packet in subsequence vs. next packet in sequence.
+                 subseqIdx++;
+                 seqIdx++;
+                 if (subseqIdx == subsequence.size()) {
+                     // We managed to match the entire subsequence in sequence.
+                     // Return the sublist of sequence that matches subsequence.
+                     /*
+                      * TODO:
+                      * ASSUMES THE BACKING LIST (i.e., 'sequence') IS _NOT_ STRUCTURALLY MODIFIED, hence may not work
+                      * for live traces!
+                      */
+                     return Optional.of(sequence.subList(seqIdx - subsequence.size(), seqIdx));
+                 }
+             } else {
+                 // Mismatch.
+                 if (subseqIdx > 0) {
+                     /*
+                      * If we managed to match parts of subsequence, we restart the search for subsequence in sequence at
+                      * the index of sequence where the current mismatch occurred. I.e., we must reset subseqIdx, but
+                      * leave seqIdx untouched.
+                      */
+                     subseqIdx = 0;
+                 } else {
+                     /*
+                      * First packet of subsequence didn't match packet at seqIdx of sequence, so we move forward in
+                      * sequence, i.e., we continue the search for subsequence in sequence starting at index seqIdx+1 of
+                      * sequence.
+                      */
+                     seqIdx++;
+                 }
+             }
+         }
+         return Optional.empty();
+     }
+     /**
+      * Given a cluster, produces a pruned version of that cluster. In the pruned version, there are no duplicate cluster
+      * members. Two cluster members are considered identical if their packets lengths and packet directions are
+      * identical. The resulting pruned cluster is unmodifiable (this applies to both the outermost list as well as the
+      * nested lists) in order to preserve its integrity when exposed to external code (e.g., through
+      * {@link #getCluster()}).
+      *
+      * @param cluster A cluster to prune.
+      * @return The resulting pruned cluster.
+      */
+     @Override
+     protected List<List<PcapPacket>> pruneCluster(List<List<PcapPacket>> cluster) {
+         List<List<PcapPacket>> prunedCluster = new ArrayList<>();
+         for (List<PcapPacket> originalClusterSeq : cluster) {
+             boolean alreadyPresent = false;
+             for (List<PcapPacket> prunedClusterSeq : prunedCluster) {
+                 Optional<List<PcapPacket>> duplicate = findSubsequenceInSequence(originalClusterSeq, prunedClusterSeq,
+                         mClusterMemberDirections, mClusterMemberDirections);
+                 if (duplicate.isPresent()) {
+                     alreadyPresent = true;
+                     break;
+                 }
+             }
+             if (!alreadyPresent) {
+                 prunedCluster.add(Collections.unmodifiableList(originalClusterSeq));
+             }
+         }
+         return Collections.unmodifiableList(prunedCluster);
+     }
+     /**
+      * Given a {@code List<PcapPacket>}, generate a {@code Conversation.Direction[]} such that each entry in the
+      * resulting {@code Conversation.Direction[]} specifies the direction of the {@link PcapPacket} at the corresponding
+      * index in the input list.
+      * @param packets The list of packets for which to construct a corresponding array of packet directions.
+      * @param routerWanIp The IP of the router's WAN port. This is used for determining the direction of packets when
+      *                    the traffic is captured just outside the local network (at the ISP side of the router). Set to
+      *                    {@code null} if {@code packets} stem from traffic captured within the local network.
+      * @return A {@code Conversation.Direction[]} specifying the direction of the {@link PcapPacket} at the
+      *         corresponding index in {@code packets}.
+      */
+     private static Conversation.Direction[] getPacketDirections(List<PcapPacket> packets, String routerWanIp) {
+         Conversation.Direction[] directions = new Conversation.Direction[packets.size()];
+         for (int i = 0; i < packets.size(); i++) {
+             PcapPacket pkt = packets.get(i);
+             if (getSourceIp(pkt).equals(getDestinationIp(pkt))) {
+                 // Sanity check: we shouldn't be processing loopback traffic
+                 throw new AssertionError("loopback traffic detected");
+             }
+             if (isSrcIpLocal(pkt) || getSourceIp(pkt).equals(routerWanIp)) {
+                 directions[i] = Conversation.Direction.CLIENT_TO_SERVER;
+             } else if (isDstIpLocal(pkt) || getDestinationIp(pkt).equals(routerWanIp)) {
+                 directions[i] = Conversation.Direction.SERVER_TO_CLIENT;
+             } else {
++                //throw new IllegalArgumentException("no local IP or router WAN port IP found, can't detect direction");
+             }
+         }
+         return directions;
+     }
+     /**
+      * Interface used by client code to register for receiving a notification whenever the {@link Layer3ClusterMatcher}
+      * detects traffic that is similar to the traffic that makes up the cluster returned by
+      * {@link Layer3ClusterMatcher#getCluster()}.
+      */
+     interface ClusterMatchObserver {
+         /**
+          * Callback that is invoked whenever a sequence that is similar to a sequence associated with the cluster (i.e.,
+          * a sequence is a member of the cluster) is detected in the traffic that the associated {@link Layer3ClusterMatcher}
+          * observes.
+          * @param clusterMatcher The {@link Layer3ClusterMatcher} that detected a match (classified traffic as pertaining to
+          *                       its associated cluster).
+          * @param match The traffic that was deemed to match the cluster associated with {@code clusterMatcher}.
+          */
+         void onMatch(Layer3ClusterMatcher clusterMatcher, List<PcapPacket> match);
+     }
+ }
index 0000000000000000000000000000000000000000,ad4710612e0e382c824c22a54a663d8bb8e06038..c1a5a9c0c2939f66c9b4bb1608796fcb841a722a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,660 +1,660 @@@
 -//        final String inputPcapFile = path + "/experimental_result/smarthome/blossom-sprinkler/eth0/blossom-sprinkler.eth0.detection.pcap";
 -        final String inputPcapFile = path + "/experimental_result/smarthome/blossom-sprinkler/wlan1/blossom-sprinkler.wlan1.detection.pcap";
+ package edu.uci.iotproject.detection.layer3;
+ import edu.uci.iotproject.analysis.TriggerTrafficExtractor;
+ import edu.uci.iotproject.analysis.UserAction;
+ import edu.uci.iotproject.io.PcapHandleReader;
+ import edu.uci.iotproject.util.PrintUtils;
+ import org.jgrapht.GraphPath;
+ import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
+ import org.jgrapht.graph.DefaultWeightedEdge;
+ import org.jgrapht.graph.SimpleDirectedWeightedGraph;
+ import org.pcap4j.core.*;
+ import java.time.Duration;
+ import java.time.ZoneId;
+ import java.time.format.DateTimeFormatter;
+ import java.time.format.FormatStyle;
+ import java.util.*;
+ import java.util.function.Consumer;
+ /**
+  * Detects an event signature that spans one or multiple TCP connections.
+  *
+  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
+  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
+  */
+ public class SignatureDetector implements PacketListener, Layer3ClusterMatcher.ClusterMatchObserver {
+     // Test client
+     public static void main(String[] args) throws PcapNativeException, NotOpenException {
+ //        if (args.length < 3) {
+ //            String errMsg = String.format("Usage: %s inputPcapFile onSignatureFile offSignatureFile",
+ //                    SignatureDetector.class.getSimpleName());
+ //            System.out.println(errMsg);
+ //            return;
+ //        }
+ //        final String inputPcapFile = args[0];
+ //        final String onSignatureFile = args[1];
+ //        final String offSignatureFile = args[2];
+         String path = "/scratch/July-2018"; // Rahmadi
+ //        String path = "/Users/varmarken/temp/UCI IoT Project/experiments"; // Janus
+ //        String path = "/home/jvarmark/iot_project/datasets"; // Hera (server)
+ //        String path = "/raid/varmarken/iot_project/datasets"; // Zeus (server)
+         // No activity test
+         //final String inputPcapFile = path + "/evaluation/no-activity/no-activity.wlan1.pcap";
+         // D-Link Siren experiment
+ //        final String inputPcapFile = path + "/evaluation/dlink-siren/dlink-siren.data.wlan1.pcap";
+ //        final String inputPcapFile = path + "/evaluation/dlink-siren/dlink-siren.eth0.local.pcap";
+         // D-Link Siren DEVICE signatures
+ //        final String onSignatureFile = path + "/2018-08/dlink-siren/onSignature-DLink-Siren-device.sig";
+ //        final String offSignatureFile = path + "/2018-08/dlink-siren/offSignature-DLink-Siren-device.sig";
+         // D-Link Siren PHONE signatures
+ //        final String onSignatureFile = path + "/2018-08/dlink-siren/onSignature-DLink-Siren-phone.sig";
+ //        final String offSignatureFile = path + "/2018-08/dlink-siren/offSignature-DLink-Siren-phone.sig";
+         // TODO: EXPERIMENT - November 19, 2018
+         // Hue Bulb experiment
+ //        final String inputPcapFile = path + "/2018-08/hue-bulb/hue-bulb.wlan1.local.pcap";
+         // Hue Bulb PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/hue-bulb/signatures/hue-bulb-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/hue-bulb/signatures/hue-bulb-offSignature-phone-side.sig";
+         /*
+         // Kwikset Doorlock Sep 12 experiment
+ //        final String inputPcapFile = path + "/evaluation/kwikset-doorlock/kwikset-doorlock.data.wlan1.pcap";
+         final String inputPcapFile = path + "/evaluation/kwikset-doorlock/kwikset-doorlock.data.eth0.pcap";
+ //        // Kwikset Doorlock PHONE signatures
+         final String onSignatureFile = path + "/2018-08/kwikset-doorlock/onSignature-Kwikset-Doorlock-phone-new.sig";
+         final String offSignatureFile = path + "/2018-08/kwikset-doorlock/offSignature-Kwikset-Doorlock-phone-new.sig";
+         */
+         // D-Link Plug experiment
+         //final String inputPcapFile = path + "/evaluation/dlink/dlink-plug.data.wlan1.pcap";
+ //        final String inputPcapFile = path + "/evaluation/dlink/dlink-plug.data.eth0.pcap";
+         // D-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/2018-07/dlink/onSignature-DLink-Plug-device.sig";
+ //        final String offSignatureFile = path + "/2018-07/dlink/offSignature-DLink-Plug-device.sig";
+         // D-Link Plug PHONE signatures
+ //        final String onSignatureFile = path + "/2018-07/dlink/onSignature-DLink-Plug-phone.sig";
+ //        final String offSignatureFile = path + "/2018-07/dlink/offSignature-DLink-Plug-phone.sig";
+         // TODO: The following are negative tests against the PCAP file from UNSW
+ //        final String inputPcapFile = path + "/UNSW/16-10-04.pcap"; // TODO: Seems to be broken! Zero-payload!
+ //        final String inputPcapFile = path + "/UNSW/16-10-12.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-28.pcap"; // TODO: Seems to be broken! Zero-payload!
+ //        final String inputPcapFile = path + "/UNSW/16-10-02.pcap"; // TODO: Seems to be broken!
+ //        final String inputPcapFile = path + "/UNSW/16-10-03.pcap"; // TODO: Seems to be broken!
+ //        final String inputPcapFile = path + "/UNSW/16-10-04-a.pcap"; // TODO: Seems to be broken! Zero-payload!
+ //        final String inputPcapFile = path + "/UNSW/16-10-04-b.pcap"; // TODO: Seems to be broken! Zero-payload!
+ //        final String inputPcapFile = path + "/UNSW/16-10-07.pcap"; // TODO: Seems to be broken!
+ //        final String inputPcapFile = path + "/UNSW/16-10-08.pcap"; // TODO: Seems to be broken!
+ //        final String inputPcapFile = path + "/UNSW/16-10-09.pcap"; // TODO: Seems to be broken!
+ //        final String inputPcapFile = path + "/UNSW/16-10-10.pcap"; // TODO: Seems to be broken!
+ //        final String inputPcapFile = path + "/UNSW/16-10-11.pcap"; // TODO: Seems to be broken!
+         // TODO: The following one is very long!!! - Split into smaller files!
+ //        final String inputPcapFile = path + "/UNSW/16-10-12-a.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-10-12-b.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-10-12-c.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-10-12-d.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-23.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-24.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-25.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-26.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-27.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-09-29.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-10-01.pcap";
+ //        final String inputPcapFile = path + "/UNSW/16-10-06.pcap";
+         // Negative test: dataset from UNB
+ //        final String inputPcapFile = path + "/evaluation/negative-datasets/UNB/Monday-WorkingHours_one-local-endpoint.pcap";
+         // TODO: The following are tests for signatures against training data
+         // D-Link Plug experiment
+ //        final String inputPcapFile = path + "/training/dlink-plug/wlan1/dlink-plug.wlan1.local.pcap";
+         // D-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/training/dlink-plug/signatures/dlink-plug-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/training/dlink-plug/signatures/dlink-plug-offSignature-device-side.sig";
+         // D-Link Plug PHONE signatures
+ //        final String onSignatureFile = path + "/training/dlink-plug/signatures/dlink-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/dlink-plug/signatures/dlink-plug-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - November 7, 2018
+         // D-Link Plug experiment
+         //final String inputPcapFile = path + "/experimental_result/standalone/dlink-plug/wlan1/dlink-plug.wlan1.local.pcap";
+         //final String inputPcapFile = path + "/experimental_result/smarthome/dlink-plug/wlan1/dlink-plug.wlan1.detection.pcap";
+         //final String inputPcapFile = path + "/experimental_result/smarthome/dlink-plug/eth0/dlink-plug.eth0.detection.pcap";
+         // D-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/dlink-plug/signatures/dlink-plug-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/dlink-plug/signatures/dlink-plug-offSignature-device-side.sig";
+         // D-Link Plug PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/dlink-plug/signatures/dlink-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/dlink-plug/signatures/dlink-plug-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - November 9, 2018
+         // D-Link Siren experiment
+         //final String inputPcapFile = path + "/experimental_result/standalone/dlink-siren/wlan1/dlink-siren.wlan1.local.pcap";
+         //final String inputPcapFile = path + "/experimental_result/smarthome/dlink-siren/wlan1/dlink-siren.wlan1.detection.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/dlink-siren/eth0/dlink-siren.eth0.detection.pcap";
+         // D-Link Siren DEVICE signatures
+         // TODO: The device signature does not have pairs---only one packet which is 216, so we don't consider this as a signature
+ //        final String onSignatureFile = path + "/experimental_result/standalone/dlink-siren/signatures/dlink-siren-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/dlink-siren/signatures/dlink-siren-offSignature-device-side.sig";
+         // D-Link Siren PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/dlink-siren/signatures/dlink-siren-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/dlink-siren/signatures/dlink-siren-offSignature-phone-side.sig";
+ //        final String onSignatureFile = path + "/training/signatures/dlink-siren/dlink-siren-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/signatures/dlink-siren/dlink-siren-offSignature-phone-side.sig";
+         // TP-Link Plug experiment
+ ////        final String inputPcapFile = path + "/training/tplink-plug/wlan1/tplink-plug.wlan1.local.pcap";
+ ////        final String inputPcapFile = path + "/experimental_result/wifi-Sniffer/tests2/airtool_2019-01-04_11.08.45.AM.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/wifi-Sniffer/tests2/command-frames-only.pcap";
+ //        // TP-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/training/tplink-plug/signatures/tplink-plug-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/training/tplink-plug/signatures/tplink-plug-offSignature-device-side.sig";
+         // TODO: EXPERIMENT - November 8, 2018
+         // TP-Link Plug experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/tplink-plug/wlan1/tplink-plug.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/standalone/tplink-plug/eth0/tplink-plug.eth0.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/tplink-plug/wlan1/tplink-plug.wlan1.detection.pcap";
+         //final String inputPcapFile = path + "/experimental_result/smarthome/tplink-plug/eth0/tplink-plug.eth0.detection.pcap";
+         // TP-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/tplink-plug/signatures/tplink-plug-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/tplink-plug/signatures/tplink-plug-offSignature-device-side.sig";
+ //        final String onSignatureFile = path + "/experimental_result/standalone/tplink-plug/signatures/tplink-plug-onSignature-device-side-outbound.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/tplink-plug/signatures/tplink-plug-offSignature-device-side-outbound.sig";
+         // TP-Link Plug PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/tplink-plug/signatures/tplink-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/tplink-plug/signatures/tplink-plug-offSignature-phone-side.sig";
+         // Arlo camera experiment
+ //        final String inputPcapFile = path + "/training/arlo-camera/wlan1/arlo-camera.wlan1.local.pcap";
+ ////        // TP-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/training/arlo-camera/signatures/arlo-camera-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/arlo-camera/signatures/arlo-camera-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - November 13, 2018
+         // Arlo Camera experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/arlo-camera/wlan1/arlo-camera.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/standalone/arlo-camera/eth0/arlo-camera.eth0.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/arlo-camera/wlan1/arlo-camera.wlan1.detection.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/arlo-camera/eth0/arlo-camera.eth0.detection.pcap";
+ //        final String inputPcapFile = path + "/training/arlo-camera/eth0/arlo-camera.eth0.local.pcap";
+         // Arlo Camera PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/arlo-camera/signatures/arlo-camera-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/arlo-camera/signatures/arlo-camera-offSignature-phone-side.sig";
+         // Amazon Alexa experiment
+ //        final String inputPcapFile = path + "/training/amazon-alexa/wlan1/alexa2.wlan1.local.pcap";
+ //        // TP-Link Plug DEVICE signatures
+ //        final String onSignatureFile = path + "/training/amazon-alexa/signatures/amazon-alexa-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/training/amazon-alexa/signatures/amazon-alexa-offSignature-device-side.sig";
+         // SmartThings Plug experiment
+ //        final String inputPcapFile = path + "/training/st-plug/wlan1/st-plug.wlan1.local.pcap";
+ //        // SmartThings Plug DEVICE signatures
+ //        //final String onSignatureFile = path + "/training/st-plug/signatures/st-plug-onSignature-device-side.sig";
+ //        //final String offSignatureFile = path + "/training/st-plug/signatures/st-plug-offSignature-device-side.sig";
+ //        // SmartThings Plug PHONE signatures
+ //        final String onSignatureFile = path + "/training/st-plug/signatures/st-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/st-plug/signatures/st-plug-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - November 12, 2018
+         // SmartThings Plug experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/st-plug/wlan1/st-plug.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/standalone/st-plug/eth0/st-plug.eth0.local.pcap";
+ //        //final String inputPcapFile = path + "/experimental_result/smarthome/st-plug/wlan1/st-plug.wlan1.detection.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/st-plug/eth0/st-plug.eth0.detection.pcap";
+ //        // SmartThings Plug PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/st-plug/signatures/st-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/st-plug/signatures/st-plug-offSignature-phone-side.sig";
+ //        final String onSignatureFile = path + "/training/signatures/st-plug/st-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/signatures/st-plug/st-plug-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - January 9, 2018
+         // Blossom Sprinkler experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/blossom-sprinkler/wlan1/blossom-sprinkler.wlan1.local.pcap";
 -//        final String onSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-onSignature-device-side.sig";
 -//        final String offSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-offSignature-device-side.sig";
++        final String inputPcapFile = path + "/experimental_result/smarthome/blossom-sprinkler/eth0/blossom-sprinkler.eth0.detection.pcap";
++//        final String inputPcapFile = path + "/experimental_result/smarthome/blossom-sprinkler/wlan1/blossom-sprinkler.wlan1.detection.pcap";
+         // Blossom Sprinkler DEVICE signatures
 -        final String onSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-onSignature-phone-side.sig";
 -        final String offSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-offSignature-phone-side.sig";
++        final String onSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-onSignature-device-side.sig";
++        final String offSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-offSignature-device-side.sig";
+         // Blossom Sprinkler PHONE signatures
 -        SignatureDetector onDetector = new SignatureDetector(onSignature, null);
 -        SignatureDetector offDetector = new SignatureDetector(offSignature, null);
++//        final String onSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-onSignature-phone-side.sig";
++//        final String offSignatureFile = path + "/experimental_result/standalone/blossom-sprinkler/signatures/blossom-sprinkler-offSignature-phone-side.sig";
+         // LiFX Bulb experiment
+ //        final String inputPcapFile = path + "/training/lifx-bulb/wlan1/lifx-bulb.wlan1.local.pcap";
+ //        // LiFX Bulb DEVICE signatures
+ //        final String onSignatureFile = path + "/training/lifx-bulb/signatures/lifx-bulb-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/training/lifx-bulb/signatures/lifx-bulb-offSignature-device-side.sig";
+         // LiFX Bulb PHONE signatures
+ //        final String onSignatureFile = path + "/training/lifx-bulb/signatures/lifx-bulb-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/lifx-bulb/signatures/lifx-bulb-offSignature-phone-side.sig";
+         // Blossom Sprinkler experiment
+ //        //final String inputPcapFile = path + "/training/blossom-sprinkler/wlan1/blossom-sprinkler.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/2018-08/blossom/blossom.wlan1.local.pcap";
+ //        //final String inputPcapFile = path + "/training/blossom-sprinkler/eth0/blossom-sprinkler.eth0.local.pcap";
+ //        // Blossom Sprinkler DEVICE signatures
+ //        final String onSignatureFile = path + "/training/blossom-sprinkler/signatures/blossom-sprinkler-onSignature-device-side.sig";
+ //        final String offSignatureFile = path + "/training/blossom-sprinkler/signatures/blossom-sprinkler-offSignature-device-side.sig";
+         // Nest Thermostat experiment
+ //        final String inputPcapFile = path + "/training/nest-thermostat/wlan1/nest-thermostat.wlan1.local.pcap";
+ //        // Nest Thermostat DEVICE signatures
+ ////        final String onSignatureFile = path + "/training/nest-thermostat/signatures/nest-thermostat-onSignature-device-side.sig";
+ ////        final String offSignatureFile = path + "/training/nest-thermostat/signatures/nest-thermostat-offSignature-device-side.sig";
+ //        // Nest Thermostat PHONE signatures
+ //        final String onSignatureFile = path + "/training/nest-thermostat/signatures/nest-thermostat-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/nest-thermostat/signatures/nest-thermostat-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - November 15, 2018
+         // Nest Thermostat experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/nest-thermostat/wlan1/nest-thermostat.wlan1.local.pcap";
+ ////        final String inputPcapFile = path + "/experimental_result/standalone/nest-thermostat/eth0/nest-thermostat.eth0.local.pcap";
+ ////        final String inputPcapFile = path + "/experimental_result/smarthome/nest-thermostat/wlan1/nest-thermostat.wlan1.detection.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/nest-thermostat/eth0/nest-thermostat.eth0.detection.pcap";
+ ////        // Nest Thermostat PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/nest-thermostat/signatures/nest-thermostat-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/nest-thermostat/signatures/nest-thermostat-offSignature-phone-side.sig";
+         /*
+         // Hue Bulb experiment
+         final String inputPcapFile = path + "/training/hue-bulb/wlan1/hue-bulb.wlan1.local.pcap";
+         // Hue Bulb PHONE signatures
+         final String onSignatureFile = path + "/training/hue-bulb/signatures/hue-bulb-onSignature-phone-side.sig";
+         final String offSignatureFile = path + "/training/hue-bulb/signatures/hue-bulb-offSignature-phone-side.sig";
+         */
+         // TP-Link Bulb experiment
+ //        final String inputPcapFile = path + "/training/tplink-bulb/wlan1/tplink-bulb.wlan1.local.pcap";
+ //        // TP-Link Bulb PHONE signatures
+ //        final String onSignatureFile = path + "/training/tplink-bulb/signatures/tplink-bulb-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/tplink-bulb/signatures/tplink-bulb-offSignature-phone-side.sig";
+         // TODO: EXPERIMENT - November 16, 2018
+         // TP-Link Bulb experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/tplink-bulb/wlan1/tplink-bulb.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/standalone/tplink-bulb/eth0/tplink-bulb.eth0.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/tplink-bulb/wlan1/tplink-bulb.wlan1.detection.pcap";
+ ////        final String inputPcapFile = path + "/experimental_result/smarthome/tplink-bulb/eth0/tplink-bulb.eth0.detection.pcap";
+ //        // TP-Link Bulb PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/tplink-bulb/signatures/tplink-bulb-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/tplink-bulb/signatures/tplink-bulb-offSignature-phone-side.sig";
+         /*
+         // WeMo Plug experiment
+         final String inputPcapFile = path + "/training/wemo-plug/wlan1/wemo-plug.wlan1.local.pcap";
+         // WeMo Plug PHONE signatures
+         final String onSignatureFile = path + "/training/wemo-plug/signatures/wemo-plug-onSignature-device-side.sig";
+         final String offSignatureFile = path + "/training/wemo-plug/signatures/wemo-plug-offSignature-device-side.sig";
+         */
+         // TODO: EXPERIMENT - November 20, 2018
+         // WeMo Plug experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/wemo-plug/wlan1/wemo-plug.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/standalone/wemo-plug/eth0/wemo-plug.eth0.local.pcap";
+         // TODO: WE HAVE 4 ADDITIONAL EVENTS (TRIGGERED MANUALLY), SO WE JUST IGNORE THEM BECAUSE THEY HAPPENED BEFORE
+         // TODO: THE ACTUAL TRIGGERS
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/wemo-plug/wlan1/wemo-plug.wlan1.detection.pcap";
+ ////        final String inputPcapFile = path + "/experimental_result/smarthome/wemo-plug/eth0/wemo-plug.eth0.detection.pcap";
+ //        // WeMo Plug PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/wemo-plug/signatures/wemo-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/wemo-plug/signatures/wemo-plug-offSignature-phone-side.sig";
+         /*
+         // WeMo Insight Plug experiment
+         final String inputPcapFile = path + "/training/wemo-insight-plug/wlan1/wemo-insight-plug.wlan1.local.pcap";
+         // WeMo Insight Plug PHONE signatures
+         final String onSignatureFile = path + "/training/wemo-insight-plug/signatures/wemo-insight-plug-onSignature-device-side.sig";
+         final String offSignatureFile = path + "/training/wemo-insight-plug/signatures/wemo-insight-plug-offSignature-device-side.sig";
+         */
+         // TODO: EXPERIMENT - November 21, 2018
+         // WeMo Insight Plug experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/wemo-insight-plug/wlan1/wemo-insight-plug.wlan1.local.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/standalone/wemo-insight-plug/eth0/wemo-insight-plug.eth0.local.pcap";
+         // TODO: WE HAVE 1 ADDITIONAL EVENT (FROM WEMO PLUG)
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/wemo-insight-plug/wlan1/wemo-insight-plug.wlan1.detection.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/wemo-insight-plug/eth0/wemo-insight-plug.eth0.detection.pcap";
+         // WeMo Insight Plug PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/wemo-insight-plug/signatures/wemo-insight-plug-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/wemo-insight-plug/signatures/wemo-insight-plug-offSignature-phone-side.sig";
+         // Kwikset Doorlock Sep 12 experiment
+ //        final String inputPcapFile = path + "/2018-08/kwikset-doorlock/kwikset3.wlan1.local.pcap";
+ //        // Kwikset Doorlock PHONE signatures
+ //        final String onSignatureFile = path + "/2018-08/kwikset-doorlock/onSignature-Kwikset-Doorlock-phone.sig";
+ //        final String offSignatureFile = path + "/2018-08/kwikset-doorlock/offSignature-Kwikset-Doorlock-phone.sig";
+         // TODO: EXPERIMENT - November 10, 2018
+         // Kwikset Door lock experiment
+ //        final String inputPcapFile = path + "/experimental_result/standalone/kwikset-doorlock/wlan1/kwikset-doorlock.wlan1.local.pcap";
+ //        //final String inputPcapFile = path + "/experimental_result/smarthome/kwikset-doorlock/wlan1/kwikset-doorlock.wlan1.detection.pcap";
+ //        final String inputPcapFile = path + "/experimental_result/smarthome/kwikset-doorlock/eth0/kwikset-doorlock.eth0.detection.pcap";
+ ////        // Kwikset Door lock PHONE signatures
+ //        final String onSignatureFile = path + "/experimental_result/standalone/kwikset-doorlock/signatures/kwikset-doorlock-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/experimental_result/standalone/kwikset-doorlock/signatures/kwikset-doorlock-offSignature-phone-side.sig";
+ //        final String onSignatureFile = path + "/training/signatures/kwikset-doorlock/kwikset-doorlock-onSignature-phone-side.sig";
+ //        final String offSignatureFile = path + "/training/signatures/kwikset-doorlock/kwikset-doorlock-offSignature-phone-side.sig";
+         // D-Link Siren experiment
+ //        final String inputPcapFile = path + "/2018-08/dlink-siren/dlink-siren.wlan1.local.pcap";
+         // D-Link Siren DEVICE signatures
+         //final String onSignatureFile = path + "/2018-08/dlink-siren/onSignature-DLink-Siren-device.sig";
+         //final String offSignatureFile = path + "/2018-08/dlink-siren/offSignature-DLink-Siren-device.sig";
+         // D-Link Siren PHONE signatures
+ //        final String onSignatureFile = path + "/2018-08/dlink-siren/onSignature-DLink-Siren-phone.sig";
+ //        final String offSignatureFile = path + "/2018-08/dlink-siren/offSignature-DLink-Siren-phone.sig";
+         // Output file names used (to make it easy to catch if one forgets to change them)
+         System.out.println("ON signature file in use is " + onSignatureFile);
+         System.out.println("OFF signature file in use is " + offSignatureFile);
+         System.out.println("PCAP file that is the target of detection is " + inputPcapFile);
+         List<List<List<PcapPacket>>> onSignature = PrintUtils.deserializeSignatureFromFile(onSignatureFile);
+         List<List<List<PcapPacket>>> offSignature = PrintUtils.deserializeSignatureFromFile(offSignatureFile);
+         // LAN
 -//        SignatureDetector onDetector = new SignatureDetector(onSignature, "128.195.205.105");
 -//        SignatureDetector offDetector = new SignatureDetector(offSignature, "128.195.205.105");
++//        SignatureDetector onDetector = new SignatureDetector(onSignature, null);
++//        SignatureDetector offDetector = new SignatureDetector(offSignature, null);
+         // WAN
++        SignatureDetector onDetector = new SignatureDetector(onSignature, "128.195.205.105");
++        SignatureDetector offDetector = new SignatureDetector(offSignature, "128.195.205.105");
+         final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).
+                 withLocale(Locale.US).withZone(ZoneId.of("America/Los_Angeles"));
+         // Outputs information about a detected event to std.out
+         final Consumer<UserAction> outputter = ua -> {
+             String eventDescription;
+             switch (ua.getType()) {
+                 case TOGGLE_ON:
+                     eventDescription = "ON";
+                     break;
+                 case TOGGLE_OFF:
+                     eventDescription = "OFF";
+                     break;
+                 default:
+                     throw new AssertionError("unhandled event type");
+             }
+             //String output = String.format("[ !!! %s SIGNATURE DETECTED at %s !!! ]",
+             //      eventDescription, dateTimeFormatter.format(ua.getTimestamp()));
+             String output = String.format("%s",
+                     dateTimeFormatter.format(ua.getTimestamp()));
+             System.out.println(output);
+         };
+         // Let's create observers that construct a UserAction representing the detected event.
+         final List<UserAction> detectedEvents = new ArrayList<>();
+         onDetector.addObserver((searched, match) -> {
+             PcapPacket firstPkt = match.get(0).get(0);
+             detectedEvents.add(new UserAction(UserAction.Type.TOGGLE_ON, firstPkt.getTimestamp()));
+         });
+         offDetector.addObserver((searched, match) -> {
+             PcapPacket firstPkt = match.get(0).get(0);
+             detectedEvents.add(new UserAction(UserAction.Type.TOGGLE_OFF, firstPkt.getTimestamp()));
+         });
+         PcapHandle handle;
+         try {
+             handle = Pcaps.openOffline(inputPcapFile, PcapHandle.TimestampPrecision.NANO);
+         } catch (PcapNativeException pne) {
+             handle = Pcaps.openOffline(inputPcapFile);
+         }
+         PcapHandleReader reader = new PcapHandleReader(handle, p -> true, onDetector, offDetector);
+         reader.readFromHandle();
+         // TODO: need a better way of triggering detection than this...
+         onDetector.mClusterMatchers.forEach(cm -> cm.performDetection());
+         offDetector.mClusterMatchers.forEach(cm -> cm.performDetection());
+         // Sort the list of detected events by timestamp to make it easier to compare it line-by-line with the trigger
+         // times file.
+         Collections.sort(detectedEvents, Comparator.comparing(UserAction::getTimestamp));
+         // Output the detected events
+         detectedEvents.forEach(outputter);
+         System.out.println("Number of detected events of type " + UserAction.Type.TOGGLE_ON + ": " +
+                 detectedEvents.stream().filter(ua -> ua.getType() == UserAction.Type.TOGGLE_ON).count());
+         System.out.println("Number of detected events of type " + UserAction.Type.TOGGLE_OFF + ": " +
+                 detectedEvents.stream().filter(ua -> ua.getType() == UserAction.Type.TOGGLE_OFF).count());
+         // TODO: Temporary clean up until we clean the pipeline
+ //        List<UserAction> cleanedDetectedEvents = SignatureDetector.removeDuplicates(detectedEvents);
+ //        cleanedDetectedEvents.forEach(outputter);
+     }
+     /**
+      * The signature that this {@link SignatureDetector} is searching for.
+      */
+     private final List<List<List<PcapPacket>>> mSignature;
+     /**
+      * The {@link Layer3ClusterMatcher}s in charge of detecting each individual sequence of packets that together make up the
+      * the signature.
+      */
+     private final List<Layer3ClusterMatcher> mClusterMatchers;
+     /**
+      * For each {@code i} ({@code i >= 0 && i < pendingMatches.length}), {@code pendingMatches[i]} holds the matches
+      * found by the {@link Layer3ClusterMatcher} at {@code mClusterMatchers.get(i)} that have yet to be "consumed", i.e.,
+      * have yet to be included in a signature detected by this {@link SignatureDetector} (a signature can be encompassed
+      * of multiple packet sequences occurring shortly after one another on multiple connections).
+      */
+     private final List<List<PcapPacket>>[] pendingMatches;
+     /**
+      * Maps a {@link Layer3ClusterMatcher} to its corresponding index in {@link #pendingMatches}.
+      */
+     private final Map<Layer3ClusterMatcher, Integer> mClusterMatcherIds;
+     private final List<SignatureDetectionObserver> mObservers = new ArrayList<>();
+     /**
+      * Remove duplicates in {@code List} of {@code UserAction} objects. We need to clean this up for user actions
+      * that appear multiple times.
+      * TODO: This static method is probably just for temporary and we could get rid of this after we clean up
+      * TODO:    the pipeline
+      *
+      * @param listUserAction A {@link List} of {@code UserAction}.
+      *
+      */
+     public static List<UserAction> removeDuplicates(List<UserAction> listUserAction) {
+         // Iterate and check for duplicates (check timestamps)
+         Set<Long> epochSecondSet = new HashSet<>();
+         // Create a target list for cleaned up list
+         List<UserAction> listUserActionClean = new ArrayList<>();
+         for(UserAction userAction : listUserAction) {
+             // Don't insert if any duplicate is found
+             if(!epochSecondSet.contains(userAction.getTimestamp().getEpochSecond())) {
+                 listUserActionClean.add(userAction);
+                 epochSecondSet.add(userAction.getTimestamp().getEpochSecond());
+             }
+         }
+         return listUserActionClean;
+     }
+     public SignatureDetector(List<List<List<PcapPacket>>> searchedSignature, String routerWanIp) {
+         // note: doesn't protect inner lists from changes :'(
+         mSignature = Collections.unmodifiableList(searchedSignature);
+         // Generate corresponding/appropriate ClusterMatchers based on the provided signature
+         List<Layer3ClusterMatcher> clusterMatchers = new ArrayList<>();
+         for (List<List<PcapPacket>> cluster : mSignature) {
+             clusterMatchers.add(new Layer3ClusterMatcher(cluster, routerWanIp, this));
+         }
+         mClusterMatchers = Collections.unmodifiableList(clusterMatchers);
+         // < exploratory >
+         pendingMatches = new List[mClusterMatchers.size()];
+         for (int i = 0; i < pendingMatches.length; i++) {
+             pendingMatches[i] = new ArrayList<>();
+         }
+         Map<Layer3ClusterMatcher, Integer> clusterMatcherIds = new HashMap<>();
+         for (int i = 0; i < mClusterMatchers.size(); i++) {
+             clusterMatcherIds.put(mClusterMatchers.get(i), i);
+         }
+         mClusterMatcherIds = Collections.unmodifiableMap(clusterMatcherIds);
+     }
+     public void addObserver(SignatureDetectionObserver observer) {
+         mObservers.add(observer);
+     }
+     public boolean removeObserver(SignatureDetectionObserver observer) {
+         return mObservers.remove(observer);
+     }
+     @Override
+     public void gotPacket(PcapPacket packet) {
+         // simply delegate packet reception to all ClusterMatchers.
+         mClusterMatchers.forEach(cm -> cm.gotPacket(packet));
+     }
+     @Override
+     public void onMatch(Layer3ClusterMatcher clusterMatcher, List<PcapPacket> match) {
+         // Add the match at the corresponding index
+         pendingMatches[mClusterMatcherIds.get(clusterMatcher)].add(match);
+         checkSignatureMatch();
+     }
+     private void checkSignatureMatch() {
+         // << Graph-based approach using Balint's idea. >>
+         // This implementation assumes that the packets in the inner lists (the sequences) are ordered by asc timestamp.
+         // There cannot be a signature match until each Layer3ClusterMatcher has found a match of its respective sequence.
+         if (Arrays.stream(pendingMatches).noneMatch(l -> l.isEmpty())) {
+             // Construct the DAG
+             final SimpleDirectedWeightedGraph<Vertex, DefaultWeightedEdge> graph =
+                     new SimpleDirectedWeightedGraph<>(DefaultWeightedEdge.class);
+             // Add a vertex for each match found by all ClusterMatchers
+             // And maintain an array to keep track of what cluster matcher each vertex corresponds to
+             final List<Vertex>[] vertices = new List[pendingMatches.length];
+             for (int i = 0; i < pendingMatches.length; i++) {
+                 vertices[i] = new ArrayList<>();
+                 for (List<PcapPacket> sequence : pendingMatches[i]) {
+                     Vertex v = new Vertex(sequence);
+                     vertices[i].add(v); // retain reference for later when we are to add edges
+                     graph.addVertex(v); // add to vertex to graph
+                 }
+             }
+             // Add dummy source and sink vertices to facilitate search.
+             final Vertex source = new Vertex(null);
+             final Vertex sink = new Vertex(null);
+             graph.addVertex(source);
+             graph.addVertex(sink);
+             // The source is connected to all vertices that wrap the sequences detected by Layer3ClusterMatcher at index 0.
+             // Note: zero cost edges as this is just a dummy link to facilitate search from a common start node.
+             for (Vertex v : vertices[0]) {
+                 DefaultWeightedEdge edge = graph.addEdge(source, v);
+                 graph.setEdgeWeight(edge, 0.0);
+             }
+             // Similarly, all vertices that wrap the sequences detected by the last Layer3ClusterMatcher of the signature
+             // are connected to the sink node.
+             for (Vertex v : vertices[vertices.length-1]) {
+                 DefaultWeightedEdge edge = graph.addEdge(v, sink);
+                 graph.setEdgeWeight(edge, 0.0);
+             }
+             // Now link sequences detected by Layer3ClusterMatcher at index i to sequences detected by Layer3ClusterMatcher at index
+             // i+1 if they obey the timestamp constraint (i.e., that the latter is later in time than the former).
+             for (int i = 0; i < vertices.length; i++) {
+                 int j = i + 1;
+                 if (j < vertices.length) {
+                     for (Vertex iv : vertices[i]) {
+                         PcapPacket ivLast = iv.sequence.get(iv.sequence.size()-1);
+                         for (Vertex jv : vertices[j]) {
+                             PcapPacket jvFirst = jv.sequence.get(jv.sequence.size()-1);
+                             if (ivLast.getTimestamp().isBefore(jvFirst.getTimestamp())) {
+                                 DefaultWeightedEdge edge = graph.addEdge(iv, jv);
+                                 // The weight is the duration of the i'th sequence plus the duration between the i'th
+                                 // and i+1'th sequence.
+                                 Duration d = Duration.
+                                         between(iv.sequence.get(0).getTimestamp(), jvFirst.getTimestamp());
+                                 // Unfortunately weights are double values, so must convert from long to double.
+                                 // TODO: need nano second precision? If so, use d.toNanos().
+                                 // TODO: risk of overflow when converting from long to double..?
+                                 graph.setEdgeWeight(edge, Long.valueOf(d.toMillis()).doubleValue());
+                             }
+                             // Alternative version if we cannot assume that sequences are ordered by timestamp:
+ //                            if (iv.sequence.stream().max(Comparator.comparing(PcapPacket::getTimestamp)).get()
+ //                                    .getTimestamp().isBefore(jv.sequence.stream().min(
+ //                                            Comparator.comparing(PcapPacket::getTimestamp)).get().getTimestamp())) {
+ //
+ //                            }
+                         }
+                     }
+                 }
+             }
+             // Graph construction complete, run shortest-path to find a (potential) signature match.
+             DijkstraShortestPath<Vertex, DefaultWeightedEdge> dijkstra = new DijkstraShortestPath<>(graph);
+             GraphPath<Vertex, DefaultWeightedEdge> shortestPath = dijkstra.getPath(source, sink);
+             if (shortestPath != null) {
+                 // The total weight is the duration between the first packet of the first sequence and the last packet
+                 // of the last sequence, so we simply have to compare the weight against the timeframe that we allow
+                 // the signature to span. For now we just use the inclusion window we defined for training purposes.
+                 // Note however, that we must convert back from double to long as the weight is stored as a double in
+                 // JGraphT's API.
+                 if (((long)shortestPath.getWeight()) < TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS) {
+                     // There's a signature match!
+                     // Extract the match from the vertices
+                     List<List<PcapPacket>> signatureMatch = new ArrayList<>();
+                     for(Vertex v : shortestPath.getVertexList()) {
+                         if (v == source || v == sink) {
+                             // Skip the dummy source and sink nodes.
+                             continue;
+                         }
+                         signatureMatch.add(v.sequence);
+                         // As there is a one-to-one correspondence between vertices[] and pendingMatches[], we know that
+                         // the sequence we've "consumed" for index i of the matched signature is also at index i in
+                         // pendingMatches. We must remove it from pendingMatches so that we don't use it to construct
+                         // another signature match in a later call.
+                         pendingMatches[signatureMatch.size()-1].remove(v.sequence);
+                     }
+                     // Declare success: notify observers
+                     mObservers.forEach(obs -> obs.onSignatureDetected(mSignature,
+                             Collections.unmodifiableList(signatureMatch)));
+                 }
+             }
+         }
+     }
+     /**
+      * Used for registering for notifications of signatures detected by a {@link SignatureDetector}.
+      */
+     interface SignatureDetectionObserver {
+         /**
+          * Invoked when the {@link SignatureDetector} detects the presence of a signature in the traffic that it's
+          * examining.
+          * @param searchedSignature The signature that the {@link SignatureDetector} reporting the match is searching
+          *                          for.
+          * @param matchingTraffic The actual traffic trace that matches the searched signature.
+          */
+         void onSignatureDetected(List<List<List<PcapPacket>>> searchedSignature,
+                                  List<List<PcapPacket>> matchingTraffic);
+     }
+     /**
+      * Encapsulates a {@code List<PcapPacket>} so as to allow the list to be used as a vertex in a graph while avoiding
+      * the expensive {@link AbstractList#equals(Object)} calls when adding vertices to the graph.
+      * Using this wrapper makes the incurred {@code equals(Object)} calls delegate to {@link Object#equals(Object)}
+      * instead of {@link AbstractList#equals(Object)}. The net effect is a faster implementation, but the graph will not
+      * recognize two lists that contain the same items--from a value and not reference point of view--as the same
+      * vertex. However, this is fine for our purposes -- in fact restricting it to reference equality seems more
+      * appropriate.
+      */
+     private static class Vertex {
+         private final List<PcapPacket> sequence;
+         private Vertex(List<PcapPacket> wrappedSequence) {
+             sequence = wrappedSequence;
+         }
+     }
+ }
index 8f58d977850036d9c71bd18df05225412c3636ca,f07072abe8d9daf8ddd9a9bd2ce40e022ed5ee79..cee09fed8c1c78ddc19459470864b01571f6fd93
@@@ -1,13 -1,15 +1,15 @@@
  package edu.uci.iotproject.util;
  
- import edu.uci.iotproject.Conversation;
+ import edu.uci.iotproject.trafficreassembly.layer3.Conversation;
  import edu.uci.iotproject.analysis.PcapPacketPair;
  import edu.uci.iotproject.analysis.TcpConversationUtils;
  import edu.uci.iotproject.analysis.TriggerTrafficExtractor;
  import org.apache.commons.math3.stat.clustering.Cluster;
  import org.pcap4j.core.PcapPacket;
+ import org.pcap4j.packet.EthernetPacket;
  import org.pcap4j.packet.IpV4Packet;
  import org.pcap4j.packet.TcpPacket;
+ import org.pcap4j.util.MacAddress;
  
  import java.util.*;
  
@@@ -26,6 -28,25 +28,25 @@@ public final class PcapPacketUtils 
       */
      private static final int SIGNATURE_MERGE_THRESHOLD = 5;
  
+     /**
+      * Gets the source address of the Ethernet part of {@code packet}.
+      * @param packet The packet for which the Ethernet source address is to be extracted.
+      * @return The source address of the Ethernet part of {@code packet}.
+      */
+     public static MacAddress getEthSrcAddr(PcapPacket packet) {
+         return getEthernetPacketOrThrow(packet).getHeader().getSrcAddr();
+     }
+     /**
+      * Gets the destination address of the Ethernet part of {@code packet}.
+      * @param packet The packet for which the Ethernet destination address is to be extracted.
+      * @return The destination address of the Ethernet part of {@code packet}.
+      */
+     public static MacAddress getEthDstAddr(PcapPacket packet) {
+         return getEthernetPacketOrThrow(packet).getHeader().getDstAddr();
+     }
      /**
       * Determines if a given {@link PcapPacket} wraps a {@link TcpPacket}.
       * @param packet The {@link PcapPacket} to inspect.
      public static List<List<List<PcapPacket>>> sortSignatures(List<List<List<PcapPacket>>> signatures) {
          // TODO: This is the simplest solution!!! Might not cover all corner cases.
          // TODO: Sort the list of lists based on the first packet's timestamps!
 -        //Collections.sort(signatures, (p1, p2) -> {
 -        //    return p1.get(0).get(0).getTimestamp().compareTo(p2.get(0).get(0).getTimestamp());
 -        //});
 +//        Collections.sort(signatures, (p1, p2) -> {
 +//            return p1.get(0).get(0).getTimestamp().compareTo(p2.get(0).get(0).getTimestamp());
 +//        });
          // TODO: The following is a more complete solution that covers corner cases.
          // Sort the list of lists based on one-to-one comparison between timestamps of signatures on both lists.
          // This also takes into account the fact that the number of signatures in the two lists could be different.
      /**
       * Gets the {@link IpV4Packet} contained in {@code packet}, or throws a {@link NullPointerException} if
       * {@code packet} does not contain an {@link IpV4Packet}.
-      * @param packet A {@link PcapPacket} that is expected to contain a {@link IpV4Packet}.
+      * @param packet A {@link PcapPacket} that is expected to contain an {@link IpV4Packet}.
       * @return The {@link IpV4Packet} contained in {@code packet}.
       * @throws NullPointerException if {@code packet} does not encapsulate an {@link IpV4Packet}.
       */
          return Objects.requireNonNull(packet.get(IpV4Packet.class), "not an IPv4 packet");
      }
  
+     /**
+      * Gets the {@link EthernetPacket} contained in {@code packet}, or throws a {@link NullPointerException} if
+      * {@code packet} does not contain an {@link EthernetPacket}.
+      * @param packet A {@link PcapPacket} that is expected to contain an {@link EthernetPacket}.
+      * @return The {@link EthernetPacket} contained in {@code packet}.
+      * @throws NullPointerException if {@code packet} does not encapsulate an {@link EthernetPacket}.
+      */
+     private static final EthernetPacket getEthernetPacketOrThrow(PcapPacket packet) {
+         return Objects.requireNonNull(packet.get(EthernetPacket.class), "not an Ethernet packet");
+     }
      /**
       * Print signatures in {@code List} of {@code List} of {@code List} of {@code PcapPacket} objects.
       *