• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.net.util;
18 
19 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
20 
21 import android.net.ip.ConnectivityPacketTracker;
22 import android.os.Handler;
23 import android.os.HandlerThread;
24 import android.os.Looper;
25 import android.os.Message;
26 import android.util.ArrayMap;
27 import android.util.LocalLog;
28 import android.util.Log;
29 
30 import androidx.annotation.NonNull;
31 
32 import com.android.internal.annotations.VisibleForTesting;
33 import com.android.net.module.util.InterfaceParams;
34 
35 import java.util.Objects;
36 
37 /**
38  * Tracks and manages raw packet captures on a network interface.
39  *
40  * <p>This class is not a thread-safe and should be only run on the handler thread.
41  * It utilizes a dedicated {@link HandlerThread} to perform capture operations, allowing
42  * the caller to interact with it asynchronously through methods like
43  * {@link #startCapture(String, long)}, {@link #stopCapture(String)},
44  * and {@link #getMatchedPacketCount(String, String)}.</p>
45  *
46  */
47 public class RawPacketTracker {
48     /**
49      * Dependencies class for testing.
50      */
51     @VisibleForTesting(visibility = PRIVATE)
52     static class Dependencies {
createPacketTracker( Handler handler, InterfaceParams ifParams, int maxPktRecords)53         public @NonNull ConnectivityPacketTracker createPacketTracker(
54                 Handler handler, InterfaceParams ifParams, int maxPktRecords) {
55             // A BPF filter is unnecessary here, as the caller uses this device to send packets
56             // and verify the APF offload reply packets received from the remote device.
57             return new ConnectivityPacketTracker(
58                     handler, ifParams, new LocalLog(maxPktRecords), false /* attachFilter */);
59         }
60 
createHandlerThread()61         public @NonNull HandlerThread createHandlerThread() {
62             final HandlerThread handlerThread = new HandlerThread(TAG + "-handler");
63             handlerThread.start();
64             return handlerThread;
65         }
66 
getLooper(HandlerThread handlerThread)67         public @NonNull Looper getLooper(HandlerThread handlerThread) {
68             return handlerThread.getLooper();
69         }
70     }
71 
72     // Maximum number of packet records to store.
73     private static final int MAX_PACKET_RECORDS = 100;
74     // Maximum duration for a packet capture session in milliseconds.
75     public static final long MAX_CAPTURE_TIME_MS = 300_000;
76     @VisibleForTesting(visibility = PRIVATE)
77     public static final int CMD_STOP_CAPTURE = 1;
78     private static final String TAG = RawPacketTracker.class.getSimpleName();
79 
80     private final @NonNull HandlerThread mHandlerThread;
81     private final @NonNull Dependencies mDeps;
82     private final @NonNull Handler mHandler;
83 
84     /**
85      * A map that stores ConnectivityPacketTracker objects, keyed by their associated
86      * network interface name, e.g: wlan0. This allows for tracking connectivity
87      * packets on a per-interface basis. This is only accessed by handler thread.
88      */
89     private final ArrayMap<String, ConnectivityPacketTracker> mTrackerMap = new ArrayMap<>();
90 
RawPacketTracker()91     public RawPacketTracker() {
92         this(new Dependencies());
93     }
94 
95     @VisibleForTesting(visibility = PRIVATE)
RawPacketTracker( @onNull Dependencies deps )96     public RawPacketTracker(
97             @NonNull Dependencies deps
98     ) {
99         mDeps = deps;
100         mHandlerThread = deps.createHandlerThread();
101         mHandler = new RawPacketTrackerHandler(deps.getLooper(mHandlerThread), this);
102     }
103 
104     private static class RawPacketTrackerHandler extends Handler {
105         private final RawPacketTracker mRawPacketTracker;
RawPacketTrackerHandler( @onNull Looper looper, @NonNull RawPacketTracker rawPacketTracker)106         private RawPacketTrackerHandler(
107                 @NonNull Looper looper,
108                 @NonNull RawPacketTracker rawPacketTracker) {
109             super(looper);
110             mRawPacketTracker = rawPacketTracker;
111         }
112 
113         @Override
handleMessage(Message msg)114         public void handleMessage(Message msg) {
115             final String ifaceName;
116             switch (msg.what) {
117                 case CMD_STOP_CAPTURE:
118                     ifaceName = (String) msg.obj;
119                     mRawPacketTracker.processStopCapture(ifaceName);
120                     break;
121                 default:
122                     Log.e(TAG, "unrecognized message: " + msg.what);
123             }
124         }
125     }
126 
127     /**
128      * Starts capturing packets on the specified network interface.
129      *
130      * <p>Initiates a packet capture session if one is not already running for the given interface.
131      * A capture timeout is set to automatically stop the capture after {@code maxCaptureTimeMs}
132      * milliseconds. If a previous stop capture event was scheduled, it is canceled.</p>
133      *
134      * @param ifaceName      The name of the network interface to capture packets on.
135      * @param maxCaptureTimeMs The maximum capture duration in milliseconds.
136      * @throws IllegalArgumentException If {@code maxCaptureTimeMs} is less than or equal to 0.
137      * @throws RuntimeException If a capture is already running on the specified interface.
138      * @throws IllegalStateException If this method is not running on handler thread
139      */
startCapture( String ifaceName, long maxCaptureTimeMs )140     public void startCapture(
141             String ifaceName, long maxCaptureTimeMs
142     ) throws IllegalArgumentException, RuntimeException, IllegalStateException {
143         ensureRunOnHandlerThread();
144         if (maxCaptureTimeMs <= 0) {
145             throw new IllegalArgumentException("maxCaptureTimeMs " + maxCaptureTimeMs + " <= 0");
146         }
147 
148         if (mTrackerMap.containsKey(ifaceName)) {
149             throw new RuntimeException(ifaceName + " is already capturing");
150         }
151 
152         final InterfaceParams ifParams = InterfaceParams.getByName(ifaceName);
153         Objects.requireNonNull(ifParams, "invalid interface " + ifaceName);
154 
155         final ConnectivityPacketTracker tracker =
156                 mDeps.createPacketTracker(mHandler, ifParams, MAX_PACKET_RECORDS);
157         tracker.start(TAG + "." + ifaceName);
158         mTrackerMap.putIfAbsent(ifaceName, tracker);
159         tracker.setCapture(true);
160 
161         // remove scheduled stop events if it already in the queue
162         mHandler.removeEqualMessages(CMD_STOP_CAPTURE, ifaceName);
163 
164         // capture up to configured capture time and stop capturing
165         final Message stopMsg = mHandler.obtainMessage(CMD_STOP_CAPTURE, ifaceName);
166         mHandler.sendMessageDelayed(stopMsg, maxCaptureTimeMs);
167     }
168 
169     /**
170      * Stops capturing packets on the specified network interface.
171      *
172      * <p>Terminates the packet capture session if one is active for the given interface.
173      * Any pending stop capture events for the interface are canceled.</p>
174      *
175      * @param ifaceName The name of the network interface to stop capturing on.
176      * @throws RuntimeException If no capture is running on the specified interface.
177      * @throws IllegalStateException If this method is not running on handler thread
178      */
stopCapture(String ifaceName)179     public void stopCapture(String ifaceName) throws RuntimeException, IllegalStateException {
180         ensureRunOnHandlerThread();
181         if (!mTrackerMap.containsKey(ifaceName)) {
182             throw new RuntimeException(ifaceName + " is already stopped");
183         }
184 
185         final Message msg = mHandler.obtainMessage(CMD_STOP_CAPTURE, ifaceName);
186         // remove scheduled stop events if it already in the queue
187         mHandler.removeEqualMessages(CMD_STOP_CAPTURE, ifaceName);
188         mHandler.sendMessage(msg);
189     }
190 
191     /**
192      * Returns the {@link Handler} associated with this RawTracker.
193      *
194      * <p>This handler is used for posting tasks to the RawTracker's internal thread.
195      * You can use it to execute code that needs to interact with the RawTracker
196      * in a thread-safe manner.
197      *
198      * @return The non-null {@link Handler} instance.
199      */
getHandler()200     public @NonNull Handler getHandler() {
201         return mHandler;
202     }
203 
204     /**
205      * Retrieves the number of captured packets matching a specific pattern.
206      *
207      * <p>Queries the packet capture data for the specified interface and counts the occurrences
208      * of packets that match the provided {@code packet} string. The count is performed
209      * asynchronously on the capture thread.</p>
210      *
211      * @param ifaceName The name of the network interface.
212      * @param packetPattern The packet pattern to match.
213      * @return The number of matched packets, or 0 if an error occurs or no matching packets are
214      *         found.
215      * @throws RuntimeException If no capture is running on the specified interface.
216      * @throws IllegalStateException If this method is not running on handler thread
217      */
getMatchedPacketCount( String ifaceName, String packetPattern )218     public int getMatchedPacketCount(
219             String ifaceName, String packetPattern
220     ) throws RuntimeException, IllegalStateException {
221         ensureRunOnHandlerThread();
222         final ConnectivityPacketTracker tracker;
223         tracker = mTrackerMap.getOrDefault(ifaceName, null);
224         if (tracker == null) {
225             throw new RuntimeException(ifaceName + " is not capturing");
226         }
227 
228         return tracker.getMatchedPacketCount(packetPattern);
229     }
230 
processStopCapture(String ifaceName)231     private void processStopCapture(String ifaceName) {
232         final ConnectivityPacketTracker tracker = mTrackerMap.get(ifaceName);
233         mTrackerMap.remove(ifaceName);
234         tracker.setCapture(false);
235     }
236 
ensureRunOnHandlerThread()237     private void ensureRunOnHandlerThread() {
238         if (mHandler.getLooper() != Looper.myLooper()) {
239             throw new IllegalStateException(
240                 "Not running on Handler thread: " + Thread.currentThread().getName()
241             );
242         }
243     }
244 }
245