1 package edu.uci.iotproject;
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;
11 * Models a (TCP) conversation/connection/session/flow (packet's belonging to the same session between a client and a
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.
18 * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
19 * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
21 public class Conversation {
23 /* Begin instance properties */
25 * The IP of the host that is considered the client (i.e. the host that initiates the conversation)
26 * in this conversation.
28 private final String mClientIp;
31 * The port number used by the host that is considered the client in this conversation.
33 private final int mClientPort;
36 * The IP of the host that is considered the server (i.e. is the responder) in this conversation.
38 private final String mServerIp;
41 * The port number used by the server in this conversation.
43 private final int mServerPort;
46 * The list of packets (with payload) pertaining to this conversation.
48 private final List<PcapPacket> mPackets;
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.
55 private final Set<Integer> mSeqNumbersClient;
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.
62 private final Set<Integer> mSeqNumbersSrv;
66 * List of pairs FINs and their corresponding ACKs associated with this conversation.
68 private List<FinAckPair> mFinPackets;
69 /* End instance properties */
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.
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<>();
86 this.mSeqNumbersClient = new HashSet<>();
87 this.mSeqNumbersSrv = new HashSet<>();
89 this.mFinPackets = new ArrayList<>();
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.
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.
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);
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}.
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);
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.
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));
135 * Attempt to ACK any FIN packets held by this conversation.
136 * @param ackPacket The ACK for a FIN previously added to this conversation.
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);
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}.
149 public List<FinAckPair> getFinAckPairs() {
150 return Collections.unmodifiableList(mFinPackets);
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.
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));
165 // =========================================================================================================
166 // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
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
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.
177 public boolean equals(Object obj) {
178 return obj instanceof Conversation && this.toString().equals(obj.toString());
182 public int hashCode() {
183 return toString().hashCode();
185 // =========================================================================================================
188 public String toString() {
189 return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
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}
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)) {
211 clientPort = srcPort;
213 serverPort = dstPort;
216 clientPort = dstPort;
218 serverPort = srcPort;
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()));
230 * Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
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?
242 * @param packet The packet.
243 * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
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);
254 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
255 Direction.class.getSimpleName()));
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.
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);
278 case SERVER_TO_CLIENT:
279 // Server to client packet.
280 mSeqNumbersSrv.add(seqNo);
283 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
284 Direction.class.getSimpleName()));
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.
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;
305 throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
310 * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
312 private enum Direction {
313 CLIENT_TO_SERVER, SERVER_TO_CLIENT