Bug-fix: Use seperate, direction-dependent sets of sequence numbers (used when determ...
[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     /**
66      * List of pairs FINs and their corresponding ACKs associated with this conversation.
67      */
68     private List<FinAckPair> mFinPackets;
69     /* End instance properties */
70
71     /**
72      * Constructs a new {@code Conversation}.
73      * @param clientIp The IP of the host that is considered the client (i.e. the host that initiates the conversation)
74      *                 in the conversation.
75      * @param clientPort The port number used by the client for the conversation.
76      * @param serverIp The IP of the host that is considered the server (i.e. is the responder) in the conversation.
77      * @param serverPort The port number used by the server for the conversation.
78      */
79     public Conversation(String clientIp, int clientPort, String serverIp, int serverPort) {
80         this.mClientIp = clientIp;
81         this.mClientPort = clientPort;
82         this.mServerIp = serverIp;
83         this.mServerPort = serverPort;
84         this.mPackets = new ArrayList<>();
85
86         this.mSeqNumbersClient = new HashSet<>();
87         this.mSeqNumbersSrv = new HashSet<>();
88
89         this.mFinPackets = new ArrayList<>();
90     }
91
92     /**
93      * Add a packet to the list of packets associated with this conversation.
94      * @param packet The packet that is to be added to (associated with) this conversation.
95      * @param ignoreRetransmissions Boolean value indicating if retransmissions should be ignored.
96      *                              If set to {@code true}, {@code packet} will <em>not</em> be added to the
97      *                              internal list of packets pertaining to this {@code Conversation}
98      *                              <em>iff</em> the sequence number of {@code packet} was already
99      *                              seen in a previous packet.
100      */
101     public void addPacket(PcapPacket packet, boolean ignoreRetransmissions) {
102         // Precondition: verify that packet does indeed pertain to conversation.
103         onAddPrecondition(packet);
104         if (ignoreRetransmissions && isRetransmission(packet)) {
105             // Packet is a retransmission. Ignore it.
106             return;
107         }
108         // Select direction-dependent set of sequence numbers seen so far and update it with sequence number of new packet.
109         addSeqNumber(packet);
110         // Finally add packet to list of packets pertaining to this conversation.
111         mPackets.add(packet);
112     }
113
114     /**
115      * Get a list of packets pertaining to this {@code Conversation}.
116      * The returned list is a read-only list.
117      * @return the list of packets pertaining to this {@code Conversation}.
118      */
119     public List<PcapPacket> getPackets() {
120         // Return read-only view to prevent external code from manipulating internal state (preserve invariant).
121         return Collections.unmodifiableList(mPackets);
122     }
123
124     /**
125      * Adds a TCP FIN packet to the list of TCP FIN packets associated with this conversation.
126      * @param finPacket The TCP FIN packet that is to be added to (associated with) this conversation.
127      */
128     public void addFinPacket(PcapPacket finPacket) {
129         // Precondition: verify that packet does indeed pertain to conversation.
130         onAddPrecondition(finPacket);
131         mFinPackets.add(new FinAckPair(finPacket));
132     }
133
134     /**
135      * Attempt to ACK any FIN packets held by this conversation.
136      * @param ackPacket The ACK for a FIN previously added to this conversation.
137      */
138     public void attemptAcknowledgementOfFin(PcapPacket ackPacket) {
139         // Precondition: verify that the packet pertains to this conversation.
140         onAddPrecondition(ackPacket);
141         // Mark unack'ed FIN(s) that this ACK matches as ACK'ed (there might be more than one in case of retransmissions..?)
142         mFinPackets.replaceAll(finAckPair -> !finAckPair.isAcknowledged() && finAckPair.isCorrespondingAckPacket(ackPacket) ? new FinAckPair(finAckPair.getFinPacket(), ackPacket) : finAckPair);
143     }
144
145     /**
146      * Retrieves an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
147      * @return an unmodifiable view of the list of {@link FinAckPair}s associated with this {@code Conversation}.
148      */
149     public List<FinAckPair> getFinAckPairs() {
150         return Collections.unmodifiableList(mFinPackets);
151     }
152
153     /**
154      * Get if this {@code Conversation} is considered to have been gracefully shut down.
155      * A {@code Conversation} has been gracefully shut down if it contains a FIN+ACK pair for both directions
156      * (client to server, and server to client).
157      * @return {@code true} if the connection has been gracefully shut down, false otherwise.
158      */
159     public boolean isGracefullyShutdown() {
160         //  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.
161         return mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mClientIp, mClientPort)) &&
162                 mFinPackets.stream().anyMatch(finAckPair -> finAckPair.isAcknowledged() && PcapPacketUtils.isSource(finAckPair.getFinPacket(), mServerIp, mServerPort));
163     }
164
165     // =========================================================================================================
166     // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
167     // in a Map.
168
169     /**
170      * <em>Note:</em> currently, equality is determined based on pairwise equality of the elements of the four tuple
171      * ({@link #mClientIp}, {@link #mClientPort}, {@link #mServerIp}, {@link #mServerPort}) for {@code this} and
172      * {@code obj}.
173      * @param obj The object to test for equality with {@code this}.
174      * @return {@code true} if {@code obj} is considered equal to {@code this} based on the definition of equality given above.
175      */
176     @Override
177     public boolean equals(Object obj) {
178         return obj instanceof Conversation && this.toString().equals(obj.toString());
179     }
180
181     @Override
182     public int hashCode() {
183         return toString().hashCode();
184     }
185     // =========================================================================================================
186
187     @Override
188     public String toString() {
189         return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
190     }
191
192     /**
193      * Invoke to verify that the precondition holds when a caller attempts to add a packet to this {@code Conversation}.
194      * An {@link IllegalArgumentException} is thrown if the precondition is violated.
195      * @param packet the packet to be added to this {@code Conversation}
196      */
197     private void onAddPrecondition(PcapPacket packet) {
198         // Apply precondition to preserve class invariant: all packets in mPackets must match the 4 tuple that
199         // defines the conversation.
200         IpV4Packet ipPacket = Objects.requireNonNull(packet.get(IpV4Packet.class));
201         // For now we only support TCP flows.
202         TcpPacket tcpPacket = Objects.requireNonNull(packet.get(TcpPacket.class));
203         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
204         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
205         int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
206         int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
207         String clientIp, serverIp;
208         int clientPort, serverPort;
209         if (ipSrc.equals(mClientIp)) {
210             clientIp = ipSrc;
211             clientPort = srcPort;
212             serverIp = ipDst;
213             serverPort = dstPort;
214         } else {
215             clientIp = ipDst;
216             clientPort = dstPort;
217             serverIp = ipSrc;
218             serverPort = srcPort;
219         }
220         if (!(clientIp.equals(mClientIp) && clientPort == mClientPort &&
221                 serverIp.equals(mServerIp) && serverPort == mServerPort)) {
222             throw new IllegalArgumentException(
223                     String.format("Attempt to add packet that does not pertain to %s",
224                             Conversation.class.getSimpleName()));
225         }
226     }
227
228     /**
229      * <p>
230      *      Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
231      *      packet.
232      * </p>
233      *
234      * <b>
235      *     TODO:
236      *     the current implementation, which uses a set of previously seen sequence numbers, will consider a segment
237      *     with a reused sequence number---occurring as a result of sequence number wrap around for a very long-lived
238      *     connection---as a retransmission (and may therefore end up discarding it even though it is in fact NOT a
239      *     retransmission). Ideas?
240      * </b>
241      *
242      * @param packet The packet.
243      * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
244      */
245     private boolean isRetransmission(PcapPacket packet) {
246         // Extract sequence number.
247         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
248         switch (getDirection(packet)) {
249             case CLIENT_TO_SERVER:
250                 return mSeqNumbersClient.contains(seqNo);
251             case SERVER_TO_CLIENT:
252                 return mSeqNumbersSrv.contains(seqNo);
253             default:
254                 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
255                         Direction.class.getSimpleName()));
256         }
257     }
258
259     /**
260      * Extracts the TCP sequence number from {@code packet} and adds it to the proper set of sequence numbers by
261      * analyzing the direction of the packet.
262      * @param packet A TCP packet (wrapped in a {@code PcapPacket}) that was added to this conversation and whose
263      *               sequence number is to be recorded as seen.
264      */
265     private void addSeqNumber(PcapPacket packet) {
266         // Note: below check is redundant if client code is correct as the call to check the precondition should already
267         // have been made by the addXPacket method that invokes this method. As such, the call below may be removed in
268         // favor of speed, but the improvement will be minor, hence the added safety may be worth it.
269         onAddPrecondition(packet);
270         // Extract sequence number.
271         int seqNo = packet.get(TcpPacket.class).getHeader().getSequenceNumber();
272         // Determine direction of packet and add packet's sequence number to corresponding set of sequence numbers.
273         switch (getDirection(packet)) {
274             case CLIENT_TO_SERVER:
275                 // Client to server packet.
276                 mSeqNumbersClient.add(seqNo);
277                 break;
278             case SERVER_TO_CLIENT:
279                 // Server to client packet.
280                 mSeqNumbersSrv.add(seqNo);
281                 break;
282             default:
283                 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
284                         Direction.class.getSimpleName()));
285         }
286     }
287
288     /**
289      * Determine the direction of {@code packet}.
290      * @param packet The packet whose direction is to be determined.
291      * @return A {@link Direction} indicating the direction of the packet.
292      */
293     private Direction getDirection(PcapPacket packet) {
294         IpV4Packet ipPacket = packet.get(IpV4Packet.class);
295         String ipSrc = ipPacket.getHeader().getSrcAddr().getHostAddress();
296         String ipDst = ipPacket.getHeader().getDstAddr().getHostAddress();
297         // Determine direction of packet.
298         if (ipSrc.equals(mClientIp) && ipDst.equals(mServerIp)) {
299             // Client to server packet.
300             return Direction.CLIENT_TO_SERVER;
301         } else if (ipSrc.equals(mServerIp) && ipDst.equals(mClientIp)) {
302             // Server to client packet.
303             return Direction.SERVER_TO_CLIENT;
304         } else {
305             throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
306         }
307     }
308
309     /**
310      * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
311      */
312     private enum Direction {
313         CLIENT_TO_SERVER, SERVER_TO_CLIENT
314     }
315
316 }