1 package edu.uci.iotproject;
3 import org.pcap4j.core.PacketListener;
4 import org.pcap4j.core.PcapPacket;
5 import org.pcap4j.packet.TcpPacket;
10 * Reassembles TCP conversations (streams).
12 * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
13 * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
15 public class TcpReassembler implements PacketListener {
18 * Holds <em>open</em> {@link Conversation}s, i.e., {@code Conversation}s that have <em>not</em> been detected as
19 * (gracefully) terminated based on the set of packets observed thus far.
20 * A {@link Conversation} is moved to {@link #mTerminatedConversations} if it can be determined that it is has
21 * terminated. Termination can be detected by a) observing two {@link FinAckPair}s, one in each direction, (graceful
22 * termination, see {@link Conversation#isGracefullyShutdown()}) or b) by observing a SYN packet that matches the
23 * four tuple of an existing {@code Conversation}, but which holds a <em>different</em> sequence number than the
24 * same-direction SYN packet recorded for the {@code Conversation}.
26 * Note that due to limitations of the {@link Set} interface (specifically, there is no {@code get(T t)} method),
27 * we have to resort to a {@link Map} (in which keys map to themselves) to "mimic" a set with {@code get(T t)}
30 * @see <a href="https://stackoverflow.com/questions/7283338/getting-an-element-from-a-set">this question on StackOverflow.com</a>
32 private final Map<Conversation, Conversation> mOpenConversations = new HashMap<>();
35 * Holds <em>terminated</em> {@link Conversation}s.
37 private final List<Conversation> mTerminatedConversations = new ArrayList<>();
40 public void gotPacket(PcapPacket pcapPacket) {
41 TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
42 if (tcpPacket == null) {
46 processPacket(pcapPacket);
50 * Get the reassembled TCP connections. Note that if this is called while packets are still being processed (by
51 * calls to {@link #gotPacket(PcapPacket)}), the behavior is undefined and the returned list may be inconsistent.
52 * @return The reassembled TCP connections.
54 public List<Conversation> getTcpConversations() {
55 ArrayList<Conversation> combined = new ArrayList<>();
56 combined.addAll(mTerminatedConversations);
57 combined.addAll(mOpenConversations.values());
61 private void processPacket(PcapPacket pcapPacket) {
62 TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
63 // Handle client connection initiation attempts.
64 if (tcpPacket.getHeader().getSyn() && !tcpPacket.getHeader().getAck()) {
65 // A segment with the SYN flag set, but no ACK flag indicates that a client is attempting to initiate a new
67 processNewConnectionRequest(pcapPacket);
70 // Handle server connection initiation acknowledgement
71 if (tcpPacket.getHeader().getSyn() && tcpPacket.getHeader().getAck()) {
72 // A segment with both the SYN and ACK flags set indicates that the server has accepted the client's request
73 // to initiate a new connection.
74 processNewConnectionAck(pcapPacket);
78 if (tcpPacket.getHeader().getRst()) {
79 processRstPacket(pcapPacket);
83 if (tcpPacket.getHeader().getFin()) {
85 processFinPacket(pcapPacket);
87 // Handle ACKs (currently only ACKs of FINS)
88 if (tcpPacket.getHeader().getAck()) {
89 processAck(pcapPacket);
91 // Handle packets that carry payload (application data).
92 if (tcpPacket.getPayload() != null) {
93 processPayloadPacket(pcapPacket);
97 private void processNewConnectionRequest(PcapPacket clientSynPacket) {
98 // A SYN w/o ACK always originates from the client.
99 Conversation conv = Conversation.fromPcapPacket(clientSynPacket, true);
100 conv.addSynPacket(clientSynPacket);
101 // Is there an ongoing conversation for the same four tuple (clientIp, clientPort, serverIp, serverPort) as
102 // found in the new SYN packet?
103 Conversation ongoingConv = mOpenConversations.get(conv);
104 if (ongoingConv != null) {
105 if (ongoingConv.isRetransmission(clientSynPacket)) {
106 // SYN retransmission detected, do nothing.
108 // TODO: the way retransmission detection is implemented may cause a bug for connections where we have
109 // not recorded the initial SYN, but only the SYN ACK, as retransmission is determined by comparing the
110 // sequence numbers of initial SYNs -- and if no initial SYN is present for the Conversation, the new
111 // SYN will be interpreted as a retransmission. Possible fix: let isRentransmission ALWAYS return false
112 // when presented with a SYN packet when the Conversation already holds a SYN ACK packet?
114 // New SYN has different sequence number than SYN recorded for ongoingConv, so this must be an attempt
115 // to establish a new conversation with the same four tuple as ongoingConv.
116 // Mark existing connection as terminated.
117 // TODO: is this 100% theoretically correct, e.g., if many connection attempts are made back to back? And RST packets?
118 mTerminatedConversations.add(ongoingConv);
119 mOpenConversations.remove(ongoingConv);
122 // Finally, update the map of open connections with the new connection.
123 mOpenConversations.put(conv, conv);
128 * TODO a problem across the board for all processXPacket methods below:
129 * if we start the capture in the middle of a TCP connection, we will not have an entry for the conversation in the
130 * map as we have not seen the initial SYN packet.
131 * Two ways we can address this:
132 * a) Perform null-checks and ignore packets for which we have not seen SYN
133 * + easy to get correct
134 * - we discard data (issue for long-lived connections!)
135 * b) Add a corresponding conversation entry whenever we encounter a packet that does not map to a conversation
136 * + we consider all data
137 * - not immediately clear if this will introduce bugs (incorrectly mapping packets to wrong conversations?)
139 * [[[ I went with option b) for now; see getOngoingConversationOrCreateNew(PcapPacket pcapPacket). ]]]
142 private void processNewConnectionAck(PcapPacket srvSynPacket) {
143 // Find the corresponding ongoing connection, if any (if we start the capture just *after* the initial SYN, no
144 // ongoing conversation entry will exist, so it must be created in that case).
145 // Conversation conv = mOpenConversations.get(Conversation.fromPcapPacket(srvSynPacket, false));
146 Conversation conv = getOngoingConversationOrCreateNew(srvSynPacket);
147 // Note: exploits &&'s short-circuit operation: only attempts to add non-retransmissions.
148 if (!conv.isRetransmission(srvSynPacket) && !conv.addSynPacket(srvSynPacket)) {
149 // For safety/debugging: if NOT a retransmission and add fails,
150 // something has gone terribly wrong/invariant is broken.
151 throw new IllegalStateException("Attempt to add SYN ACK packet that was NOT a retransmission failed." +
152 Conversation.class.getSimpleName() + " invariant broken.");
156 private void processRstPacket(PcapPacket rstPacket) {
157 Conversation conv = getOngoingConversationOrCreateNew(rstPacket);
158 // Move conversation to set of terminated conversations.
159 mTerminatedConversations.add(conv);
160 mOpenConversations.remove(conv, conv);
163 private void processFinPacket(PcapPacket finPacket) {
164 // getOngoingConversationForPacket(finPacket).addFinPacket(finPacket);
165 getOngoingConversationOrCreateNew(finPacket).addFinPacket(finPacket);
168 private void processAck(PcapPacket ackPacket) {
169 // getOngoingConversationForPacket(ackPacket).attemptAcknowledgementOfFin(ackPacket);
170 // Note that unlike the style for SYN, FIN, and payload packets, for "ACK only" packets, we want to avoid
171 // creating a new conversation.
172 Conversation conv = getOngoingConversationForPacket(ackPacket);
174 // The ACK may be an ACK of a FIN, so attempt to mark the FIN as ack'ed.
175 conv.attemptAcknowledgementOfFin(ackPacket);
176 if (conv.isGracefullyShutdown()) {
177 // Move conversation to set of terminated conversations.
178 mTerminatedConversations.add(conv);
179 mOpenConversations.remove(conv);
182 // Note: add (additional) processing of ACKs (that are not ACKs of FINs) as necessary here...
185 private void processPayloadPacket(PcapPacket pcapPacket) {
186 // getOngoingConversationForPacket(pcapPacket).addPacket(pcapPacket, true);
187 getOngoingConversationOrCreateNew(pcapPacket).addPacket(pcapPacket, true);
191 * Locates an ongoing conversation (if any) that {@code pcapPacket} pertains to.
192 * @param pcapPacket The packet that is to be mapped to an ongoing {@code Conversation}.
193 * @return The {@code Conversation} matching {@code pcapPacket} or {@code null} if there is no match.
195 private Conversation getOngoingConversationForPacket(PcapPacket pcapPacket) {
196 // We cannot know if this is a client-to-server or server-to-client packet without trying both options...
197 Conversation conv = mOpenConversations.get(Conversation.fromPcapPacket(pcapPacket, true));
199 conv = mOpenConversations.get(Conversation.fromPcapPacket(pcapPacket, false));
205 * Like {@link #getOngoingConversationForPacket(PcapPacket)}, but creates and inserts a new {@code Conversation}
206 * into {@link #mOpenConversations} if no open conversation is found (i.e., in the case that
207 * {@link #getOngoingConversationForPacket(PcapPacket)} returns {@code null}).
209 * @param pcapPacket The packet that is to be mapped to an ongoing {@code Conversation}.
210 * @return The existing, ongoing {@code Conversation} matching {@code pcapPacket} or the newly created one in case
211 * no match was found.
213 private Conversation getOngoingConversationOrCreateNew(PcapPacket pcapPacket) {
214 Conversation conv = getOngoingConversationForPacket(pcapPacket);
216 TcpPacket tcpPacket = pcapPacket.get(TcpPacket.class);
217 if (tcpPacket.getHeader().getSyn() && tcpPacket.getHeader().getAck()) {
218 // A SYN ACK packet always originates from the server (it is a reply to the initial SYN packet from the client)
219 conv = Conversation.fromPcapPacket(pcapPacket, false);
221 // TODO: can we do anything else but arbitrarily select who is designated as the server in this case?
222 conv = Conversation.fromPcapPacket(pcapPacket, false);
224 mOpenConversations.put(conv, conv);