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