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