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;
65 * List of SYN packets pertaining to this conversation.
67 private List<PcapPacket> mSynPackets;
70 * List of pairs FINs and their corresponding ACKs associated with this conversation.
72 private List<FinAckPair> mFinPackets;
73 /* End instance properties */
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
82 * @return A {@code Conversation} initiated with ip:port for client and server according to the direction of the packet.
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);
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.
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<>();
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.
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.
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; }
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}.
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);
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}.
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.
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.");
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) {
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;
194 if (matchingPrevSyn) {
197 // Update direction-dependent set of sequence numbers and record/log packet.
198 addSeqNumber(synPacket);
199 return mSynPackets.add(synPacket);
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();
214 boolean fourTupleMatch = srcAddrMatch && dstAddrMatch && srcPortMatch && dstPortMatch;
216 boolean seqNoMatch = synPacketTcpSection.getHeader().getSequenceNumber() ==
217 pTcp.getHeader().getSequenceNumber();
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)");
227 return fourTupleMatch && seqNoMatch;
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}.
237 public List<PcapPacket> getSynPackets() {
238 return Collections.unmodifiableList(mSynPackets);
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.
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));
254 * Attempt to ACK any FIN packets held by this conversation.
255 * @param ackPacket The ACK for a FIN previously added to this conversation.
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);
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}.
268 public List<FinAckPair> getFinAckPairs() {
269 return Collections.unmodifiableList(mFinPackets);
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.
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));
284 // =========================================================================================================
285 // We simply reuse equals and hashCode methods of String.class to be able to use this class as a key
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
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.
296 public boolean equals(Object obj) {
297 return obj instanceof Conversation && this.toString().equals(obj.toString());
301 public int hashCode() {
302 return toString().hashCode();
304 // =========================================================================================================
307 public String toString() {
308 return String.format("%s:%d %s:%d", mClientIp, mClientPort, mServerIp, mServerPort);
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}
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)) {
330 clientPort = srcPort;
332 serverPort = dstPort;
335 clientPort = dstPort;
337 serverPort = srcPort;
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()));
349 * Determines if the TCP packet contained in {@code packet} is a retransmission of a previously seen (logged)
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?
361 * @param packet The packet.
362 * @return {@code true} if {@code packet} was determined to be a retransmission, {@code false} otherwise.
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);
373 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
374 Direction.class.getSimpleName()));
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.
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);
397 case SERVER_TO_CLIENT:
398 // Server to client packet.
399 mSeqNumbersSrv.add(seqNo);
402 throw new RuntimeException(String.format("Unexpected value of enum '%s'",
403 Direction.class.getSimpleName()));
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.
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;
424 throw new IllegalArgumentException("getDirection: packet not related to " + getClass().getSimpleName());
429 * Utility enum for expressing the direction of a packet pertaining to this {@code Conversation}.
431 private enum Direction {
432 CLIENT_TO_SERVER, SERVER_TO_CLIENT