3629e8086836ce5f8342c86341aff5a2268860ac
[pingpong.git] / Code / Projects / PacketLevelSignatureExtractor / src / main / java / edu / uci / iotproject / SignatureGenerator.java
1 package edu.uci.iotproject;
2
3 import static edu.uci.iotproject.analysis.UserAction.Type;
4
5 import edu.uci.iotproject.analysis.*;
6 import edu.uci.iotproject.io.PrintWriterUtils;
7 import edu.uci.iotproject.io.TriggerTimesFileReader;
8 import edu.uci.iotproject.trafficreassembly.layer3.Conversation;
9 import edu.uci.iotproject.trafficreassembly.layer3.TcpReassembler;
10 import edu.uci.iotproject.util.PcapPacketUtils;
11 import edu.uci.iotproject.util.PrintUtils;
12 import org.apache.commons.math3.stat.clustering.Cluster;
13 import org.apache.commons.math3.stat.clustering.DBSCANClusterer;
14 import org.pcap4j.core.*;
15 import org.pcap4j.packet.namednumber.DataLinkType;
16
17 import java.io.*;
18 import java.net.UnknownHostException;
19 import java.time.Duration;
20 import java.time.Instant;
21 import java.util.*;
22 import java.util.concurrent.TimeoutException;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
25
26 /**
27  * This is a system that reads PCAP files to compare
28  * patterns of DNS hostnames, packet sequences, and packet
29  * lengths with training data to determine certain events
30  * or actions for smart home devices.
31  *
32  * @author Janus Varmarken
33  * @author Rahmadi Trimananda (rtrimana@uci.edu)
34  * @version 0.1
35  */
36 public class SignatureGenerator {
37
38     /**
39      * If set to {@code true}, output written to the results file is also dumped to standard out.
40      */
41     private static boolean DUPLICATE_OUTPUT_TO_STD_OUT = true;
42     /**
43      * File name for logging.
44      */
45     private static String LOG_EXTENSION = "_signature-generation.log";
46     /**
47      * Directory for logging.
48      */
49     private static String LOG_DIRECTORY = "./";
50
51     public static void main(String[] args) throws PcapNativeException, NotOpenException, EOFException,
52             TimeoutException, UnknownHostException, IOException {
53         // -------------------------------------------------------------------------------------------------------------
54         // ------------ # Code for extracting traffic generated by a device within x seconds of a trigger # ------------
55         if (args.length < 11) {
56             String errMsg = String.format("Usage: %s inputPcapFile outputPcapFile triggerTimesFile deviceIp" +
57                             " onSignatureFile offSignatureFile onClusterAnalysisFile offClusterAnalysisFile epsilon" +
58                             " deletedSequencesOn deletedSequencesOff" +
59                             "\n  inputPcapFile: the target of the detection" +
60                             "\n  outputPcapFile: the processed PCAP file through 15-second window filtering" +
61                             "\n  triggerTimesFile: the trigger timestamps" +
62                             "\n  deviceIp: the IP address of the device we want to generate a signature for" +
63                             "\n  onSignatureFile: name of the ON signature file" +
64                             "\n  offSignatureFile: name of the OFF signature file" +
65                             "\n  onClusterAnalysisFile: name of the ON signature cluster analysis file" +
66                             "\n  offClusterAnalysisFile: name of the OFF signature cluster analysis file" +
67                             "\n  epsilon: epsilon value of the DBSCAN algorithm" +
68                             "\n  deletedSequencesOn: sequences to be deleted from the final ON signature" +
69                             " (please separate with commas, e.g., 0,1,2, or put '-1' if not needed)" +
70                             "\n  deletedSequencesOff: sequences to be deleted from the final OFF signature" +
71                             " (please separate with commas, e.g., 0,1,2, or put '-1' if not needed)",
72                     SignatureGenerator.class.getSimpleName());
73             System.out.println(errMsg);
74             return;
75         }
76         boolean verbose = true;
77         final String inputPcapFile = args[0];
78         final String outputPcapFile = args[1];
79         final String triggerTimesFile = args[2];
80         final String deviceIp = args[3];
81         final String onSignatureFile = args[4];
82         final String offSignatureFile = args[5];
83         final String onClusterAnalysisFile = args[6];
84         final String offClusterAnalysisFile = args[7];
85         final double eps = Double.parseDouble(args[8]);
86         final String deletedSequencesOn = args[9];
87         final String deletedSequencesOff = args[10];
88         final String logFile = inputPcapFile + LOG_EXTENSION;
89
90         // Prepare file outputter.
91         File outputFile = new File(logFile);
92         outputFile.getParentFile().mkdirs();
93         final PrintWriter resultsWriter = new PrintWriter(new FileWriter(outputFile));
94
95         // =========================================== TRAFFIC FILTERING ============================================
96
97         TriggerTimesFileReader ttfr = new TriggerTimesFileReader();
98         List<Instant> triggerTimes = ttfr.readTriggerTimes(triggerTimesFile, false);
99         // Tag each trigger with "ON" or "OFF", assuming that the first trigger is an "ON" and that they alternate.
100         List<UserAction> userActions = new ArrayList<>();
101         for (int i = 0; i < triggerTimes.size(); i++) {
102             userActions.add(new UserAction(i % 2 == 0 ? Type.TOGGLE_ON : Type.TOGGLE_OFF, triggerTimes.get(i)));
103         }
104         TriggerTrafficExtractor tte = new TriggerTrafficExtractor(inputPcapFile, triggerTimes, deviceIp);
105         final PcapDumper outputter = Pcaps.openDead(DataLinkType.EN10MB, 65536).dumpOpen(outputPcapFile);
106         DnsMap dnsMap = new DnsMap();
107         TcpReassembler tcpReassembler = new TcpReassembler();
108         TrafficLabeler trafficLabeler = new TrafficLabeler(userActions);
109         tte.performExtraction(pkt -> {
110             try {
111                 outputter.dump(pkt);
112             } catch (NotOpenException e) {
113                 e.printStackTrace();
114             }
115         }, dnsMap, tcpReassembler, trafficLabeler);
116         outputter.flush();
117         outputter.close();
118
119         if (tte.getPacketsIncludedCount() != trafficLabeler.getTotalPacketCount()) {
120             // Sanity/debug check
121             throw new AssertionError(String.format("mismatch between packet count in %s and %s",
122                     TriggerTrafficExtractor.class.getSimpleName(), TrafficLabeler.class.getSimpleName()));
123         }
124
125         // Extract all conversations present in the filtered trace.
126         List<Conversation> allConversations = tcpReassembler.getTcpConversations();
127         // Group conversations by hostname.
128         Map<String, List<Conversation>> convsByHostname =
129                 TcpConversationUtils.groupConversationsByHostname(allConversations, dnsMap);
130         PrintWriterUtils.println("Grouped conversations by hostname.", resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
131         // For each hostname, count the frequencies of packet lengths exchanged with that hostname.
132         final Map<String, Map<Integer, Integer>> pktLenFreqsByHostname = new HashMap<>();
133         convsByHostname.forEach((host, convs) -> pktLenFreqsByHostname.put(host,
134                 TcpConversationUtils.countPacketLengthFrequencies(convs)));
135         PrintWriterUtils.println("Counted frequencies of packet lengths exchanged with each hostname.",
136                 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
137         // For each hostname, count the frequencies of packet sequences (i.e., count how many
138         // conversations exchange a sequence of packets of some specific lengths).
139         final Map<String, Map<String, Integer>> pktSeqFreqsByHostname = new HashMap<>();
140         convsByHostname.forEach((host, convs) -> pktSeqFreqsByHostname.put(host,
141                 TcpConversationUtils.countPacketSequenceFrequencies(convs)));
142         PrintWriterUtils.println("Counted frequencies of packet sequences exchanged with each hostname.",
143                 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
144         // For each hostname, count frequencies of packet pairs exchanged
145         // with that hostname across all conversations
146         final Map<String, Map<String, Integer>> pktPairFreqsByHostname =
147                 TcpConversationUtils.countPacketPairFrequenciesByHostname(allConversations, dnsMap);
148         PrintWriterUtils.println("Counted frequencies of packet pairs per hostname.",
149                 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
150         // For each user action, reassemble the set of TCP connections occurring shortly after
151         final Map<UserAction, List<Conversation>> userActionToConversations =
152                 trafficLabeler.getLabeledReassembledTcpTraffic();
153         final Map<UserAction, Map<String, List<Conversation>>> userActionsToConvsByHostname =
154                 trafficLabeler.getLabeledReassembledTcpTraffic(dnsMap);
155         PrintWriterUtils.println("Reassembled TCP conversations occurring shortly after each user event.",
156                 resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
157
158         /*
159          * NOTE: no need to generate these more complex on/off maps that also contain mappings from hostname and
160          * sequence identifiers as we do not care about hostnames and sequences during clustering.
161          * We can simply use the UserAction->List<Conversation> map to generate ON/OFF groupings of conversations.
162          */
163         // Contains all ON events: hostname -> sequence identifier -> list of conversations with that sequence
164         Map<String, Map<String, List<Conversation>>> ons = new HashMap<>();
165         // Contains all OFF events: hostname -> sequence identifier -> list of conversations with that sequence
166         Map<String, Map<String, List<Conversation>>> offs = new HashMap<>();
167         userActionsToConvsByHostname.forEach((ua, hostnameToConvs) -> {
168             Map<String, Map<String, List<Conversation>>> outer = ua.getType() == Type.TOGGLE_ON ? ons : offs;
169             hostnameToConvs.forEach((host, convs) -> {
170                 Map<String, List<Conversation>> seqsToConvs = TcpConversationUtils.
171                         groupConversationsByPacketSequence(convs, verbose);
172                 outer.merge(host, seqsToConvs, (oldMap, newMap) -> {
173                     newMap.forEach((sequence, cs) -> oldMap.merge(sequence, cs, (list1, list2) -> {
174                         list1.addAll(list2);
175                         return list1;
176                     }));
177                     return oldMap;
178                 });
179             });
180         });
181
182         // ============================================== PAIR CLUSTERING ============================================
183         // TODO: No need to use the more convoluted on/off maps; Can simply use the UserAction->List<Conversation> map
184         // TODO: when don't care about hostnames and sequences (see comment earlier).
185         // ===========================================================================================================
186         List<Conversation> onConversations = userActionToConversations.entrySet().stream().
187                 filter(e -> e.getKey().getType() == Type.TOGGLE_ON). // drop all OFF events from stream
188                 map(e -> e.getValue()). // no longer interested in the UserActions
189                 flatMap(List::stream). // flatten List<List<T>> to a List<T>
190                 collect(Collectors.toList());
191         List<Conversation> offConversations = userActionToConversations.entrySet().stream().
192                 filter(e -> e.getKey().getType() == Type.TOGGLE_OFF).
193                 map(e -> e.getValue()).
194                 flatMap(List::stream).
195                 collect(Collectors.toList());
196         //Collections.sort(onConversations, (c1, c2) -> c1.getPackets().)
197
198         List<PcapPacketPair> onPairs = onConversations.stream().
199                 map(c -> c.isTls() ? TcpConversationUtils.extractTlsAppDataPacketPairs(c) :
200                         TcpConversationUtils.extractPacketPairs(c)).
201                 flatMap(List::stream). // flatten List<List<>> to List<>
202                 collect(Collectors.toList());
203         List<PcapPacketPair> offPairs = offConversations.stream().
204                 map(c -> c.isTls() ? TcpConversationUtils.extractTlsAppDataPacketPairs(c) :
205                         TcpConversationUtils.extractPacketPairs(c)).
206                 flatMap(List::stream). // flatten List<List<>> to List<>
207                 collect(Collectors.toList());
208         // Note: need to update the DnsMap of all PcapPacketPairs if we want to use the IP/hostname-sensitive distance.
209         Stream.concat(Stream.of(onPairs), Stream.of(offPairs)).flatMap(List::stream).forEach(p -> p.setDnsMap(dnsMap));
210         // Perform clustering on conversation logged as part of all ON events.
211         // Calculate number of events per type (only ON/only OFF), which means half of the number of all timestamps.
212         int numberOfEventsPerType = triggerTimes.size() / 2;
213         int lowerBound = numberOfEventsPerType - (int)(numberOfEventsPerType * 0.1);
214         int upperBound = numberOfEventsPerType + (int)(numberOfEventsPerType * 0.1);
215         int minPts = lowerBound;
216         DBSCANClusterer<PcapPacketPair> onClusterer = new DBSCANClusterer<>(eps, minPts);
217         List<Cluster<PcapPacketPair>> onClusters = onClusterer.cluster(onPairs);
218         // Perform clustering on conversation logged as part of all OFF events.
219         DBSCANClusterer<PcapPacketPair> offClusterer = new DBSCANClusterer<>(eps, minPts);
220         List<Cluster<PcapPacketPair>> offClusters = offClusterer.cluster(offPairs);
221         // Sort the conversations as reference
222         List<Conversation> sortedAllConversation = TcpConversationUtils.sortConversationList(allConversations);
223         // Output clusters
224         PrintWriterUtils.println("========================================", resultsWriter,
225                 DUPLICATE_OUTPUT_TO_STD_OUT);
226         PrintWriterUtils.println("       Clustering results for ON        ", resultsWriter,
227                 DUPLICATE_OUTPUT_TO_STD_OUT);
228         PrintWriterUtils.println("       Number of clusters: " + onClusters.size(), resultsWriter,
229                 DUPLICATE_OUTPUT_TO_STD_OUT);
230         int count = 0;
231         List<List<List<PcapPacket>>> ppListOfListReadOn = new ArrayList<>();
232         List<List<List<PcapPacket>>> ppListOfListListOn = new ArrayList<>();
233         List<List<List<PcapPacket>>> corePointRangeSignatureOn = new ArrayList<>();
234         for (Cluster<PcapPacketPair> c : onClusters) {
235             PrintWriterUtils.println(String.format("<<< Cluster #%02d (%03d points) >>>", ++count, c.getPoints().size()),
236                     resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
237             PrintWriterUtils.println(PrintUtils.toSummaryString(c), resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
238             if(c.getPoints().size() > lowerBound && c.getPoints().size() < upperBound) {
239                 // Print to file
240                 List<List<PcapPacket>> ppListOfList = PcapPacketUtils.clusterToListOfPcapPackets(c);
241                 // Check for overlaps and decide whether to do range-based or conservative checking
242                 corePointRangeSignatureOn.add(PcapPacketUtils.extractRangeCorePoints(ppListOfList, eps, minPts));
243                 ppListOfListListOn.add(ppListOfList);
244             }
245         }
246         PrintWriterUtils.println("========================================", resultsWriter,
247                 DUPLICATE_OUTPUT_TO_STD_OUT);
248         PrintWriterUtils.println("       Clustering results for OFF        ", resultsWriter,
249                 DUPLICATE_OUTPUT_TO_STD_OUT);
250         PrintWriterUtils.println("       Number of clusters: " + offClusters.size(), resultsWriter,
251                 DUPLICATE_OUTPUT_TO_STD_OUT);
252         count = 0;
253         List<List<List<PcapPacket>>> ppListOfListReadOff = new ArrayList<>();
254         List<List<List<PcapPacket>>> ppListOfListListOff = new ArrayList<>();
255         List<List<List<PcapPacket>>> corePointRangeSignatureOff = new ArrayList<>();
256         for (Cluster<PcapPacketPair> c : offClusters) {
257             PrintWriterUtils.println(String.format("<<< Cluster #%03d (%06d points) >>>", ++count, c.getPoints().size()),
258                     resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
259             PrintWriterUtils.println(PrintUtils.toSummaryString(c), resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
260             if(c.getPoints().size() > lowerBound && c.getPoints().size() < upperBound) {
261                 // Print to file
262                 List<List<PcapPacket>> ppListOfList = PcapPacketUtils.clusterToListOfPcapPackets(c);
263                 // Check for overlaps and decide whether to do range-based or conservative checking
264                 corePointRangeSignatureOff.add(PcapPacketUtils.extractRangeCorePoints(ppListOfList, eps, minPts));
265                 ppListOfListListOff.add(ppListOfList);
266             }
267         }
268
269         // =========================================== SIGNATURE CREATION ===========================================
270         // Concatenate
271         ppListOfListListOn = PcapPacketUtils.concatSequences(ppListOfListListOn, sortedAllConversation);
272         // Remove sequences in the list that have overlap
273         StringTokenizer stringTokenizerOn = new StringTokenizer(deletedSequencesOn, ",");
274         while(stringTokenizerOn.hasMoreTokens()) {
275             int sequenceToDelete = Integer.parseInt(stringTokenizerOn.nextToken());
276             if (sequenceToDelete == -1) { // '-1' means there is no removal
277                 break;
278             }
279             PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOn, sequenceToDelete);
280         }
281         ppListOfListListOn = PcapPacketUtils.sortSequences(ppListOfListListOn);
282
283         // Concatenate
284         ppListOfListListOff = PcapPacketUtils.concatSequences(ppListOfListListOff, sortedAllConversation);
285         // Remove sequences in the list that have overlap
286         StringTokenizer stringTokenizerOff = new StringTokenizer(deletedSequencesOff, ",");
287         while(stringTokenizerOff.hasMoreTokens()) {
288             int sequenceToDelete = Integer.parseInt(stringTokenizerOff.nextToken());
289             if (sequenceToDelete == -1) { // '-1' means there is no removal
290                 break;
291             }
292             PcapPacketUtils.removeSequenceFromSignature(ppListOfListListOff, sequenceToDelete);
293         }
294         ppListOfListListOff = PcapPacketUtils.sortSequences(ppListOfListListOff);
295         // Write the signatures into the screen
296         PrintWriterUtils.println("========================================", resultsWriter,
297                 DUPLICATE_OUTPUT_TO_STD_OUT);
298         PrintWriterUtils.println("              ON Signature              ", resultsWriter,
299                 DUPLICATE_OUTPUT_TO_STD_OUT);
300         PrintWriterUtils.println("========================================", resultsWriter,
301                 DUPLICATE_OUTPUT_TO_STD_OUT);
302         PcapPacketUtils.printSignatures(ppListOfListListOn, resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
303         PrintWriterUtils.println("========================================", resultsWriter,
304                 DUPLICATE_OUTPUT_TO_STD_OUT);
305         PrintWriterUtils.println("              OFF Signature             ", resultsWriter,
306                 DUPLICATE_OUTPUT_TO_STD_OUT);
307         PrintWriterUtils.println("========================================", resultsWriter,
308                 DUPLICATE_OUTPUT_TO_STD_OUT);
309         PcapPacketUtils.printSignatures(ppListOfListListOff, resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
310         // Printing signatures into files
311         PrintUtils.serializeIntoFile(onSignatureFile, ppListOfListListOn);
312         PrintUtils.serializeIntoFile(offSignatureFile, ppListOfListListOff);
313         // Printing cluster analyses into files
314         PrintUtils.serializeIntoFile(onClusterAnalysisFile, corePointRangeSignatureOn);
315         PrintUtils.serializeIntoFile(offClusterAnalysisFile, corePointRangeSignatureOff);
316
317         // =========================================== SIGNATURE DURATION ===========================================
318         List<Instant> firstSignatureTimestamps = new ArrayList<>();
319         List<Instant> lastSignatureTimestamps = new ArrayList<>();
320         if (!ppListOfListListOn.isEmpty()) {
321             List<List<PcapPacket>> firstListOnSign = ppListOfListListOn.get(0);
322             List<List<PcapPacket>> lastListOnSign = ppListOfListListOn.get(ppListOfListListOn.size() - 1);
323             // Load ON signature first and last packet's timestamps
324             for (List<PcapPacket> list : firstListOnSign) {
325                 // Get timestamp Instant from the last packet
326                 firstSignatureTimestamps.add(list.get(0).getTimestamp());
327             }
328             for (List<PcapPacket> list : lastListOnSign) {
329                 // Get timestamp Instant from the last packet
330                 int lastPacketIndex = list.size() - 1;
331                 lastSignatureTimestamps.add(list.get(lastPacketIndex).getTimestamp());
332             }
333         }
334
335         if (!ppListOfListListOn.isEmpty()) {
336             List<List<PcapPacket>> firstListOffSign = ppListOfListListOff.get(0);
337             List<List<PcapPacket>> lastListOffSign = ppListOfListListOff.get(ppListOfListListOff.size() - 1);
338             // Load OFF signature first and last packet's timestamps
339             for (List<PcapPacket> list : firstListOffSign) {
340                 // Get timestamp Instant from the last packet
341                 firstSignatureTimestamps.add(list.get(0).getTimestamp());
342             }
343             for (List<PcapPacket> list : lastListOffSign) {
344                 // Get timestamp Instant from the last packet
345                 int lastPacketIndex = list.size() - 1;
346                 lastSignatureTimestamps.add(list.get(lastPacketIndex).getTimestamp());
347             }
348         }
349         // Sort the timestamps
350         firstSignatureTimestamps.sort((p1, p2) -> {
351             return p1.compareTo(p2);
352         });
353         // Sort the timestamps
354         lastSignatureTimestamps.sort((p1, p2) -> {
355             return p1.compareTo(p2);
356         });
357
358         Iterator<Instant> iterFirst = firstSignatureTimestamps.iterator();
359         Iterator<Instant> iterLast = lastSignatureTimestamps.iterator();
360         long duration;
361         long maxDuration = Long.MIN_VALUE;
362         PrintWriterUtils.println("========================================", resultsWriter,
363                 DUPLICATE_OUTPUT_TO_STD_OUT);
364         PrintWriterUtils.println("           Signature Durations          ", resultsWriter,
365                 DUPLICATE_OUTPUT_TO_STD_OUT);
366         PrintWriterUtils.println("========================================", resultsWriter,
367                 DUPLICATE_OUTPUT_TO_STD_OUT);
368         while (iterFirst.hasNext() && iterLast.hasNext()) {
369             Instant firstInst = (Instant) iterFirst.next();
370             Instant lastInst = (Instant) iterLast.next();
371             Duration dur = Duration.between(firstInst, lastInst);
372             duration = dur.toMillis();
373             // Check duration --- should be below 15 seconds
374             if (duration > TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS) {
375                 while (duration > TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS && iterFirst.hasNext()) {
376                     // that means we have to move to the next trigger
377                     firstInst = (Instant) iterFirst.next();
378                     dur = Duration.between(firstInst, lastInst);
379                     duration = dur.toMillis();
380                 }
381             } else { // Below 0/Negative --- that means we have to move to the next signature
382                 while (duration < 0 && iterLast.hasNext()) {
383                     // that means we have to move to the next trigger
384                     lastInst = (Instant) iterLast.next();
385                     dur = Duration.between(firstInst, lastInst);
386                     duration = dur.toMillis();
387                 }
388             }
389             PrintWriterUtils.println(duration, resultsWriter, DUPLICATE_OUTPUT_TO_STD_OUT);
390             // Update duration if this bigger than the max value and still less than the window inclusion time
391             maxDuration = maxDuration < duration && duration <= TriggerTrafficExtractor.INCLUSION_WINDOW_MILLIS ?
392                     duration : maxDuration;
393         }
394         // Just assign the value 0 if there is no signature
395         if (maxDuration == Long.MIN_VALUE) {
396             maxDuration = 0;
397         }
398         PrintWriterUtils.println("========================================", resultsWriter,
399                 DUPLICATE_OUTPUT_TO_STD_OUT);
400         PrintWriterUtils.println("Max signature duration: " + maxDuration, resultsWriter,
401                 DUPLICATE_OUTPUT_TO_STD_OUT);
402         PrintWriterUtils.println("========================================", resultsWriter,
403                 DUPLICATE_OUTPUT_TO_STD_OUT);
404         resultsWriter.flush();
405         resultsWriter.close();
406         // ==========================================================================================================
407     }
408 }