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