Merge branch 'master' of https://github.uci.edu/rtrimana/smart_home_traffic
[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         // Preserve order of packets in list: sort according to timestamp.
139         if (mPackets.size() > 1 &&
140                 mPackets.get(mPackets.size()-1).getTimestamp().isBefore(mPackets.get(mPackets.size()-2).getTimestamp())) {
141             Collections.sort(mPackets, (o1, o2) -> {
142                 if (o1.getTimestamp().isBefore(o2.getTimestamp())) { return -1; }
143                 else if (o2.getTimestamp().isBefore(o1.getTimestamp())) { return 1; }
144                 else { return 0; }
145             });
146         }
147     }
148
149     /**
150      * Get a list of packets pertaining to this {@code Conversation}.
151      * The returned list is a read-only list.
152      * @return the list of packets pertaining to this {@code Conversation}.
153      */
154     public List<PcapPacket> getPackets() {
155         // Return read-only view to prevent external code from manipulating internal state (preserve invariant).
156         return Collections.unmodifiableList(mPackets);
157     }
158
159     /**
160      * Records a TCP SYN packet as pertaining to this conversation (adds it to the the internal list).
161      * Attempts to add duplicate SYN packets will be ignored, and the caller is made aware of the attempt to add a
162      * duplicate by the return value being {@code false}.
163      *
164      * @param synPacket A {@link PcapPacket} wrapping a TCP SYN packet.
165      * @return {@code true} if the packet was successfully added to this {@code Conversation}, {@code false} otherwise.
166      */
167     public boolean addSynPacket(PcapPacket synPacket) {
168         onAddPrecondition(synPacket);
169         final IpV4Packet synPacketIpSection = synPacket.get(IpV4Packet.class);
170         final TcpPacket synPacketTcpSection = synPacket.get(TcpPacket.class);
171         if (synPacketTcpSection == null || !synPacketTcpSection.getHeader().getSyn()) {
172             throw new IllegalArgumentException("Not a SYN packet.");
173         }
174         // We are only interested in recording one copy of the two SYN packets (one SYN packet in each direction), i.e.,
175         // we want to discard retransmitted SYN packets.
176         if (mSynPackets.size() >= 2) {
177             return false;
178         }
179         // Check the set of recorded SYN packets to see if we have already recorded a SYN packet going in the same
180         // direction as the packet given in the argument.
181         boolean matchingPrevSyn = mSynPackets.stream().anyMatch(p -> {
182             IpV4Packet pIp = p.get(IpV4Packet.class);
183             TcpPacket pTcp = p.get(TcpPacket.class);
184             boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
185                     equals(pIp.getHeader().getSrcAddr().getHostAddress());
186             boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
187                     equals(pIp.getHeader().getDstAddr().getHostAddress());
188             boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
189                     pTcp.getHeader().getSrcPort().valueAsInt();
190             boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().valueAsInt() ==
191                     pTcp.getHeader().getDstPort().valueAsInt();
192             return srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
193         });
194         if (matchingPrevSyn) {
195             return false;
196         }
197         // Update direction-dependent set of sequence numbers and record/log packet.
198         addSeqNumber(synPacket);
199         return mSynPackets.add(synPacket);
200
201         /*
202         mSynPackets.stream().anyMatch(p -> {
203             IpV4Packet pIp = p.get(IpV4Packet.class);
204             TcpPacket pTcp = p.get(TcpPacket.class);
205             boolean srcAddrMatch = synPacketIpSection.getHeader().getSrcAddr().getHostAddress().
206                     equals(pIp.getHeader().getSrcAddr().getHostAddress());
207             boolean dstAddrMatch = synPacketIpSection.getHeader().getDstAddr().getHostAddress().
208                     equals(pIp.getHeader().getDstAddr().getHostAddress());
209             boolean srcPortMatch = synPacketTcpSection.getHeader().getSrcPort().valueAsInt() ==
210                     pTcp.getHeader().getSrcPort().valueAsInt();
211             boolean dstPortMatch = synPacketTcpSection.getHeader().getDstPort().value() ==
212                     pTcp.getHeader().getDstPort().value();
213
214             boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
215
216             boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() ==
217                     pTcp.getHeader().getSequenceNumber();
218
219             if (fourTupleMatch && !seqNoMatch) {
220                 // If the four tuple that identifies the conversation matches, but the sequence number is different,
221                 // it means that this SYN packet is, in fact, an attempt to establish a **new** connection, and hence
222                 // the given packet is NOT part of this conversation, even though the ip:port combinations are (by
223                 // chance) selected such that they match this conversation.
224                 throw new IllegalArgumentException("Attempt to add SYN packet that belongs to a different conversation " +
225                         "(which is identified by the same four tuple as this conversation)");
226             }
227             return fourTupleMatch && seqNoMatch;
228         });
229         */
230     }
231
232     /**
233      * Get a list of SYN packets pertaining to this {@code Conversation}.
234      * The returned list is a read-only list.
235      * @return the list of SYN packets pertaining to this {@code Conversation}.
236      */
237     public List<PcapPacket> getSynPackets() {
238         return Collections.unmodifiableList(mSynPackets);
239     }
240
241     /**
242      * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
243      * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
244      */
245     public void addFinPacket(PcapPacket finPacket) {
246         // Precondition: verify that packet does indeed pertain to conversation.
247         onAddPrecondition(finPacket);
248         // TODO: should call addSeqNumber here?
249         addSeqNumber(finPacket);
250         mFinPackets.add(new FinAckPair(finPacket));
251     }
252
253     /**
254      * Attempt to ACK any FIN packets held by this conversation.
255      * @param ackPacket The ACK for a FIN previously added to this conversation.
256      */
257     public void attemptAcknowledgementOfFin(PcapPacket ackPacket) {
258         // Precondition: verify that the packet pertains to this conversation.
259         onAddPrecondition(ackPacket);
260         // Mark unack'ed FIN(s) that this ACK matches as ACK'ed (there might be more than one in case of retransmissions..?)
261         mFinPackets.replaceAll(finAckPair -> !finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
262     }
263
264     /**
265      * Retrieves an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
266      * @return an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
267      */
268     public List<FinAckPair> getFinAckPairs() {
269         return Collections.unmodifiableList(mFinPackets);
270     }
271
272     /**
273      * Get if this {@code Conversation} is considered to have been gracefully shut down.
274      * A {@code Conversation} has been gracefully shut down if it contains a FIN+ACK pair for both directions
275      * (client to server, and server to client).
276      * @return {@code true} if the connection has been gracefully shut down, false otherwise.
277      */
278     public boolean isGracefullyShutdown() {
279         //  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.
280         return mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mClientIp, mClientPort)) &&
281                 mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
282     }
283
284     // =========================================================================================================
285     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
286     // in a Map.
287
288     /**
289      * <em>Note:</em> currently, equality is determined based on pairwise equality of the elements of the four tuple
290      * ({@link #mClientIp}, {@link #mClientPort}, {@link #mServerIp}, {@link #mServerPort}) for {@code this} and
291      * {@code obj}.
292      * @param obj The object to test for equality with {@code this}.
293      * @return {@code true} if {@code obj} is considered equal to {@code this} based on the definition of equality given above.
294      */
295     @Override
296     public boolean equals(Object obj) {
297         return obj instanceof Conversation && this.toString().equals(obj.toString());
298     }
299
300     @Override
301     public int hashCode() {
302         return toString().hashCode();
303     }
304     // =========================================================================================================
305
306     @Override
307     public String toString() {
308         return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
309     }
310
311     /**
312      * Invoke to verify that the precondition holds when a caller attempts to add a packet to this {@code Conversation}.
313      * An {@link IllegalArgumentException} is thrown if the precondition is violated.
314      * @param packet the packet to be added to this {@code Conversation}
315      */
316     private void onAddPrecondition(PcapPacket packet) {
317         // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
318         // defines the conversation.
319         IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
320         // For now we only support TCP flows.
321         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
322         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
323         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
324         int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
325         int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
326         String clientIp, serverIp;
327         int clientPort, serverPort;
328         if (ipSrc.equals(mClientIp)) {
329             clientIp = ipSrc;
330             clientPort = srcPort;
331             serverIp = ipDst;
332             serverPort = dstPort;
333         } else {
334             clientIp = ipDst;
335             clientPort = dstPort;
336             serverIp = ipSrc;
337             serverPort = srcPort;
338         }
339         if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
340                 serverIp.equals(mServerIp) && serverPort == mServerPort)) {
341             throw new IllegalArgumentException(
342                     String.format("Attempt to add packet that does not pertain to %s",
343                             Conversation.class.getSimpleName()));
344         }
345     }
346
347     /**
348      * <p>
349      *      Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
350      *      packet.
351      * </p>
352      *
353      * <b>
354      *     TODO:
355      *     the current implementation, which uses a set of previously seen sequence numbers, will consider a segment
356      *     with a reused sequence number---occurring as a result of sequence number wrap around for a very long-lived
357      *     connection---as a retransmission (and may therefore end up discarding it even though it is in fact NOT a
358      *     retransmission). Ideas?
359      * </b>
360      *
361      * @param packet The packet.
362      * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
363      */
364     public boolean isRetransmission(PcapPacket packet) {
365         // Extract sequence number.
366         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
367         switch (getDirection(packet)) {
368             case CLIENT_TO_SERVER:
369                 return mSeqNumbersClient.contains(seqNo);
370             case SERVER_TO_CLIENT:
371                 return mSeqNumbersSrv.contains(seqNo);
372             default:
373                 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
374                         Direction.class.getSimpleName()));
375         }
376     }
377
378     /**
379      * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by
380      * analyzing the direction of the packet.
381      * @param packet A TCP packet (wrapped in a {@code PcapPacket}) that was added to this conversation and whose
382      *               sequence number is to be recorded as seen.
383      */
384     private void addSeqNumber(PcapPacket packet) {
385         // Note: below check is redundant if client code is correct as the call to check the precondition should already
386         // have been made by the addXPacket method that invokes this method. As such, the call below may be removed in
387         // favor of speed, but the improvement will be minor, hence the added safety may be worth it.
388         onAddPrecondition(packet);
389         // Extract sequence number.
390         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
391         // Determine direction of packet and add packet's sequence number to corresponding set of sequence numbers.
392         switch (getDirection(packet)) {
393             case CLIENT_TO_SERVER:
394                 // Client to server packet.
395                 mSeqNumbersClient.add(seqNo);
396                 break;
397             case SERVER_TO_CLIENT:
398                 // Server to client packet.
399                 mSeqNumbersSrv.add(seqNo);
400                 break;
401             default:
402                 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
403                         Direction.class.getSimpleName()));
404         }
405     }
406
407     /**
408      * Determine the direction of {@code packet}.
409      * @param packet The packet whose direction is to be determined.
410      * @return A {@link Direction} indicating the direction of the packet.
411      */
412     private Direction getDirection(PcapPacket packet) {
413         IpV4Packet ipPacket = packet.get(IpV4Packet.class);
414         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
415         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
416         // Determine direction of packet.
417         if (ipSrc.equals(mClientIp) && ipDst.equals(mServerIp)) {
418             // Client to server packet.
419             return Direction.CLIENT_TO_SERVER;
420         } else if (ipSrc.equals(mServerIp) && ipDst.equals(mClientIp)) {
421             // Server to client packet.
422             return Direction.SERVER_TO_CLIENT;
423         } else {
424             throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
425         }
426     }
427
428     /**
429      * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
430      */
431     private enum Direction {
432         CLIENT_TO_SERVER, SERVER_TO_CLIENT
433     }
434
435 }