b9f3e07b1c227c4620372363e9461a44ed687895
[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.TcpPacket;
7
8 import java.util.*;
9
10 /**
11  * Models a (TCP) conversation/connection/session/flow (packet's belonging to the same session between a client and a
12  * server).
13  * Holds a list of {@link PcapPacket}s identified as pertaining to the flow. Note that this list is <em>not</em>
14  * considered when determining equality of two {@code Conversation} instances in order to allow for a
15  * {@code Conversation} to function as a key in data structures such as {@link java.util.Map} and {@link java.util.Set}.
16  * See {@link #equals(Object)} for the definition of equality.
17  *
18  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
19  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
20  */
21 public class Conversation {
22
23     /* Begin instance properties */
24     /**
25      * The IP of the host that is considered the client (i.e. the host that initiates the conversation)
26      * in this conversation.
27      */
28     private final String mClientIp;
29
30     /**
31      * The port number used by the host that is considered the client in this conversation.
32      */
33     private final int mClientPort;
34
35     /**
36      * The IP of the host that is considered the server (i.e. is the responder) in this conversation.
37      */
38     private final String mServerIp;
39
40     /**
41      * The port number used by the server in this conversation.
42      */
43     private final int mServerPort;
44
45     /**
46      * The list of packets (with payload) pertaining to this conversation.
47      */
48     private final List<PcapPacket> mPackets;
49
50     /**
51      * Contains the sequence numbers used thus far by the host that is considered the <em>client</em> in this
52      * {@code Conversation}.
53      * Used for filtering out retransmissions.
54      */
55     private final Set<Integer> mSeqNumbersClient;
56
57     /**
58      * Contains the sequence numbers used thus far by the host that is considered the <em>server</em> in this
59      * {@code Conversation}.
60      * Used for filtering out retransmissions.
61      */
62     private final Set<Integer> mSeqNumbersSrv;
63
64     /**
65      * List of SYN packets pertaining to this conversation.
66      */
67     private List<PcapPacket> mSynPackets;
68
69     /**
70      * List of pairs FINs and their corresponding ACKs associated with this conversation.
71      */
72     private List<FinAckPair> mFinPackets;
73     /* End instance properties */
74
75     /**
76      * Factory method for creating a {@code Conversation} from a {@link PcapPacket}.
77      * @param pcapPacket The {@code PcapPacket} that wraps a TCP segment for which a {@code Conversation} is to be initiated.
78      * @param clientIsSrc If {@code true}, the source address and source port found in the IP datagram and TCP segment
79      *                    wrapped in the {@code PcapPacket} are regarded as pertaining to the client, and the destination
80      *                    address and destination port are regarded as pertaining to the server---and vice versa if set
81      *                    to {@code false}.
82      * @return A {@code Conversation} initiated with ip:port for client and server according to the direction of the packet.
83      */
84     public static Conversation fromPcapPacket(PcapPacket pcapPacket, boolean clientIsSrc) {
85         IpV4Packet ipPacket = pcapPacket.get(IpV4Packet.class);
86         TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
87         String clientIp = clientIsSrc ? ipPacket.getHeader().getSrcAddr().getHostAddress() :
88                 ipPacket.getHeader().getDstAddr().getHostAddress();
89         String srvIp = clientIsSrc ? ipPacket.getHeader().getDstAddr().getHostAddress() :
90                 ipPacket.getHeader().getSrcAddr().getHostAddress();
91         int clientPort = clientIsSrc ? tcpPacket.getHeader().getSrcPort().valueAsInt() :
92                 tcpPacket.getHeader().getDstPort().valueAsInt();
93         int srvPort = clientIsSrc ? tcpPacket.getHeader().getDstPort().valueAsInt() :
94                 tcpPacket.getHeader().getSrcPort().valueAsInt();
95         return new Conversation(clientIp, clientPort, srvIp, srvPort);
96     }
97
98     /**
99      * Constructs a new {@code Conversation}.
100      * @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation)
101      *                 in the conversation.
102      * @param clientPort The port number used by the client for the conversation.
103      * @param serverIp The IP of the host that is considered the server (i.e. is the responder) in the conversation.
104      * @param serverPort The port number used by the server for the conversation.
105      */
106     public Conversation(String clientIp, int clientPort, String serverIp, int serverPort) {
107         this.mClientIp = clientIp;
108         this.mClientPort = clientPort;
109         this.mServerIp = serverIp;
110         this.mServerPort = serverPort;
111         this.mPackets = new ArrayList<>();
112         this.mSeqNumbersClient = new HashSet<>();
113         this.mSeqNumbersSrv = new HashSet<>();
114         this.mSynPackets = new ArrayList<>();
115         this.mFinPackets = new ArrayList<>();
116     }
117
118     /**
119      * Add a packet to the list of packets associated with this conversation.
120      * @param packet The packet that is to be added to (associated with) this conversation.
121      * @param ignoreRetransmissions Boolean value indicating if retransmissions should be ignored.
122      *                              If set to {@code true}, {@code packet} will <em>not</em> be added to the
123      *                              internal list of packets pertaining to this {@code Conversation}
124      *                              <em>iff</em> the sequence number of {@code packet} was already
125      *                              seen in a previous packet.
126      */
127     public void addPacket(PcapPacket packet, boolean ignoreRetransmissions) {
128         // Precondition: verify that packet does indeed pertain to conversation.
129         onAddPrecondition(packet);
130         if (ignoreRetransmissions && isRetransmission(packet)) {
131             // Packet is a retransmission. Ignore it.
132             return;
133         }
134         // Select direction-dependent set of sequence numbers seen so far and update it with sequence number of new packet.
135         addSeqNumber(packet);
136         // Finally add packet to list of packets pertaining to this conversation.
137         mPackets.add(packet);
138     }
139
140     /**
141      * Get a list of packets pertaining to this {@code Conversation}.
142      * The returned list is a read-only list.
143      * @return the list of packets pertaining to this {@code Conversation}.
144      */
145     public List<PcapPacket> getPackets() {
146         // Return read-only view to prevent external code from manipulating internal state (preserve invariant).
147         return Collections.unmodifiableList(mPackets);
148     }
149
150     /**
151      * Records a TCP SYN packet as pertaining to this conversation (adds it to the the internal list).
152      * Attempts to add duplicate SYN packets will be ignored, and the caller is made aware of the attempt to add a
153      * duplicate by the return value being {@code false}.
154      *
155      * @param synPacket A {@link PcapPacket} wrapping a TCP SYN packet.
156      * @return {@code true} if the packet was successfully added to this {@code Conversation}, {@code false} otherwise.
157      */
158     public boolean addSynPacket(PcapPacket synPacket) {
159         onAddPrecondition(synPacket);
160         final IpV4Packet synPacketIpSection = synPacket.get(IpV4Packet.class);
161         final TcpPacket synPacketTcpSection = synPacket.get(TcpPacket.class);
162         if (synPacketTcpSection == null || !synPacketTcpSection.getHeader().getSyn()) {
163             throw new IllegalArgumentException("Not a SYN packet.");
164         }
165         // We are only interested in recording one copy of the two SYN packets (one SYN packet in each direction), i.e.,
166         // we want to discard retransmitted SYN packets.
167         if (mSynPackets.size() >= 2) {
168             return false;
169         }
170         // Check the set of recorded SYN packets to see if we have already recorded a SYN packet going in the same
171         // direction as the packet given in the argument.
172         boolean matchingPrevSyn = mSynPackets.stream().anyMatch(p -> {
173             IpV4Packet pIp = p.get(IpV4Packet.class);
174             TcpPacket pTcp = p.get(TcpPacket.class);
175             boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
176                     equals(pIp.getHeader().getSrcAddr().getHostAddress());
177             boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
178                     equals(pIp.getHeader().getDstAddr().getHostAddress());
179             boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
180                     pTcp.getHeader().getSrcPort().valueAsInt();
181             boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().valueAsInt() ==
182                     pTcp.getHeader().getDstPort().valueAsInt();
183             return srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
184         });
185         if (matchingPrevSyn) {
186             return false;
187         }
188         // Update direction-dependent set of sequence numbers and record/log packet.
189         addSeqNumber(synPacket);
190         return mSynPackets.add(synPacket);
191
192         /*
193         mSynPackets.stream().anyMatch(p -> {
194             IpV4Packet pIp = p.get(IpV4Packet.class);
195             TcpPacket pTcp = p.get(TcpPacket.class);
196             boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
197                     equals(pIp.getHeader().getSrcAddr().getHostAddress());
198             boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
199                     equals(pIp.getHeader().getDstAddr().getHostAddress());
200             boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
201                     pTcp.getHeader().getSrcPort().valueAsInt();
202             boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().value() ==
203                     pTcp.getHeader().getDstPort().value();
204
205             boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
206
207             boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() ==
208                     pTcp.getHeader().getSequenceNumber();
209
210             if (fourTupleMatch && !seqNoMatch) {
211                 // If the four tuple that identifies the conversation matches, but the sequence number is different,
212                 // it means that this SYN packet is, in fact, an attempt to establish a **new** connection, and hence
213                 // the given packet is NOT part of this conversation, even though the ip:port combinations are (by
214                 // chance) selected such that they match this conversation.
215                 throw new IllegalArgumentException("Attempt to add SYN packet that belongs to a different conversation " +
216                         "(which is identified by the same four tuple as this conversation)");
217             }
218             return fourTupleMatch && seqNoMatch;
219         });
220         */
221     }
222
223     /**
224      * Get a list of SYN packets pertaining to this {@code Conversation}.
225      * The returned list is a read-only list.
226      * @return the list of SYN packets pertaining to this {@code Conversation}.
227      */
228     public List<PcapPacket> getSynPackets() {
229         return Collections.unmodifiableList(mSynPackets);
230     }
231
232     /**
233      * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
234      * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
235      */
236     public void addFinPacket(PcapPacket finPacket) {
237         // Precondition: verify that packet does indeed pertain to conversation.
238         onAddPrecondition(finPacket);
239         // TODO: should call addSeqNumber here?
240         addSeqNumber(finPacket);
241         mFinPackets.add(new FinAckPair(finPacket));
242     }
243
244     /**
245      * Attempt to ACK any FIN packets held by this conversation.
246      * @param ackPacket The ACK for a FIN previously added to this conversation.
247      */
248     public void attemptAcknowledgementOfFin(PcapPacket ackPacket) {
249         // Precondition: verify that the packet pertains to this conversation.
250         onAddPrecondition(ackPacket);
251         // Mark unack'ed FIN(s) that this ACK matches as ACK'ed (there might be more than one in case of retransmissions..?)
252         mFinPackets.replaceAll(finAckPair -> !finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
253     }
254
255     /**
256      * Retrieves an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
257      * @return an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
258      */
259     public List<FinAckPair> getFinAckPairs() {
260         return Collections.unmodifiableList(mFinPackets);
261     }
262
263     /**
264      * Get if this {@code Conversation} is considered to have been gracefully shut down.
265      * A {@code Conversation} has been gracefully shut down if it contains a FIN+ACK pair for both directions
266      * (client to server, and server to client).
267      * @return {@code true} if the connection has been gracefully shut down, false otherwise.
268      */
269     public boolean isGracefullyShutdown() {
270         //  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.
271         return mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mClientIp, mClientPort)) &&
272                 mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
273     }
274
275     // =========================================================================================================
276     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
277     // in a Map.
278
279     /**
280      * <em>Note:</em> currently, equality is determined based on pairwise equality of the elements of the four tuple
281      * ({@link #mClientIp}, {@link #mClientPort}, {@link #mServerIp}, {@link #mServerPort}) for {@code this} and
282      * {@code obj}.
283      * @param obj The object to test for equality with {@code this}.
284      * @return {@code true} if {@code obj} is considered equal to {@code this} based on the definition of equality given above.
285      */
286     @Override
287     public boolean equals(Object obj) {
288         return obj instanceof Conversation && this.toString().equals(obj.toString());
289     }
290
291     @Override
292     public int hashCode() {
293         return toString().hashCode();
294     }
295     // =========================================================================================================
296
297     @Override
298     public String toString() {
299         return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
300     }
301
302     /**
303      * Invoke to verify that the precondition holds when a caller attempts to add a packet to this {@code Conversation}.
304      * An {@link IllegalArgumentException} is thrown if the precondition is violated.
305      * @param packet the packet to be added to this {@code Conversation}
306      */
307     private void onAddPrecondition(PcapPacket packet) {
308         // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
309         // defines the conversation.
310         IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
311         // For now we only support TCP flows.
312         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
313         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
314         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
315         int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
316         int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
317         String clientIp, serverIp;
318         int clientPort, serverPort;
319         if (ipSrc.equals(mClientIp)) {
320             clientIp = ipSrc;
321             clientPort = srcPort;
322             serverIp = ipDst;
323             serverPort = dstPort;
324         } else {
325             clientIp = ipDst;
326             clientPort = dstPort;
327             serverIp = ipSrc;
328             serverPort = srcPort;
329         }
330         if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
331                 serverIp.equals(mServerIp) && serverPort == mServerPort)) {
332             throw new IllegalArgumentException(
333                     String.format("Attempt to add packet that does not pertain to %s",
334                             Conversation.class.getSimpleName()));
335         }
336     }
337
338     /**
339      * <p>
340      *      Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
341      *      packet.
342      * </p>
343      *
344      * <b>
345      *     TODO:
346      *     the current implementation, which uses a set of previously seen sequence numbers, will consider a segment
347      *     with a reused sequence number---occurring as a result of sequence number wrap around for a very long-lived
348      *     connection---as a retransmission (and may therefore end up discarding it even though it is in fact NOT a
349      *     retransmission). Ideas?
350      * </b>
351      *
352      * @param packet The packet.
353      * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
354      */
355     public boolean isRetransmission(PcapPacket packet) {
356         // Extract sequence number.
357         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
358         switch (getDirection(packet)) {
359             case CLIENT_TO_SERVER:
360                 return mSeqNumbersClient.contains(seqNo);
361             case SERVER_TO_CLIENT:
362                 return mSeqNumbersSrv.contains(seqNo);
363             default:
364                 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
365                         Direction.class.getSimpleName()));
366         }
367     }
368
369     /**
370      * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by
371      * analyzing the direction of the packet.
372      * @param packet A TCP packet (wrapped in a {@code PcapPacket}) that was added to this conversation and whose
373      *               sequence number is to be recorded as seen.
374      */
375     private void addSeqNumber(PcapPacket packet) {
376         // Note: below check is redundant if client code is correct as the call to check the precondition should already
377         // have been made by the addXPacket method that invokes this method. As such, the call below may be removed in
378         // favor of speed, but the improvement will be minor, hence the added safety may be worth it.
379         onAddPrecondition(packet);
380         // Extract sequence number.
381         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
382         // Determine direction of packet and add packet's sequence number to corresponding set of sequence numbers.
383         switch (getDirection(packet)) {
384             case CLIENT_TO_SERVER:
385                 // Client to server packet.
386                 mSeqNumbersClient.add(seqNo);
387                 break;
388             case SERVER_TO_CLIENT:
389                 // Server to client packet.
390                 mSeqNumbersSrv.add(seqNo);
391                 break;
392             default:
393                 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
394                         Direction.class.getSimpleName()));
395         }
396     }
397
398     /**
399      * Determine the direction of {@code packet}.
400      * @param packet The packet whose direction is to be determined.
401      * @return A {@link Direction} indicating the direction of the packet.
402      */
403     private Direction getDirection(PcapPacket packet) {
404         IpV4Packet ipPacket = packet.get(IpV4Packet.class);
405         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
406         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
407         // Determine direction of packet.
408         if (ipSrc.equals(mClientIp) && ipDst.equals(mServerIp)) {
409             // Client to server packet.
410             return Direction.CLIENT_TO_SERVER;
411         } else if (ipSrc.equals(mServerIp) && ipDst.equals(mClientIp)) {
412             // Server to client packet.
413             return Direction.SERVER_TO_CLIENT;
414         } else {
415             throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
416         }
417     }
418
419     /**
420      * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
421      */
422     private enum Direction {
423         CLIENT_TO_SERVER, SERVER_TO_CLIENT
424     }
425
426 }