TcpConversationUtils.java: add method for grouping conversations by TLS Application...
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / analysis / TcpConversationUtils.java
1 package edu.uci.iotproject.analysis;
2
3 import edu.uci.iotproject.Conversation;
4 import edu.uci.iotproject.DnsMap;
5 import edu.uci.iotproject.FinAckPair;
6 import edu.uci.iotproject.util.PcapPacketUtils;
7 import org.pcap4j.core.PcapPacket;
8 import org.pcap4j.packet.IpV4Packet;
9 import org.pcap4j.packet.TcpPacket;
10
11 import java.util.*;
12 import java.util.stream.Collectors;
13
14 /**
15  * Utility functions for analyzing and structuring (sets of) {@link Conversation}s.
16  *
17  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
18  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
19  */
20 public class TcpConversationUtils {
21
22     /**
23      * <p>
24      *      Given a {@link Conversation}, extract its set of "packet pairs", i.e., pairs of request-reply packets.
25      * </p>
26      *
27      * <b>Note:</b> in the current implementation, if one endpoint sends multiple packets back-to-back with no
28      * interleaved reply packets from the other endpoint, such packets are converted to one-item pairs (i.e., instances
29      * of {@lin PcapPacketPair} where {@link PcapPacketPair#getSecond()} is {@code null}).
30      *
31      * @param conv The {@code Conversation} for which packet pairs are to be extracted.
32      * @return The packet pairs extracted from {@code conv}.
33      */
34     public static List<PcapPacketPair> extractPacketPairs(Conversation conv) {
35         List<PcapPacket> packets = conv.getPackets();
36         List<PcapPacketPair> pairs = new ArrayList<>();
37         int i = 0;
38         while (i < packets.size()) {
39             PcapPacket p1 = packets.get(i);
40             String p1SrcIp = p1.get(IpV4Packet.class).getHeader().getSrcAddr().getHostAddress();
41             int p1SrcPort = p1.get(TcpPacket.class).getHeader().getSrcPort().valueAsInt();
42             if (i+1 < packets.size()) {
43                 PcapPacket p2 = packets.get(i+1);
44                 if (PcapPacketUtils.isSource(p2, p1SrcIp, p1SrcPort)) {
45                     // Two packets in a row going in the same direction -> create one item pair for p1
46                     pairs.add(new PcapPacketPair(p1, null));
47                     // Advance one packet as the following two packets may form a valid two-item pair.
48                     i++;
49                 } else {
50                     // The two packets form a response-reply pair, create two-item pair.
51                     pairs.add(new PcapPacketPair(p1, p2));
52                     // Advance two packets as we have already processed the packet at index i+1 in order to create the pair.
53                     i += 2;
54                 }
55             } else {
56                 // Last packet of conversation => one item pair
57                 pairs.add(new PcapPacketPair(p1, null));
58                 // Advance i to ensure termination.
59                 i++;
60             }
61         }
62         return pairs;
63         // TODO: what if there is long time between response and reply packet? Should we add a threshold and exclude those cases?
64     }
65
66     /**
67      * Given a collection of TCP conversations and associated DNS mappings, groups the conversations by hostname.
68      * @param tcpConversations The collection of TCP conversations.
69      * @param ipHostnameMappings The associated DNS mappings.
70      * @return A map where each key is a hostname and its associated value is a list of conversations where one of the
71      *         two communicating hosts is that hostname (i.e. its IP maps to the hostname).
72      */
73     public static Map<String, List<Conversation>> groupConversationsByHostname(Collection<Conversation> tcpConversations, DnsMap ipHostnameMappings) {
74         HashMap<String, List<Conversation>> result = new HashMap<>();
75         for (Conversation c : tcpConversations) {
76             if (c.getPackets().size() == 0) {
77                 String warningStr = String.format("Detected a %s [%s] with no payload packets.",
78                         c.getClass().getSimpleName(), c.toString());
79                 System.err.println(warningStr);
80                 continue;
81             }
82             IpV4Packet firstPacketIp = c.getPackets().get(0).get(IpV4Packet.class);
83             String ipSrc = firstPacketIp.getHeader().getSrcAddr().getHostAddress();
84             String ipDst = firstPacketIp.getHeader().getDstAddr().getHostAddress();
85             // Check if src or dst IP is associated with one or more hostnames.
86             Set<String> hostnames = ipHostnameMappings.getHostnamesForIp(ipSrc);
87             if (hostnames == null) {
88                 // No luck with src ip (possibly because it's a client->srv packet), try dst ip.
89                 hostnames = ipHostnameMappings.getHostnamesForIp(ipDst);
90             }
91             if (hostnames != null) {
92                 // Put a reference to the conversation for each of the hostnames that the conversation's IP maps to.
93                 for (String hostname : hostnames) {
94                     List<Conversation> newValue = new ArrayList<>();
95                     newValue.add(c);
96                     result.merge(hostname, newValue, (l1, l2) -> { l1.addAll(l2); return l1; });
97                 }
98                 if (hostnames.size() > 1) {
99                     // Print notice of IP mapping to multiple hostnames (debugging)
100                     System.err.println(String.format("%s: encountered an IP that maps to multiple (%d) hostnames",
101                             TcpConversationUtils.class.getSimpleName(), hostnames.size()));
102                 }
103             } else {
104                 // If no hostname mapping, store conversation under the key that is the concatenation of the two IPs.
105                 // In order to ensure consistency when mapping conversations, use lexicographic order to select which IP
106                 // goes first.
107                 String delimiter = "_";
108                 // Note that the in case the comparison returns 0, the strings are equal, so it doesn't matter which of
109                 // ipSrc and ipDst go first (also, this case should not occur in practice as it means that the device is
110                 // communicating with itself!)
111                 String key = ipSrc.compareTo(ipDst) <= 0 ? ipSrc + delimiter + ipDst : ipDst + delimiter + ipSrc;
112                 List<Conversation> newValue = new ArrayList<>();
113                 newValue.add(c);
114                 result.merge(key, newValue, (l1, l2) -> { l1.addAll(l2); return l1; });
115             }
116         }
117         return result;
118     }
119
120     public static Map<String, Integer> countPacketSequenceFrequencies(Collection<Conversation> conversations) {
121         Map<String, Integer> result = new HashMap<>();
122         for (Conversation conv : conversations) {
123             if (conv.getPackets().size() == 0) {
124                 // Skip conversations with no payload packets.
125                 continue;
126             }
127             StringBuilder sb = new StringBuilder();
128             for (PcapPacket pp : conv.getPackets()) {
129                 sb.append(pp.length() + " ");
130             }
131             result.merge(sb.toString(), 1, (i1, i2) -> i1+i2);
132         }
133         return result;
134     }
135
136     /**
137      * Given a {@link Collection} of {@link Conversation}s, builds a {@link Map} from {@link String} to {@link List}
138      * of {@link Conversation}s such that each key is the <em>concatenation of the packet lengths of all payload packets
139      * (i.e., the set of packets returned by {@link Conversation#getPackets()}) separated by a delimiter</em> of any
140      * {@link Conversation} pointed to by that key. In other words, what the {@link Conversation}s {@code cs} pointed to
141      * by the key {@code s} have in common is that they all contain exactly the same number of payload packets <em>and
142      * </em> these payload packets are identical across all {@code Conversation}s in {@code convs} in terms of packet
143      * length and packet order. For example, if the key is "152 440 550", this means that every individual
144      * {@code Conversation} in the list of {@code Conversation}s pointed to by that key contain exactly three payload
145      * packet of lengths 152, 440, and 550, and these three packets are ordered in the order prescribed by the key.
146      * This verbose version prints out the SYNACK, SYN, FINACK, FIN, RST, etc. packets.
147      *
148      * @param conversations The collection of {@code Conversation}s to group by packet sequence.
149      * @return a {@link Map} from {@link String} to {@link List} of {@link Conversation}s such that each key is the
150      *         <em>concatenation of the packet lengths of all payload packets (i.e., the set of packets returned by
151      *         {@link Conversation#getPackets()}) separated by a delimiter</em> of any {@link Conversation} pointed to
152      *         by that key.
153      */
154     public static Map<String, List<Conversation>> groupConversationsByPacketSequenceVerbose(Collection<Conversation> conversations) {
155         Map<String, List<Conversation>> result = new HashMap<>();
156         for (Conversation conv : conversations) {
157             if (conv.getPackets().size() == 0) {
158                 // Skip conversations with no payload packets.
159                 continue;
160             }
161             StringBuilder sb = new StringBuilder();
162             // Add SYN and SYNACK at front of sequence to indicate if we saw the handshake or if recording started in
163             // the middle of the conversation.
164             for (PcapPacket syn : conv.getSynPackets()) {
165                 TcpPacket.TcpHeader tcpHeader = syn.get(TcpPacket.class).getHeader();
166                 if (tcpHeader.getSyn() && tcpHeader.getAck()) {
167                     // Only append a space if there's preceding content.
168                     appendSpaceIfNotEmpty(sb);
169                     sb.append("SYNACK");
170                 } else if (tcpHeader.getSyn()) {
171                     if (sb.length() != 0) {
172                         // If present in the trace, the client's SYN should be at the front of the list, so it should be
173                         // appended as the first item.
174                         throw new AssertionError("StringBuilder had content when appending SYN");
175                     }
176                     sb.append("SYN");
177                 }
178             }
179             // Then append the length of all application data packets.
180             for (PcapPacket pp : conv.getPackets()) {
181                 // Only append a space if there's preceding content.
182                 appendSpaceIfNotEmpty(sb);
183                 sb.append("(" + conv.getDirection(pp).toCompactString() + "_" + pp.length() + ")");
184             }
185             // Then append the logged FINs to indicate if conversation was terminated gracefully.
186             for (FinAckPair fap : conv.getFinAckPairs()) {
187                 appendSpaceIfNotEmpty(sb);
188                 sb.append(fap.isAcknowledged() ? "FINACK" : "FIN");
189             }
190             // Then append the logged RSTs to indicate if conversation was terminated abruptly.
191             for (PcapPacket pp : conv.getRstPackets()) {
192                 appendSpaceIfNotEmpty(sb);
193                 sb.append("RST");
194             }
195             List<Conversation> oneItemList = new ArrayList<>();
196             oneItemList.add(conv);
197             result.merge(sb.toString(), oneItemList, (oldList, newList) -> {
198                 oldList.addAll(newList);
199                 return oldList;
200             });
201         }
202         return result;
203     }
204
205     /**
206      * Given a {@link Collection} of {@link Conversation}s, builds a {@link Map} from {@link String} to {@link List}
207      * of {@link Conversation}s such that each key is the <em>concatenation of the packet lengths of all payload packets
208      * (i.e., the set of packets returned by {@link Conversation#getPackets()}) separated by a delimiter</em> of any
209      * {@link Conversation} pointed to by that key. In other words, what the {@link Conversation}s {@code cs} pointed to
210      * by the key {@code s} have in common is that they all contain exactly the same number of payload packets <em>and
211      * </em> these payload packets are identical across all {@code Conversation}s in {@code convs} in terms of packet
212      * length and packet order. For example, if the key is "152 440 550", this means that every individual
213      * {@code Conversation} in the list of {@code Conversation}s pointed to by that key contain exactly three payload
214      * packet of lengths 152, 440, and 550, and these three packets are ordered in the order prescribed by the key.
215      *
216      * @param conversations The collection of {@code Conversation}s to group by packet sequence.
217      * @return a {@link Map} from {@link String} to {@link List} of {@link Conversation}s such that each key is the
218      *         <em>concatenation of the packet lengths of all payload packets (i.e., the set of packets returned by
219      *         {@link Conversation#getPackets()}) separated by a delimiter</em> of any {@link Conversation} pointed to
220      *         by that key.
221      */
222     public static Map<String, List<Conversation>> groupConversationsByPacketSequence(Collection<Conversation> conversations) {
223         Map<String, List<Conversation>> result = new HashMap<>();
224         for (Conversation conv : conversations) {
225             if (conv.getPackets().size() == 0) {
226                 // Skip conversations with no payload packets.
227                 continue;
228             }
229             StringBuilder sb = new StringBuilder();
230             // Then append the length of all application data packets.
231             for (PcapPacket pp : conv.getPackets()) {
232                 // Only append a space if there's preceding content.
233                 appendSpaceIfNotEmpty(sb);
234                 sb.append(pp.length());
235             }
236             List<Conversation> oneItemList = new ArrayList<>();
237             oneItemList.add(conv);
238             result.merge(sb.toString(), oneItemList, (oldList, newList) -> {
239                 oldList.addAll(newList);
240                 return oldList;
241             });
242         }
243         return result;
244     }
245
246     public static Map<String, List<Conversation>> groupConversationsByTlsApplicationDataPacketSequence(Collection<Conversation> conversations) {
247         return conversations.stream().collect(Collectors.groupingBy(
248                 c -> c.getTlsApplicationDataPackets().stream().map(p -> Integer.toString(p.getOriginalLength())).
249                         reduce("", (s1, s2) -> s1.length() == 0 ? s2 : s1 + " " + s2))
250         );
251     }
252
253     /**
254      * Given a {@link Conversation}, counts the frequencies of each unique packet length seen as part of the
255      * {@code Conversation}.
256      * @param c The {@code Conversation} for which unique packet length frequencies are to be determined.
257      * @return A mapping from packet length to its frequency.
258      */
259     public static Map<Integer, Integer> countPacketLengthFrequencies(Conversation c) {
260         Map<Integer, Integer> result = new HashMap<>();
261         for (PcapPacket packet : c.getPackets()) {
262             result.merge(packet.length(), 1, (i1, i2) -> i1 + i2);
263         }
264         return result;
265     }
266
267     /**
268      * Like {@link #countPacketLengthFrequencies(Conversation)}, but counts packet length frequencies for a collection
269      * of {@code Conversation}s, i.e., the frequency of a packet length becomes the total number of packets with that
270      * length across <em>all</em> {@code Conversation}s in {@code conversations}.
271      * @param conversations The collection of {@code Conversation}s for which packet length frequencies are to be
272      *                      counted.
273      * @return A mapping from packet length to its frequency.
274      */
275     public static Map<Integer, Integer> countPacketLengthFrequencies(Collection<Conversation> conversations) {
276         Map<Integer, Integer> result = new HashMap<>();
277         for (Conversation c : conversations) {
278             Map<Integer, Integer> intermediateResult = countPacketLengthFrequencies(c);
279             for (Map.Entry<Integer, Integer> entry : intermediateResult.entrySet()) {
280                 result.merge(entry.getKey(), entry.getValue(), (i1, i2) -> i1 + i2);
281             }
282         }
283         return result;
284     }
285
286     public static Map<String, Integer> countPacketPairFrequencies(Collection<PcapPacketPair> pairs) {
287         Map<String, Integer> result = new HashMap<>();
288         for (PcapPacketPair ppp : pairs) {
289             result.merge(ppp.toString(), 1, (i1, i2) -> i1 + i2);
290         }
291         return result;
292     }
293
294     public static Map<String, Map<String, Integer>> countPacketPairFrequenciesByHostname(Collection<Conversation> tcpConversations, DnsMap ipHostnameMappings) {
295         Map<String, List<Conversation>> convsByHostname = groupConversationsByHostname(tcpConversations, ipHostnameMappings);
296         HashMap<String, Map<String, Integer>> result = new HashMap<>();
297         for (Map.Entry<String, List<Conversation>> entry : convsByHostname.entrySet()) {
298             // Merge all packet pairs exchanged during the course of all conversations with hostname into one list
299             List<PcapPacketPair> allPairsExchangedWithHostname = new ArrayList<>();
300             entry.getValue().forEach(conversation -> allPairsExchangedWithHostname.addAll(extractPacketPairs(conversation)));
301             // Then count the frequencies of packet pairs exchanged with the hostname, irrespective of individual
302             // conversations
303             result.put(entry.getKey(), countPacketPairFrequencies(allPairsExchangedWithHostname));
304         }
305         return result;
306     }
307
308     /**
309      * Given a {@link Conversation}, extract its packet length sequence.
310      * @param c The {@link Conversation} from which a packet length sequence is to be extracted.
311      * @return An {@code Integer[]} that holds the packet lengths of all payload-carrying packets in {@code c}. The
312      *         packet lengths in the returned array are ordered by packet timestamp.
313      */
314     public static Integer[] getPacketLengthSequence(Conversation c) {
315         return getPacketLengthSequence(c.getPackets());
316     }
317
318
319     /**
320      * Given a {@link Conversation}, extract its packet length sequence, but only include packet lengths of those
321      * packets that carry TLS Application Data.
322      * @param c The {@link Conversation} from which a TLS Application Data packet length sequence is to be extracted.
323      * @return An {@code Integer[]} that holds the packet lengths of all packets in {@code c} that carry TLS Application
324      *         Data. The packet lengths in the returned array are ordered by packet timestamp.
325      */
326     public static Integer[] getPacketLengthSequenceTlsAppDataOnly(Conversation c) {
327         if (!c.isTls()) {
328             throw new IllegalArgumentException("Provided " + c.getClass().getSimpleName() + " was not a TLS session");
329         }
330         return getPacketLengthSequence(c.getTlsApplicationDataPackets());
331     }
332
333     /**
334      * Given a list of packets, extract the packet lengths and wrap them in an array such that the packet lengths in the
335      * resulting array appear in the same order as their corresponding packets in the input list.
336      * @param packets The list of packets for which the packet lengths are to be extracted.
337      * @return An array containing the packet lengths in the same order as their corresponding packets in the input list.
338      */
339     private static Integer[] getPacketLengthSequence(List<PcapPacket> packets) {
340         return packets.stream().map(pkt -> pkt.getOriginalLength()).toArray(Integer[]::new);
341     }
342
343
344     /**
345      * Appends a space to {@code sb} <em>iff</em> {@code sb} already contains some content.
346      * @param sb A {@link StringBuilder} that should have a space appended <em>iff</em> it is not empty.
347      */
348     private static void appendSpaceIfNotEmpty(StringBuilder sb) {
349         if (sb.length() != 0) {
350             sb.append(" ");
351         }
352     }
353 }