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