Checking in new code for signature generation; Arlo has a signature for camera on...
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / Conversation.java
1 package edu.uci.iotproject;
2
3 import edu.uci.iotproject.analysis.TcpConversationUtils;
4 import edu.uci.iotproject.util.PcapPacketUtils;
5 import org.pcap4j.core.PcapPacket;
6 import org.pcap4j.packet.IpV4Packet;
7 import org.pcap4j.packet.Packet;
8 import org.pcap4j.packet.TcpPacket;
9
10 import java.util.*;
11
12 /**
13  * Models a (TCP) conversation/connection/session/flow (packet's belonging to the same session between a client and a
14  * server).
15  * Holds a list of {@link PcapPacket}s identified as pertaining to the flow. Note that this list is <em>not</em>
16  * considered when determining equality of two {@code Conversation} instances in order to allow for a
17  * {@code Conversation} to function as a key in data structures such as {@link java.util.Map} and {@link java.util.Set}.
18  * See {@link #equals(Object)} for the definition of equality.
19  *
20  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
21  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
22  */
23 public class Conversation {
24
25     /* Begin instance properties */
26     /**
27      * The IP of the host that is considered the client (i.e. the host that initiates the conversation)
28      * in this conversation.
29      */
30     private final String mClientIp;
31
32     /**
33      * The port number used by the host that is considered the client in this conversation.
34      */
35     private final int mClientPort;
36
37     /**
38      * The IP of the host that is considered the server (i.e. is the responder) in this conversation.
39      */
40     private final String mServerIp;
41
42     /**
43      * The port number used by the server in this conversation.
44      */
45     private final int mServerPort;
46
47     /**
48      * The list of packets (with payload) pertaining to this conversation.
49      */
50     private final List<PcapPacket> mPackets;
51
52     /**
53      * If {@link #isTls()} is {@code true}, this list contains the subset of {@link #mPackets} which are TLS Application
54      * Data packets.
55      */
56     private final List<PcapPacket> mTlsApplicationDataPackets;
57
58     /**
59      * Contains the sequence numbers used thus far by the host that is considered the <em>client</em> in this
60      * {@code Conversation}.
61      * Used for filtering out retransmissions.
62      */
63     private final Set<Integer> mSeqNumbersClient;
64
65     /**
66      * Contains the sequence numbers used thus far by the host that is considered the <em>server</em> in this
67      * {@code Conversation}.
68      * Used for filtering out retransmissions.
69      */
70     private final Set<Integer> mSeqNumbersSrv;
71
72     /**
73      * List of SYN packets pertaining to this conversation.
74      */
75     private final List<PcapPacket> mSynPackets;
76
77     /**
78      * List of pairs FINs and their corresponding ACKs associated with this conversation.
79      */
80     private final List<FinAckPair> mFinPackets;
81
82     /**
83      * List of RST packets associated with this conversation.
84      */
85     private final List<PcapPacket> mRstPackets;
86
87     /**
88      * Boolean to mark the packet as Application Data based on the previous packet that reaches MTU
89      */
90     private boolean mApplicationData;
91     /* End instance properties */
92
93     /**
94      * Factory method for creating a {@code Conversation} from a {@link PcapPacket}.
95      * @param pcapPacket The {@code PcapPacket} that wraps a TCP segment for which a {@code Conversation} is to be initiated.
96      * @param clientIsSrc If {@code true}, the source address and source port found in the IP datagram and TCP segment
97      *                    wrapped in the {@code PcapPacket} are regarded as pertaining to the client, and the destination
98      *                    address and destination port are regarded as pertaining to the server---and vice versa if set
99      *                    to {@code false}.
100      * @return A {@code Conversation} initiated with ip:port for client and server according to the direction of the packet.
101      */
102     public static Conversation fromPcapPacket(PcapPacket pcapPacket, boolean clientIsSrc) {
103         IpV4Packet ipPacket = pcapPacket.get(IpV4Packet.class);
104         TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
105         String clientIp = clientIsSrc ? ipPacket.getHeader().getSrcAddr().getHostAddress() :
106                 ipPacket.getHeader().getDstAddr().getHostAddress();
107         String srvIp = clientIsSrc ? ipPacket.getHeader().getDstAddr().getHostAddress() :
108                 ipPacket.getHeader().getSrcAddr().getHostAddress();
109         int clientPort = clientIsSrc ? tcpPacket.getHeader().getSrcPort().valueAsInt() :
110                 tcpPacket.getHeader().getDstPort().valueAsInt();
111         int srvPort = clientIsSrc ? tcpPacket.getHeader().getDstPort().valueAsInt() :
112                 tcpPacket.getHeader().getSrcPort().valueAsInt();
113         return new Conversation(clientIp, clientPort, srvIp, srvPort);
114     }
115
116     /**
117      * Constructs a new {@code Conversation}.
118      * @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation)
119      *                 in the conversation.
120      * @param clientPort The port number used by the client for the conversation.
121      * @param serverIp The IP of the host that is considered the server (i.e. is the responder) in the conversation.
122      * @param serverPort The port number used by the server for the conversation.
123      */
124     public Conversation(String clientIp, int clientPort, String serverIp, int serverPort) {
125         this.mClientIp = clientIp;
126         this.mClientPort = clientPort;
127         this.mServerIp = serverIp;
128         this.mServerPort = serverPort;
129         this.mPackets = new ArrayList<>();
130         this.mTlsApplicationDataPackets = new ArrayList<>();
131         this.mSeqNumbersClient = new HashSet<>();
132         this.mSeqNumbersSrv = new HashSet<>();
133         this.mSynPackets = new ArrayList<>();
134         this.mFinPackets = new ArrayList<>();
135         this.mRstPackets = new ArrayList<>();
136         this.mApplicationData = false;
137     }
138
139     /**
140      * Add a packet to the list of packets associated with this conversation.
141      * @param packet The packet that is to be added to (associated with) this conversation.
142      * @param ignoreRetransmissions Boolean value indicating if retransmissions should be ignored.
143      *                              If set to {@code true}, {@code packet} will <em>not</em> be added to the
144      *                              internal list of packets pertaining to this {@code Conversation}
145      *                              <em>iff</em> the sequence number of {@code packet} was already
146      *                              seen in a previous packet.
147      */
148     public void addPacket(PcapPacket packet, boolean ignoreRetransmissions) {
149         // Precondition: verify that packet does indeed pertain to conversation.
150         onAddPrecondition(packet);
151         if (ignoreRetransmissions && isRetransmission(packet)) {
152             // Packet is a retransmission. Ignore it.
153             return;
154         }
155         // Select direction-dependent set of sequence numbers seen so far and update it with sequence number of new packet.
156         addSeqNumber(packet);
157         // Finally add packet to list of packets pertaining to this conversation.
158         mPackets.add(packet);
159         // Preserve order of packets in list: sort according to timestamp.
160         if (mPackets.size() > 1 &&
161                 mPackets.get(mPackets.size()-1).getTimestamp().isBefore(mPackets.get(mPackets.size()-2).getTimestamp())) {
162             Collections.sort(mPackets, (o1, o2) -> {
163                 if (o1.getTimestamp().isBefore(o2.getTimestamp())) { return -1; }
164                 else if (o2.getTimestamp().isBefore(o1.getTimestamp())) { return 1; }
165                 else { return 0; }
166             });
167         }
168         // If TLS, inspect packet to see if it's a TLS Application Data packet, and if so add it to the list of TLS
169         // Application Data packets.
170         if (isTls()) {
171             TcpPacket tcpPacket = packet.get(TcpPacket.class);
172             Packet tcpPayload = tcpPacket.getPayload();
173             if (tcpPayload == null) {
174                 return;
175             }
176             byte[] rawPayload = tcpPayload.getRawData();
177             // The SSL record header is at the front of the payload and is 5 bytes long.
178             // The SSL record header type field (the first byte) is set to 23 if it is an Application Data packet.
179             if (rawPayload != null && rawPayload.length >= 5) {
180                 if (rawPayload[0] == 23) {
181                     mTlsApplicationDataPackets.add(packet);
182                     // Consider the following packet a data packet if this packet's size == MTU size 1448
183                     if (rawPayload.length >= 1448)
184                         mApplicationData = true;
185                 } else if (rawPayload[0] == 20) {
186                     // Do nothing for now - CHANGE_CIPHER_SPEC
187                 } else if (rawPayload[0] == 21) {
188                     // Do nothing for now - ALERT
189                 } else if (rawPayload[0] == 22) {
190                     // Do nothing for now - HANDSHAKE
191                 } else {
192                     // If it is TLS with payload, but rawPayload[0] != 23
193                     if (mApplicationData == true) {
194                         // It is a continuation of the previous packet if the previous packet reaches MTU size 1448 and
195                         // it is not either type 20, 21, or 22
196                         mTlsApplicationDataPackets.add(packet);
197                         if (rawPayload.length < 1448)
198                             mApplicationData = false;
199                     }
200                 }
201             }
202         }
203     }
204
205     /**
206      * Get a list of packets pertaining to this {@code Conversation}.
207      * The returned list is a read-only list.
208      * @return the list of packets pertaining to this {@code Conversation}.
209      */
210     public List<PcapPacket> getPackets() {
211         // Return read-only view to prevent external code from manipulating internal state (preserve invariant).
212         return Collections.unmodifiableList(mPackets);
213     }
214
215     /**
216      * Records a TCP SYN packet as pertaining to this conversation (adds it to the the internal list).
217      * Attempts to add duplicate SYN packets will be ignored, and the caller is made aware of the attempt to add a
218      * duplicate by the return value being {@code false}.
219      *
220      * @param synPacket A {@link PcapPacket} wrapping a TCP SYN packet.
221      * @return {@code true} if the packet was successfully added to this {@code Conversation}, {@code false} otherwise.
222      */
223     public boolean addSynPacket(PcapPacket synPacket) {
224         onAddPrecondition(synPacket);
225         final IpV4Packet synPacketIpSection = synPacket.get(IpV4Packet.class);
226         final TcpPacket synPacketTcpSection = synPacket.get(TcpPacket.class);
227         if (synPacketTcpSection == null || !synPacketTcpSection.getHeader().getSyn()) {
228             throw new IllegalArgumentException("Not a SYN packet.");
229         }
230         // We are only interested in recording one copy of the two SYN packets (one SYN packet in each direction), i.e.,
231         // we want to discard retransmitted SYN packets.
232         if (mSynPackets.size() >= 2) {
233             return false;
234         }
235         // Check the set of recorded SYN packets to see if we have already recorded a SYN packet going in the same
236         // direction as the packet given in the argument.
237         boolean matchingPrevSyn = mSynPackets.stream().anyMatch(p -> {
238             IpV4Packet pIp = p.get(IpV4Packet.class);
239             TcpPacket pTcp = p.get(TcpPacket.class);
240             boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
241                     equals(pIp.getHeader().getSrcAddr().getHostAddress());
242             boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
243                     equals(pIp.getHeader().getDstAddr().getHostAddress());
244             boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
245                     pTcp.getHeader().getSrcPort().valueAsInt();
246             boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().valueAsInt() ==
247                     pTcp.getHeader().getDstPort().valueAsInt();
248             return srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
249         });
250         if (matchingPrevSyn) {
251             return false;
252         }
253         // Update direction-dependent set of sequence numbers and record/log packet.
254         addSeqNumber(synPacket);
255         return mSynPackets.add(synPacket);
256
257         /*
258         mSynPackets.stream().anyMatch(p -> {
259             IpV4Packet pIp = p.get(IpV4Packet.class);
260             TcpPacket pTcp = p.get(TcpPacket.class);
261             boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
262                     equals(pIp.getHeader().getSrcAddr().getHostAddress());
263             boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
264                     equals(pIp.getHeader().getDstAddr().getHostAddress());
265             boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
266                     pTcp.getHeader().getSrcPort().valueAsInt();
267             boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().value() ==
268                     pTcp.getHeader().getDstPort().value();
269
270             boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
271
272             boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() ==
273                     pTcp.getHeader().getSequenceNumber();
274
275             if (fourTupleMatch && !seqNoMatch) {
276                 // If the four tuple that identifies the conversation matches, but the sequence number is different,
277                 // it means that this SYN packet is, in fact, an attempt to establish a **new** connection, and hence
278                 // the given packet is NOT part of this conversation, even though the ip:port combinations are (by
279                 // chance) selected such that they match this conversation.
280                 throw new IllegalArgumentException("Attempt to add SYN packet that belongs to a different conversation " +
281                         "(which is identified by the same four tuple as this conversation)");
282             }
283             return fourTupleMatch && seqNoMatch;
284         });
285         */
286     }
287
288     /**
289      * Get a list of SYN packets pertaining to this {@code Conversation}.
290      * The returned list is a read-only list.
291      * @return the list of SYN packets pertaining to this {@code Conversation}.
292      */
293     public List<PcapPacket> getSynPackets() {
294         return Collections.unmodifiableList(mSynPackets);
295     }
296
297     /**
298      * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
299      * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
300      */
301     public void addFinPacket(PcapPacket finPacket) {
302         // Precondition: verify that packet does indeed pertain to conversation.
303         onAddPrecondition(finPacket);
304         // TODO: should call addSeqNumber here?
305         addSeqNumber(finPacket);
306         mFinPackets.add(new FinAckPair(finPacket));
307     }
308
309     /**
310      * Attempt to ACK any FIN packets held by this conversation.
311      * @param ackPacket The ACK for a FIN previously added to this conversation.
312      */
313     public void attemptAcknowledgementOfFin(PcapPacket ackPacket) {
314         // Precondition: verify that the packet pertains to this conversation.
315         onAddPrecondition(ackPacket);
316         // Mark unack'ed FIN(s) that this ACK matches as ACK'ed (there might be more than one in case of retransmissions..?)
317         mFinPackets.replaceAll(finAckPair -> !finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
318     }
319
320     /**
321      * Retrieves an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
322      * @return an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
323      */
324     public List<FinAckPair> getFinAckPairs() {
325         return Collections.unmodifiableList(mFinPackets);
326     }
327
328     /**
329      * Get if this {@code Conversation} is considered to have been gracefully shut down.
330      * A {@code Conversation} has been gracefully shut down if it contains a FIN+ACK pair for both directions
331      * (client to server, and server to client).
332      * @return {@code true} if the connection has been gracefully shut down, false otherwise.
333      */
334     public boolean isGracefullyShutdown() {
335         //  The conversation has been gracefully shut down if we have recorded a FIN from both the client and the server which have both been ack'ed.
336         return mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mClientIp, mClientPort)) &&
337                 mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
338     }
339
340     /**
341      * Add a TCP segment for which the RST flag is set to this {@code Conversation}.
342      * @param packet A {@link PcapPacket} wrapping a TCP segment pertaining to this {@code Conversation} for which the
343      *               RST flag is set.
344      */
345     public void addRstPacket(PcapPacket packet) {
346         /*
347          * TODO:
348          * When now also keeping track of RST packets, should we also...?
349          * 1) Prevent later packets from being added once a RST segment has been added?
350          * 2) Extend 'isGracefullyShutdown()' to also consider RST segments, or add another method, 'isShutdown()' that
351          *    both considers FIN/ACK (graceful) as well as RST (abrupt/"ungraceful") shutdown?
352          * 3) Should it be impossible to associate more than one RST segment with each Conversation?
353          */
354         onAddPrecondition(packet);
355         TcpPacket tcpPacket = packet.get(TcpPacket.class);
356         if (tcpPacket == null || !tcpPacket.getHeader().getRst()) {
357             throw new IllegalArgumentException("not a RST packet");
358         }
359         mRstPackets.add(packet);
360     }
361
362     /**
363      * Get the TCP segments pertaining to this {@code Conversation} for which it was detected that the RST flag is set.
364      * @return the TCP segments pertaining to this {@code Conversation} for which it was detected that the RST flag is
365      *         set.
366      */
367     public List<PcapPacket> getRstPackets() {
368         return Collections.unmodifiableList(mRstPackets);
369     }
370
371     // =========================================================================================================
372     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
373     // in a Map.
374
375     /**
376      * <em>Note:</em> currently, equality is determined based on pairwise equality of the elements of the four tuple
377      * ({@link #mClientIp}, {@link #mClientPort}, {@link #mServerIp}, {@link #mServerPort}) for {@code this} and
378      * {@code obj}.
379      * @param obj The object to test for equality with {@code this}.
380      * @return {@code true} if {@code obj} is considered equal to {@code this} based on the definition of equality given above.
381      */
382     @Override
383     public boolean equals(Object obj) {
384         return obj instanceof Conversation && this.toString().equals(obj.toString());
385     }
386
387     @Override
388     public int hashCode() {
389         return toString().hashCode();
390     }
391     // =========================================================================================================
392
393     @Override
394     public String toString() {
395         return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
396     }
397
398     /**
399      * Invoke to verify that the precondition holds when a caller attempts to add a packet to this {@code Conversation}.
400      * An {@link IllegalArgumentException} is thrown if the precondition is violated.
401      * @param packet the packet to be added to this {@code Conversation}
402      */
403     private void onAddPrecondition(PcapPacket packet) {
404         // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
405         // defines the conversation.
406         IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
407         // For now we only support TCP flows.
408         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
409         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
410         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
411         int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
412         int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
413         String clientIp, serverIp;
414         int clientPort, serverPort;
415         if (ipSrc.equals(mClientIp)) {
416             clientIp = ipSrc;
417             clientPort = srcPort;
418             serverIp = ipDst;
419             serverPort = dstPort;
420         } else {
421             clientIp = ipDst;
422             clientPort = dstPort;
423             serverIp = ipSrc;
424             serverPort = srcPort;
425         }
426         if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
427                 serverIp.equals(mServerIp) && serverPort == mServerPort)) {
428             throw new IllegalArgumentException(
429                     String.format("Attempt to add packet that does not pertain to %s",
430                             Conversation.class.getSimpleName()));
431         }
432     }
433
434     /**
435      * <p>
436      *      Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
437      *      packet.
438      * </p>
439      *
440      * <b>
441      *     TODO:
442      *     the current implementation, which uses a set of previously seen sequence numbers, will consider a segment
443      *     with a reused sequence number---occurring as a result of sequence number wrap around for a very long-lived
444      *     connection---as a retransmission (and may therefore end up discarding it even though it is in fact NOT a
445      *     retransmission). Ideas?
446      * </b>
447      *
448      * @param packet The packet.
449      * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
450      */
451     public boolean isRetransmission(PcapPacket packet) {
452         // Extract sequence number.
453         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
454         switch (getDirection(packet)) {
455             case CLIENT_TO_SERVER:
456                 return mSeqNumbersClient.contains(seqNo);
457             case SERVER_TO_CLIENT:
458                 return mSeqNumbersSrv.contains(seqNo);
459             default:
460                 throw new AssertionError(String.format("Unexpected value of enum '%s'",
461                         Direction.class.getSimpleName()));
462         }
463     }
464
465     /**
466      * <p>
467      *     Is this {@code Conversation} a TLS session?
468      * </p>
469      *
470      * <em>Note: the current implementation simply examines the port number(s) for 443; it does <b>not</b> verify if the
471      * application data is indeed encrypted.</em>
472      *
473      * @return {@code true} if this {@code Conversation} is interpreted as a TLS session, {@code false} otherwise.
474      */
475     public boolean isTls() {
476         /*
477          * TODO:
478          * - may want to change this to be "return mServerPort == 443 || mClientPort == 443;" in order to also detect
479          *   TLS in those cases where it is not possible to correctly label who is the client and who is the server,
480          *   i.e., when the trace does not contain the SYN/SYNACK exchange.
481          * - current implementation relies on the server using the conventional TLS port number; may instead want to
482          *   inspect the first 4 bytes of each potential TLS packet to see if they match the SSL record header.
483          *
484          * 08/31/18: Added unconvetional TLS ports used by WeMo plugs and LiFX bulb.
485          * 09/20/18: Moved hardcoded ports to other class to allow other classes to query the set of TLS ports.
486          */
487         return TcpConversationUtils.isTlsPort(mServerPort) || TcpConversationUtils.isTlsPort(mClientPort);
488     }
489
490     /**
491      * If this {@code Conversation} is backing a TLS session (i.e., if the value of {@link #isTls()} is {@code true}),
492      * get the packets labeled as TLS Application Data packets. This is a subset of the full set of payload-carrying
493      * packets (as returned by {@link #getPackets()}). An exception is thrown if this method is invoked on a
494      * {@code Conversation} for which {@link #isTls()} returns {@code false}.
495      *
496      * @return A list containing exactly those packets that could be identified as TLS Application Data packets (through
497      *         inspecting of the SSL record header). The list may be empty, if no TLS application data packets have been
498      *         recorded for this {@code Conversation}.
499      */
500     public List<PcapPacket> getTlsApplicationDataPackets() {
501         if (!isTls()) {
502             throw new NoSuchElementException("cannot get TLS Application Data packets for non-TLS TCP conversation");
503         }
504         return Collections.unmodifiableList(mTlsApplicationDataPackets);
505     }
506
507     /**
508      * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by
509      * analyzing the direction of the packet.
510      * @param packet A TCP packet (wrapped in a {@code PcapPacket}) that was added to this conversation and whose
511      *               sequence number is to be recorded as seen.
512      */
513     private void addSeqNumber(PcapPacket packet) {
514         // Note: below check is redundant if client code is correct as the call to check the precondition should already
515         // have been made by the addXPacket method that invokes this method. As such, the call below may be removed in
516         // favor of speed, but the improvement will be minor, hence the added safety may be worth it.
517         onAddPrecondition(packet);
518         // Extract sequence number.
519         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
520         // Determine direction of packet and add packet's sequence number to corresponding set of sequence numbers.
521         switch (getDirection(packet)) {
522             case CLIENT_TO_SERVER:
523                 // Client to server packet.
524                 mSeqNumbersClient.add(seqNo);
525                 break;
526             case SERVER_TO_CLIENT:
527                 // Server to client packet.
528                 mSeqNumbersSrv.add(seqNo);
529                 break;
530             default:
531                 throw new AssertionError(String.format("Unexpected value of enum '%s'",
532                         Direction.class.getSimpleName()));
533         }
534     }
535
536     /**
537      * Determine the direction of {@code packet}. An {@link IllegalArgumentException} is thrown if {@code packet} does
538      * not pertain to this conversation.
539      *
540      * @param packet The packet whose direction is to be determined.
541      * @return A {@link Direction} indicating the direction of the packet.
542      */
543     public Direction getDirection(PcapPacket packet) {
544         IpV4Packet ipPacket = packet.get(IpV4Packet.class);
545         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
546         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
547         // Determine direction of packet.
548         if (ipSrc.equals(mClientIp) && ipDst.equals(mServerIp)) {
549             // Client to server packet.
550             return Direction.CLIENT_TO_SERVER;
551         } else if (ipSrc.equals(mServerIp) && ipDst.equals(mClientIp)) {
552             // Server to client packet.
553             return Direction.SERVER_TO_CLIENT;
554         } else {
555             throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
556         }
557     }
558
559     /**
560      * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
561      */
562     public enum Direction {
563
564         CLIENT_TO_SERVER {
565             @Override
566             public String toCompactString() {
567                 return "*";
568             }
569         },
570         SERVER_TO_CLIENT {
571             @Override
572             public String toCompactString() {
573                 return "";
574             }
575         };
576
577         /**
578          * Get a compact string representation of this {@code Direction}.
579          * @return a compact string representation of this {@code Direction}.
580          */
581         abstract public String toCompactString();
582
583     }
584
585 }