Filter out retransmissions when reconstructing TCP flows. Now able to detect all...
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / FlowPatternFinder.java
1 package edu.uci.iotproject;
2
3 import org.pcap4j.core.NotOpenException;
4 import org.pcap4j.core.PcapHandle;
5 import org.pcap4j.core.PcapNativeException;
6 import org.pcap4j.core.PcapPacket;
7 import org.pcap4j.packet.IpV4Packet;
8 import org.pcap4j.packet.Packet;
9 import org.pcap4j.packet.TcpPacket;
10
11 import java.io.EOFException;
12 import java.time.Instant;
13 import java.util.*;
14 import java.util.concurrent.TimeoutException;
15
16 /**
17  * Provides functionality for searching for the presence of a {@link FlowPattern} in a PCAP trace.
18  *
19  * @author Janus Varmarken
20  */
21 public class FlowPatternFinder {
22
23     private final Map<String, Set<String>> dnsMap;
24     private final Map<Conversation, List<PcapPacket>> connections = new HashMap<>();
25
26     public FlowPatternFinder(Map<String, Set<String>> dnsMap) {
27         this.dnsMap = Objects.requireNonNull(dnsMap);
28     }
29
30     private static final Set<String> EMPTY_SET = Collections.unmodifiableSet(new HashSet<>());
31
32     // TODO clean up exceptions etc.
33     public void findFlowPattern(PcapHandle pcap, FlowPattern pattern)
34             throws PcapNativeException, NotOpenException, TimeoutException {
35         try {
36             PcapPacket packet;
37
38             while ((packet = pcap.getNextPacketEx()) != null) {
39
40                 // For now, we only work support pattern search in TCP over IPv4.
41                 IpV4Packet ipPacket = packet.get(IpV4Packet.class);
42                 TcpPacket tcpPacket = packet.get(TcpPacket.class);
43                 if (ipPacket == null || tcpPacket == null) {
44                     continue;
45                 }
46                 String srcAddress = ipPacket.getHeader().getSrcAddr().getHostAddress();
47                 String dstAddress = ipPacket.getHeader().getDstAddr().getHostAddress();
48                 int srcPort = tcpPacket.getHeader().getSrcPort().valueAsInt();
49                 int dstPort = tcpPacket.getHeader().getDstPort().valueAsInt();
50                 // Is this packet related to the pattern and coming from the cloud server?
51                 boolean fromServer = dnsMap.getOrDefault(srcAddress, EMPTY_SET).contains(pattern.getHostname());
52                 // Is this packet related to the pattern and going to the cloud server?
53                 boolean fromClient = dnsMap.getOrDefault(dstAddress, EMPTY_SET).contains(pattern.getHostname());
54                 if (!fromServer && !fromClient) {
55                     // Packet not related to pattern, skip it.
56                     continue;
57                 }
58                 if (tcpPacket.getPayload() == null) {
59                     // We skip non-payload control packets as these are less predictable and should therefore not be
60                     // part of a signature (e.g. receiver can choose not to ACK immediately)
61                     continue;
62                 }
63
64                 // Identify conversations (connections/sessions) by the four-tuple (clientIp, clientPort, serverIp, serverPort).
65                 // TODO: this is strictly not sufficient to differentiate one TCP session from another, but should suffice for now.
66                 Conversation conversation = fromClient ? new Conversation(srcAddress, srcPort, dstAddress, dstPort) :
67                         new Conversation(dstAddress, dstPort, srcAddress, srcPort);
68                 List<PcapPacket> listWrappedPacket = new ArrayList<>();
69                 listWrappedPacket.add(packet);
70                 // Create new conversation entry, or append packet to existing.
71                 connections.merge(conversation, listWrappedPacket, (v1, v2) -> {
72                     // TODO: in theory, this is insufficient to detect retransmissions due to TCP seq.no. rollover.
73                     // TODO: bad for performance, O(n) for each packet added to flow (n being length of the flow).
74                     boolean retransmission = v1.stream().anyMatch(p -> p.get(TcpPacket.class).getHeader().getSequenceNumber() == v2.get(0).get(TcpPacket.class).getHeader().getSequenceNumber());
75                     if (!retransmission) {
76                         // Do not add if retransmission -> avoid duplicate packets in flow
77                         v1.addAll(v2);
78                     }
79                     return v1;
80                 });
81             }
82         } catch (EOFException eofe) {
83             System.out.println("findFlowPattern: finished processing entire file");
84             find(pattern);
85         }
86     }
87
88     private void find(FlowPattern pattern) {
89         for (Conversation con : connections.keySet()) {
90             List<PcapPacket> packets = connections.get(con);
91             if (packets.size() != pattern.getPacketOrder().size()) {
92                 // Not a complete match if different number of packets.
93                 continue;
94             }
95             boolean completeMatch = true;
96             for (int i = 0; i < packets.size(); i++) {
97                 TcpPacket tcpPacket = packets.get(i).get(TcpPacket.class);
98                 if (tcpPacket.getPayload().length() != pattern.getPacketOrder().get(i)) {
99                     completeMatch = false;
100                     break;
101                 }
102             }
103             if (completeMatch) {
104                 PcapPacket firstPacketInFlow = packets.get(0);
105                 System.out.println(
106                         String.format("[ detected a complete match of pattern '%s' at %s]",
107                                 pattern.getPatternId(), firstPacketInFlow.getTimestamp().toString()));
108             }
109         }
110     }
111
112     /**
113      * Immutable class used for identifying a conversation/connection/session/flow (packet's belonging to the same
114      * session between a client and a server).
115      */
116     private static class Conversation {
117
118         private final String clientIp;
119         private final int clientPort;
120         private final String serverIp;
121         private final int serverPort;
122
123         public Conversation(String clientIp, int clientPort, String serverIp, int serverPort) {
124             this.clientIp = clientIp;
125             this.clientPort = clientPort;
126             this.serverIp = serverIp;
127             this.serverPort = serverPort;
128         }
129
130
131         // =========================================================================================================
132         // We simply reuse equals and hashCode methods of String.class to be able to use this immutable class as a key
133         // in a Map.
134         @Override
135         public boolean equals(Object obj) {
136             return obj instanceof Conversation && this.toString().equals(obj.toString());
137         }
138         @Override
139         public int hashCode() {
140             return toString().hashCode();
141         }
142         // =========================================================================================================
143
144         @Override
145         public String toString() {
146             return String.format("%s:%d %s:%d", clientIp, clientPort, serverIp, serverPort);
147         }
148     }
149
150 }