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