1 package edu.uci.iotproject;
3 import edu.uci.iotproject.comparison.ComparisonFunctions;
4 import edu.uci.iotproject.comparison.CompleteMatchPatternComparisonResult;
5 import edu.uci.iotproject.comparison.PatternComparisonTask;
6 import org.pcap4j.core.NotOpenException;
7 import org.pcap4j.core.PcapHandle;
8 import org.pcap4j.core.PcapNativeException;
9 import org.pcap4j.core.PcapPacket;
10 import org.pcap4j.packet.DnsPacket;
11 import org.pcap4j.packet.IpV4Packet;
12 import org.pcap4j.packet.TcpPacket;
15 import java.net.UnknownHostException;
16 import java.text.DateFormat;
17 import java.text.SimpleDateFormat;
18 import java.time.temporal.ChronoField;
20 import java.util.concurrent.*;
24 * <p>Provides functionality for searching for the presence of a {@link FlowPattern} in a PCAP trace.</p>
27 * The (entire) PCAP trace is traversed and parsed on one thread (specifically, the thread that calls
28 * {@link #findFlowPattern()}). This thread builds a {@link DnsMap} using the DNS packets present in the trace and uses
29 * that {@code DnsMap} to reassemble {@link Conversation}s that <em>potentially</em> match the provided
30 * {@link FlowPattern} (in that one end/party of said conversations matches the hostname(s) specified by the given
31 * {@code FlowPattern}).
32 * These potential matches are then examined on background worker thread(s) to determine if they are indeed a (complete)
33 * match of the provided {@code FlowPattern}.
36 * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
37 * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
39 public class FlowPatternFinder {
41 /* Begin class properties */
43 * {@link ExecutorService} responsible for parallelizing pattern searches.
44 * Declared as static to allow for reuse of threads across different instances of {@code FlowPatternFinder} and to
45 * avoid the overhead of initializing a new thread pool for each {@code FlowPatternFinder} instance.
47 private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
48 /* End class properties */
50 /* Begin instance properties */
52 * Holds a set of {@link Conversation}s that <em>potentially</em> match {@link #mPattern} since each individual
53 * {@code Conversation} is communication with the hostname identified by {@code mPattern.getHostname()}.
54 * Note that due to limitations of the {@link Set} interface (specifically, there is no {@code get(T t)} method),
55 * we have to resort to a {@link Map} (in which keys map to themselves) to "mimic" a set with {@code get(T t)}
58 * @see <a href="https://stackoverflow.com/questions/7283338/getting-an-element-from-a-set">this question on StackOverflow.com</a>
60 private final Map<Conversation, Conversation> mConversations;
63 * Holds a list of trigger times.
65 private final List<Long> mTriggerTimes;
66 private static int triggerListCounter;
68 private final DnsMap mDnsMap;
69 private final PcapHandle mPcap;
70 private final FlowPattern mPattern;
71 private final ConversationPair mConvPair;
72 private final String FILE = "./devices/tplink_switch/datapoints.csv";
73 //private final String REF_FILE = "./devices/tplink_switch/tplink-june-14-2018.timestamps";
74 private final String REF_FILE = "./devices/tplink_switch/tplink-feb-13-2018.timestamps";
76 private final List<Future<CompleteMatchPatternComparisonResult>> mPendingComparisons = new ArrayList<>();
77 /* End instance properties */
80 * Constructs a new {@code FlowPatternFinder}.
81 * @param pcap an <em>open</em> {@link PcapHandle} that provides access to the trace that is to be examined.
82 * @param pattern the {@link FlowPattern} to search for.
84 public FlowPatternFinder(PcapHandle pcap, FlowPattern pattern) {
85 this.mConversations = new HashMap<>();
86 this.mTriggerTimes = readTriggerTimes(REF_FILE);
87 triggerListCounter = 0;
88 this.mDnsMap = new DnsMap();
89 this.mPcap = Objects.requireNonNull(pcap,
90 String.format("Argument of type '%s' cannot be null", PcapHandle.class.getSimpleName()));
91 this.mPattern = Objects.requireNonNull(pattern,
92 String.format("Argument of type '%s' cannot be null", FlowPattern.class.getSimpleName()));
93 this.mConvPair = new ConversationPair(FILE, ConversationPair.Direction.DEVICE_TO_SERVER);
97 private List<Long> readTriggerTimes(String refFileName) {
99 List<Long> listTriggerTimes = new ArrayList<>();
101 File file = new File(refFileName);
102 BufferedReader br = new BufferedReader(new FileReader(file));
104 while ((s = br.readLine()) != null) {
105 listTriggerTimes.add(timeToMillis(s, false));
107 } catch (IOException e) {
110 System.out.println("List has: " + listTriggerTimes.size());
112 return listTriggerTimes;
116 * Starts the pattern search.
118 public void start() {
121 findSignatureBasedOnTimestamp();
125 * Find patterns based on the FlowPattern object (run by a thread)
127 private void findFlowPattern() {
130 // TODO: The new comparison method is pending
131 // TODO: For now, just compare using one hostname and one list per FlowPattern
132 // List<String> hostnameList = mPattern.getHostnameList();
133 // int hostIndex = 0;
134 while ((packet = mPcap.getNextPacketEx()) != null) {
135 // Let DnsMap handle DNS packets.
136 if (packet.get(DnsPacket.class) != null) {
137 // Check if this is a valid DNS packet
138 mDnsMap.validateAndAddNewEntry(packet);
141 // For now, we only work support pattern search in TCP over IPv4.
142 final IpV4Packet ipPacket = packet.get(IpV4Packet.class);
143 final TcpPacket tcpPacket = packet.get(TcpPacket.class);
144 if (ipPacket == null || tcpPacket == null) {
148 String srcAddress = ipPacket.getHeader().getSrcAddr().getHostAddress();
149 String dstAddress = ipPacket.getHeader().getDstAddr().getHostAddress();
150 int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
151 int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
152 // Is this packet related to the pattern; i.e. is it going to (or coming from) the cloud server?
153 boolean fromServer = mDnsMap.isRelatedToCloudServer(srcAddress, mPattern.getHostname());
154 boolean fromClient = mDnsMap.isRelatedToCloudServer(dstAddress, mPattern.getHostname());
155 // String currentHostname = hostnameList.get(hostIndex);
156 // boolean fromServer = mDnsMap.isRelatedToCloudServer(srcAddress, currentHostname);
157 // boolean fromClient = mDnsMap.isRelatedToCloudServer(dstAddress, currentHostname);
158 if (!fromServer && !fromClient) {
159 // Packet not related to pattern, skip it.
163 // Conversations (connections/sessions) are identified by the four-tuple
164 // (clientIp, clientPort, serverIp, serverPort) (see Conversation Javadoc).
165 // Create "dummy" conversation for looking up an existing entry.
166 Conversation conversation = fromClient ? new Conversation(srcAddress, srcPort, dstAddress, dstPort) :
167 new Conversation(dstAddress, dstPort, srcAddress, srcPort);
168 // Add the packet so that the "dummy" conversation can be immediately added to the map if no entry
169 // exists for the conversation that the current packet belongs to.
170 if (tcpPacket.getHeader().getFin()) {
171 // Record FIN packets.
172 conversation.addFinPacket(packet);
174 if (tcpPacket.getPayload() != null) {
175 // Record regular payload packets.
176 conversation.addPacket(packet, true);
178 // Note: does not make sense to call attemptAcknowledgementOfFin here as the new packet has no FINs
179 // in its list, so if this packet is an ACK, it would not be added anyway.
180 // Need to retain a final reference to get access to the packet in the lambda below.
181 final PcapPacket finalPacket = packet;
182 // Add the new conversation to the map if an equal entry is not already present.
183 // If an existing entry is already present, the current packet is simply added to that conversation.
184 mConversations.merge(conversation, conversation, (existingEntry, toMerge) -> {
185 // toMerge may not have any payload packets if the current packet is a FIN packet.
186 if (toMerge.getPackets().size() > 0) {
187 existingEntry.addPacket(toMerge.getPackets().get(0), true);
189 if (toMerge.getFinAckPairs().size() > 0) {
190 // Add the FIN packet to the existing entry.
191 existingEntry.addFinPacket(toMerge.getFinAckPairs().get(0).getFinPacket());
193 if (finalPacket.get(TcpPacket.class).getHeader().getAck()) {
194 existingEntry.attemptAcknowledgementOfFin(finalPacket);
196 return existingEntry;
198 // Refresh reference to point to entry in map (in case packet was added to existing entry).
199 conversation = mConversations.get(conversation);
200 if (conversation.isGracefullyShutdown()) {
201 // Conversation terminated gracefully, so we can now start analyzing it.
202 // Remove the Conversation from the map and start the analysis.
203 // Any future packets identified by the same four tuple will be tied to a new Conversation instance.
204 mConversations.remove(conversation);
205 // Create comparison task and send to executor service.
206 PatternComparisonTask<CompleteMatchPatternComparisonResult> comparisonTask =
207 new PatternComparisonTask<>(conversation, mPattern, ComparisonFunctions.SUB_SEQUENCE_COMPLETE_MATCH);
208 mPendingComparisons.add(EXECUTOR_SERVICE.submit(comparisonTask));
209 // Increment hostIndex to find the next
213 } catch (EOFException eofe) {
214 // TODO should check for leftover conversations in map here and fire tasks for those.
215 // TODO [cont'd] such tasks may be present if connections did not terminate gracefully or if there are longlived connections.
216 System.out.println("[ findFlowPattern ] Finished processing entire PCAP stream!");
217 System.out.println("[ findFlowPattern ] Now waiting for comparisons to finish...");
218 // Wait for all comparisons to finish, then output their results to std.out.
219 for(Future<CompleteMatchPatternComparisonResult> comparisonTask : mPendingComparisons) {
221 // Blocks until result is ready.
222 CompleteMatchPatternComparisonResult comparisonResult = comparisonTask.get();
223 if (comparisonResult.getResult()) {
224 System.out.println(comparisonResult.getTextualDescription());
226 } catch (InterruptedException|ExecutionException e) {
230 } catch (UnknownHostException |
231 PcapNativeException |
233 TimeoutException ex) {
234 ex.printStackTrace();
239 * Find patterns based on the FlowPattern object (run by a thread)
241 private void findSignatureBasedOnTimestamp() {
244 // TODO: The new comparison method is pending
245 // TODO: For now, just compare using one hostname and one list per FlowPattern
246 while ((packet = mPcap.getNextPacketEx()) != null) {
247 // Let DnsMap handle DNS packets.
248 if (packet.get(DnsPacket.class) != null) {
249 // Check if this is a valid DNS packet
250 mDnsMap.validateAndAddNewEntry(packet);
253 // For now, we only work support pattern search in TCP over IPv4.
254 final IpV4Packet ipPacket = packet.get(IpV4Packet.class);
255 final TcpPacket tcpPacket = packet.get(TcpPacket.class);
256 if (ipPacket == null || tcpPacket == null) {
260 String srcAddress = ipPacket.getHeader().getSrcAddr().getHostAddress();
261 String dstAddress = ipPacket.getHeader().getDstAddr().getHostAddress();
262 int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
263 int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
264 // Is this packet related to the pattern; i.e. is it going to (or coming from) the cloud server?
265 boolean fromServer = mDnsMap.isRelatedToCloudServer(srcAddress, mPattern.getHostname());
266 boolean fromClient = mDnsMap.isRelatedToCloudServer(dstAddress, mPattern.getHostname());
267 if (!fromServer && !fromClient) {
268 // Packet not related to pattern, skip it.
271 // Record the conversation pairs
272 if (tcpPacket.getPayload() != null && checkTimeStamp(packet)) {
273 mConvPair.writeConversationPair(packet, fromClient, fromServer);
276 } catch (EOFException eofe) {
277 triggerListCounter = 0;
279 System.out.println("[ findFlowPattern ] ConversationPair writer closed!");
280 System.out.println("[ findFlowPattern ] Frequencies of data points:");
281 mConvPair.printListFrequency();
282 } catch (UnknownHostException |
283 PcapNativeException |
285 TimeoutException ex) {
286 ex.printStackTrace();
290 private boolean checkTimeStamp(PcapPacket packet) {
292 // Extract time from the packet's timestamp
293 String timeStamp = packet.getTimestamp().toString();
294 String timeString = timeStamp.substring(timeStamp.indexOf("T") + 1, timeStamp.indexOf("."));
295 long time = timeToMillis(timeString, true);
297 // We accept packets that are at most 3 seconds away from the trigger time
298 if ((mTriggerTimes.get(triggerListCounter) <= time) &&
299 (time <= mTriggerTimes.get(triggerListCounter) + 3000)) {
300 //System.out.println("Gets here 1: " + timeString + " index: " + triggerListCounter);
303 // Handle the case that the timestamp is > 3000, but < next timestamp
304 // in the list. We ignore these packets.
305 if (time < mTriggerTimes.get(triggerListCounter)) {
306 // Timestamp is smaller than trigger, ignore!
307 //System.out.println("Gets here 2: " + timeString + " index: " + triggerListCounter);
309 } else { // Timestamp is greater than trigger, increment!
310 triggerListCounter = triggerListCounter + 1;
311 //System.out.println("Gets here 3: " + timeString + " index: " + triggerListCounter);
313 return checkTimeStamp(packet);
317 //System.out.println("Timestamp: " + timeToMillis(time, true));
318 //String time2 = "21:38:08";
319 //System.out.println("Timestamp: " + timeToMillis(time2, true));
323 * A private function that returns time in milliseconds.
324 * @param time The time in the form of String.
325 * @param is24Hr If true, then this is in 24-hour format.
327 private long timeToMillis(String time, boolean is24Hr) {
329 String format = null;
332 } else { // 12 Hr format
333 format = "hh:mm:ss aa";
335 DateFormat sdf = new SimpleDateFormat(format);
338 date = sdf.parse(time);
339 } catch(Exception e) {
344 return date.getTime();