SignatureDetector.java: closer towards final implementation (e.g., also check packet...
[pingpong.git] / Code / Projects / SmartPlugDetector / src / main / java / edu / uci / iotproject / detection / SignatureDetector.java
1 package edu.uci.iotproject.detection;
2
3 import edu.uci.iotproject.Conversation;
4 import edu.uci.iotproject.TcpReassembler;
5 import edu.uci.iotproject.analysis.TcpConversationUtils;
6 import org.pcap4j.core.PacketListener;
7 import org.pcap4j.core.PcapPacket;
8
9 import java.util.Arrays;
10 import java.util.List;
11 import java.util.Objects;
12
13 import static edu.uci.iotproject.util.PcapPacketUtils.*;
14
15 /**
16  * TODO add class documentation.
17  *
18  * @author Janus Varmarken {@literal <jvarmark@uci.edu>}
19  * @author Rahmadi Trimananda {@literal <rtrimana@uci.edu>}
20  */
21 public class SignatureDetector implements PacketListener {
22
23     /**
24      * The signature that this {@link SignatureDetector} is trying to detect in the observed traffic.
25      */
26     private final List<List<PcapPacket>> mSignature;
27
28     /**
29      * The directions of packets in the sequence that make up {@link #mSignature}.
30      */
31     private final Conversation.Direction[] mSignatureDirections;
32
33     /**
34      * For reassembling the observed traffic into TCP connections.
35      */
36     private final TcpReassembler mTcpReassembler = new TcpReassembler();
37
38     /**
39      * IP of the router's WAN port (if analyzed traffic is captured at the ISP's point of view).
40      */
41     private final String mRouterWanIp;
42
43     public SignatureDetector(List<List<PcapPacket>> signature, String routerWanIp) {
44         Objects.requireNonNull(signature, "signature cannot be null");
45         if (signature.isEmpty() || signature.stream().anyMatch(inner -> inner.isEmpty())) {
46             throw new IllegalArgumentException("signature is empty (or contains empty inner List)");
47         }
48         mSignature = signature;
49         mRouterWanIp = routerWanIp;
50         // Build the direction sequence.
51         // Note: assumes that the provided signature was captured within the local network (routerWanIp is set to null).
52         mSignatureDirections = getPacketDirections(mSignature.get(0), null);
53         /*
54          * Enforce restriction on cluster/signature members: all representatives must exhibit the same direction pattern
55          * and contain the same number of packets. Note that this is a somewhat heavy operation, so it may be disabled
56          * later on in favor of performance. However, it is only run once (at instantiation), so the overhead may be
57          * warranted in order to ensure correctness, especially during the development/debugging phase.
58          */
59         if (mSignature.stream().
60                 anyMatch(inner -> !Arrays.equals(mSignatureDirections, getPacketDirections(inner, null)))) {
61             throw new IllegalArgumentException(
62                     "signature members must contain the same number of packets and exhibit the same packet direction " +
63                             "pattern"
64             );
65         }
66     }
67
68     @Override
69     public void gotPacket(PcapPacket packet) {
70         // Present packet to TCP reassembler so that it can be mapped to a connection (if it is a TCP packet).
71         mTcpReassembler.gotPacket(packet);
72
73     }
74
75     private void performDetection() {
76         // Let's start out simple by building a version that only works for signatures that do not span across multiple
77         // TCP conversations...
78         for (Conversation c : mTcpReassembler.getTcpConversations()) {
79             for (List<PcapPacket> sequence : mSignature) {
80                 boolean matchFound = isSequenceInConversation(sequence, c);
81                 if (matchFound) {
82                     onSequenceDetected(sequence, c);
83                     // Found signature in current conversation, so break inner loop and continue with next conversation.
84                     break;
85                 }
86             }
87             /*
88              * TODO:
89              * if no item in cluster matches, also perform a distance-based matching to cover those cases where we did
90              * not manage to capture every single mutation of the sequence during training.
91              *
92              * Need to compute average/centroid of cluster to do so...? Compute within-cluster variance, then check if
93              * distance between input conversation and cluster average/centroid is smaller than or equal to the computed
94              * variance?
95              */
96         }
97     }
98
99     private void onSequenceDetected(List<PcapPacket> sequence, Conversation c) {
100         // TODO implement whatever output we want, e.g., print to std.out or notify observer objects
101
102
103     }
104
105     /**
106      * Examine if a {@link Conversation} contains a given sequence of packets. Note: the current implementation actually
107      * searches for a substring as it does not allow for interleaved packets in {@code c} that are not in
108      * {@code sequence}; for example, if {@code sequence} consists of packet lengths [2, 3, 5] and {@code c} consists of
109      * packet lengths [2, 3, 4, 5], the result will be {@code false}. If we are to allow interleaved packets, we need
110      * a modified version of <a href="https://stackoverflow.com/a/20545604/1214974">this</a>.
111      * @param sequence The sequence to look for.
112      * @param c The {@link Conversation} to search for {@code sequence} in.
113      * @return {@code true} if {@code c} contains {@code sequence}, {@code false} otherwise.
114      */
115     private boolean isSequenceInConversation(List<PcapPacket> sequence, Conversation c) {
116         // The packets we match against differ depending on whether the signature is a TLS or non-TLS signature.
117         boolean tlsSequence = isTlsSequence(sequence);
118         if (tlsSequence && !c.isTls()) {
119             // If we're looking for a TLS signature and this conversation does not appear to be a TLS conversation, we
120             // are done. Note: this assumes that they do NOT start performing TLS on new ports that are not captured in
121             // Conversation.isTls()
122             return false;
123         }
124         // Based on TLS or non-TLS signature, fetch the corresponding list of packets to match against.
125         List<PcapPacket> packets = tlsSequence ? c.getTlsApplicationDataPackets() : c.getPackets();
126         // If sequence is longer than the conversation, it can obviously not be contained in the conversation.
127         if (packets.size() < sequence.size()) {
128             return false;
129         }
130         /*
131          * Generate packet direction array for c. We have already generated the packet direction array for sequence as
132          * part of the constructor (mSignatureDirections).
133          */
134         Conversation.Direction[] cDirections = getPacketDirections(packets, mRouterWanIp);
135         int seqIdx = 0;
136         int convIdx = 0;
137         while (convIdx < packets.size()) {
138             PcapPacket seqPkt = sequence.get(seqIdx);
139             PcapPacket convPkt = packets.get(convIdx);
140             // We only have a match if packet lengths and directions match.
141             if (convPkt.getOriginalLength() == seqPkt.getOriginalLength() &&
142                     mSignatureDirections[seqIdx] == cDirections[convIdx]) {
143                 // A match, advance both indices to consider next packet in sequence vs. next packet in conversation
144                 seqIdx++;
145                 convIdx++;
146                 if (seqIdx == sequence.size()) {
147                     // we managed to match the full sequence in the conversation.
148                     return true;
149                 }
150             } else {
151                 // Mismatch.
152                 if (seqIdx > 0) {
153                     /*
154                      * If we managed to match parts of sequence, we restart the search for sequence in c at the index of
155                      * c where the current mismatch occurred. I.e., we must reset seqIdx, but leave convIdx untouched.
156                      */
157                     seqIdx = 0;
158                 } else {
159                     /*
160                      * First packet of sequence didn't match packet at convIdx of conversation, so we move forward in
161                      * conversation, i.e., we continue the search for sequence in c starting at index convIdx+1 of c.
162                      */
163                     convIdx++;
164                 }
165             }
166         }
167         return false;
168     }
169
170     private boolean isTlsSequence(List<PcapPacket> sequence) {
171         // NOTE: Assumes ALL packets in sequence pertain to the same TCP connection!
172         PcapPacket firstPkt = sequence.get(0);
173         int srcPort = getSourcePort(firstPkt);
174         int dstPort = getDestinationPort(firstPkt);
175         return TcpConversationUtils.isTlsPort(srcPort) || TcpConversationUtils.isTlsPort(dstPort);
176     }
177
178     /**
179      * Given a {@code List<PcapPacket>}, generate a {@code Conversation.Direction[]} such that each entry in the
180      * resulting {@code Conversation.Direction[]} specifies the direction of the {@link PcapPacket} at the corresponding
181      * index in the input list.
182      * @param packets The list of packets for which to construct a corresponding array of packet directions.
183      * @param routerWanIp The IP of the router's WAN port. This is used for determining the direction of packets when
184      *                    the traffic is captured just outside the local network (at the ISP side of the router). Set to
185      *                    {@code null} if {@code packets} stem from traffic captured within the local network.
186      * @return A {@code Conversation.Direction[]} specifying the direction of the {@link PcapPacket} at the
187      *         corresponding index in {@code packets}.
188      */
189     private static Conversation.Direction[] getPacketDirections(List<PcapPacket> packets, String routerWanIp) {
190         Conversation.Direction[] directions = new Conversation.Direction[packets.size()];
191         for (int i = 0; i < packets.size(); i++) {
192             PcapPacket pkt = packets.get(i);
193             if (getSourceIp(pkt).equals(getDestinationIp(pkt))) {
194                 // Sanity check: we shouldn't be processing loopback traffic
195                 throw new AssertionError("loopback traffic detected");
196             }
197             if (isSrcIpLocal(pkt) || getSourceIp(pkt).equals(routerWanIp)) {
198                 directions[i] = Conversation.Direction.CLIENT_TO_SERVER;
199             } else if (isDstIpLocal(pkt) || getDestinationIp(pkt).equals(routerWanIp)) {
200                 directions[i] = Conversation.Direction.SERVER_TO_CLIENT;
201             } else {
202                 throw new IllegalArgumentException("no local IP or router WAN port IP found, can't detect direction");
203             }
204         }
205         return directions;
206     }
207
208 }