Snap for 12429534 from c90403f22b9cf110d6d2861c66718ba4d32d876a to mainline-mediaprovider-release Change-Id: I36df895090eff589db5f8cd6909155abefcf986b
diff --git a/Android.bp b/Android.bp index 8f4997c..734797a 100644 --- a/Android.bp +++ b/Android.bp
@@ -275,11 +275,13 @@ ], libs: [ "error_prone_annotations", + "framework-annotations-lib", "unsupportedappusage", ], static_libs: [ "androidx.annotation_annotation", "modules-utils-build_system", + "modules-utils-expresslog", "modules-utils-preconditions", "modules-utils-shell-command-handler", "modules-utils-statemachine",
diff --git a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java b/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java deleted file mode 100644 index 1b2cc78..0000000 --- a/apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java +++ /dev/null
@@ -1,104 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.networkstack.apishim.api29; - -import android.net.Uri; -import android.os.Build; - -import androidx.annotation.NonNull; -import androidx.annotation.RequiresApi; -import androidx.annotation.VisibleForTesting; - -import com.android.networkstack.apishim.common.CaptivePortalDataShim; -import com.android.networkstack.apishim.common.UnsupportedApiLevelException; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Compatibility implementation of {@link CaptivePortalData}. - * - * <p>Use {@link com.android.networkstack.apishim.CaptivePortalDataShimImpl} instead of this - * fallback implementation. - */ -@RequiresApi(Build.VERSION_CODES.Q) -public abstract class CaptivePortalDataShimImpl implements CaptivePortalDataShim { - protected CaptivePortalDataShimImpl() {} - - /** - * Parse a {@link android.net.CaptivePortalDataShim} from JSON. - * - * <p>Use - * {@link com.android.networkstack.apishim.CaptivePortalDataShimImpl#fromJson(JSONObject)} - * instead of this API 29 compatibility version. - */ - @NonNull - public static CaptivePortalDataShim fromJson(JSONObject object) throws JSONException, - UnsupportedApiLevelException { - // Data class not supported in API 29 - throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29"); - } - - @Override - public CharSequence getVenueFriendlyName() { - // Not supported in API level 29 - return null; - } - - @Override - public int getUserPortalUrlSource() { - // Not supported in API level 29 - return ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_OTHER; - } - - @VisibleForTesting - public static boolean isSupported() { - return false; - } - - /** - * Generate a {@link CaptivePortalDataShim} object with a friendly name set - * - * @param friendlyName The friendly name to set - * @return a {@link CaptivePortalData} object with a friendly name set - */ - @Override - public CaptivePortalDataShim withVenueFriendlyName(String friendlyName) - throws UnsupportedApiLevelException { - // Not supported in API level 29 - throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29"); - } - - /** - * Generate a {@link CaptivePortalDataShim} object with a friendly name and Passpoint external - * URLs set - * - * @param friendlyName The friendly name to set - * @param venueInfoUrl Venue information URL - * @param termsAndConditionsUrl Terms and conditions URL - * - * @return a {@link CaptivePortalDataShim} object with friendly name, venue info URL and terms - * and conditions URL set - */ - @Override - public CaptivePortalDataShim withPasspointInfo(@NonNull String friendlyName, - @NonNull Uri venueInfoUrl, @NonNull Uri termsAndConditionsUrl) - throws UnsupportedApiLevelException { - // Not supported in API level 29 - throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29"); - } -}
diff --git a/apishim/29/com/android/networkstack/apishim/api29/BroadcastOptionsShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/BroadcastOptionsShimImpl.java similarity index 96% rename from apishim/29/com/android/networkstack/apishim/api29/BroadcastOptionsShimImpl.java rename to apishim/30/com/android/networkstack/apishim/api30/BroadcastOptionsShimImpl.java index ab58dc2..2db73c0 100644 --- a/apishim/29/com/android/networkstack/apishim/api29/BroadcastOptionsShimImpl.java +++ b/apishim/30/com/android/networkstack/apishim/api30/BroadcastOptionsShimImpl.java
@@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.networkstack.apishim.api29; +package com.android.networkstack.apishim.api30; import android.app.BroadcastOptions; import android.os.Build;
diff --git a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java index 8dce170..d84bb52 100644 --- a/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java +++ b/apishim/30/com/android/networkstack/apishim/api30/CaptivePortalDataShimImpl.java
@@ -22,12 +22,10 @@ import android.os.Build; import android.os.RemoteException; -import androidx.annotation.ChecksSdkIntAtLeast; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import com.android.networkstack.apishim.common.CaptivePortalDataShim; -import com.android.networkstack.apishim.common.ShimUtils; import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import org.json.JSONException; @@ -37,8 +35,7 @@ * Compatibility implementation of {@link CaptivePortalDataShim}. */ @RequiresApi(Build.VERSION_CODES.R) -public class CaptivePortalDataShimImpl - extends com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl { +public class CaptivePortalDataShimImpl implements CaptivePortalDataShim { @NonNull protected final CaptivePortalData mData; @@ -53,16 +50,9 @@ /** * Parse a {@link CaptivePortalDataShim} from a JSON object. * @throws JSONException The JSON is not a representation of correct captive portal data. - * @throws UnsupportedApiLevelException CaptivePortalData is not available on this API level. */ - @RequiresApi(Build.VERSION_CODES.Q) @NonNull - public static CaptivePortalDataShim fromJson(JSONObject obj) throws JSONException, - UnsupportedApiLevelException { - if (!isSupported()) { - return com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl.fromJson(obj); - } - + public static CaptivePortalDataShim fromJson(JSONObject obj) throws JSONException { final long refreshTimeMs = System.currentTimeMillis(); final long secondsRemaining = getLongOrDefault(obj, "seconds-remaining", -1L); final long millisRemaining = secondsRemaining <= Long.MAX_VALUE / 1000 @@ -81,10 +71,8 @@ .build()); } - @RequiresApi(Build.VERSION_CODES.Q) - @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R) public static boolean isSupported() { - return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); + return true; } private static long getLongOrDefault(JSONObject o, String key, long def) throws JSONException { @@ -123,6 +111,18 @@ } @Override + public CharSequence getVenueFriendlyName() { + // Not supported in API level 30 + return null; + } + + @Override + public int getUserPortalUrlSource() { + // Not supported in API level 30 + return ConstantsShim.CAPTIVE_PORTAL_DATA_SOURCE_OTHER; + } + + @Override public void notifyChanged(INetworkMonitorCallbacks cb) throws RemoteException { cb.notifyCaptivePortalDataChanged(mData); }
diff --git a/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java b/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java index 5e38766..87c8e94 100644 --- a/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java +++ b/apishim/33/com/android/networkstack/apishim/api33/BroadcastOptionsShimImpl.java
@@ -31,7 +31,7 @@ */ @RequiresApi(Build.VERSION_CODES.TIRAMISU) public class BroadcastOptionsShimImpl - extends com.android.networkstack.apishim.api29.BroadcastOptionsShimImpl { + extends com.android.networkstack.apishim.api30.BroadcastOptionsShimImpl { protected BroadcastOptionsShimImpl(@NonNull BroadcastOptions options) { super(options); } @@ -42,7 +42,7 @@ @RequiresApi(Build.VERSION_CODES.TIRAMISU) public static BroadcastOptionsShim newInstance(@NonNull BroadcastOptions options) { if (!isAtLeastT()) { - return com.android.networkstack.apishim.api29.BroadcastOptionsShimImpl.newInstance( + return com.android.networkstack.apishim.api30.BroadcastOptionsShimImpl.newInstance( options); } return new BroadcastOptionsShimImpl(options);
diff --git a/src/android/net/apf/ApfFilter.java b/src/android/net/apf/ApfFilter.java index 90bd832..a40d8b6 100644 --- a/src/android/net/apf/ApfFilter.java +++ b/src/android/net/apf/ApfFilter.java
@@ -93,6 +93,7 @@ import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_SLLA_OPTION; import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_TENTATIVE; import static android.net.apf.ApfCounterTracker.Counter.PASSED_IPV6_NS_NO_ADDRESS; +import static android.net.apf.ApfCounterTracker.getCounterValue; import static android.net.apf.BaseApfGenerator.MemorySlot; import static android.net.apf.BaseApfGenerator.Register.R0; import static android.net.apf.BaseApfGenerator.Register.R1; @@ -2461,6 +2462,10 @@ rasToFilter.add(ra); } + // Increase the counter before we generate the program. + // This keeps the APF_PROGRAM_ID counter in sync with the program. + mNumProgramUpdates++; + // Step 2: Actually generate the program gen = emitPrologueLocked(); for (Ra ra : rasToFilter) { @@ -2483,7 +2488,6 @@ } mLastInstalledProgramMinLifetime = programMinLft; mLastInstalledProgram = program; - mNumProgramUpdates++; mMaxProgramSize = Math.max(mMaxProgramSize, program.length); if (VDBG) { @@ -2861,14 +2865,19 @@ return; } pw.println("Program updates: " + mNumProgramUpdates); + int filterAgeSeconds = secondsSinceBoot() - mLastTimeInstalledProgram; pw.println(String.format( "Last program length %d, installed %ds ago, lifetime %ds", - mLastInstalledProgram.length, secondsSinceBoot() - mLastTimeInstalledProgram, + mLastInstalledProgram.length, filterAgeSeconds, mLastInstalledProgramMinLifetime)); - - pw.print("Denylisted Ethertypes:"); - for (int p : mEthTypeBlackList) { - pw.print(String.format(" %04x", p)); + if (SdkLevel.isAtLeastV()) { + pw.print("Hardcoded Allowlisted Ethertypes:"); + pw.println(" 0800(IPv4) 0806(ARP) 86DD(IPv6) 888E(EAPOL) 88B4(WAPI)"); + } else { + pw.print("Denylisted Ethertypes:"); + for (int p : mEthTypeBlackList) { + pw.print(String.format(" %04x", p)); + } } pw.println(); pw.println("RA filters:"); @@ -2930,16 +2939,61 @@ } else { try { Counter[] counters = Counter.class.getEnumConstants(); + long counterFilterAgeSeconds = + getCounterValue(mDataSnapshot, Counter.FILTER_AGE_SECONDS); + long counterApfProgramId = + getCounterValue(mDataSnapshot, Counter.APF_PROGRAM_ID); for (Counter c : Arrays.asList(counters).subList(1, counters.length)) { - long value = ApfCounterTracker.getCounterValue(mDataSnapshot, c); - // Only print non-zero counters - if (value != 0) { - pw.println(c.toString() + ": " + value); + long value = getCounterValue(mDataSnapshot, c); + + String note = ""; + boolean checkValueIncreases = true; + switch (c) { + case FILTER_AGE_SECONDS: + checkValueIncreases = false; + if (value != counterFilterAgeSeconds) { + note = " [ERROR: impossible]"; + } else if (counterApfProgramId < mNumProgramUpdates) { + note = " [IGNORE: obsolete program]"; + } else if (value > filterAgeSeconds) { + long offset = value - filterAgeSeconds; + note = " [ERROR: in the future by " + offset + "s]"; + } + break; + case FILTER_AGE_16384THS: + if (mApfVersionSupported > BaseApfGenerator.APF_VERSION_4) { + checkValueIncreases = false; + if (value % 16384 == 0) { + // valid, but unlikely + note = " [INFO: zero fractional portion]"; + } + if (value / 16384 != counterFilterAgeSeconds) { + // should not be able to happen + note = " [ERROR: mismatch with FILTER_AGE_SECONDS]"; + } + } else if (value != 0) { + note = " [UNEXPECTED: APF<=4, yet non-zero]"; + } + break; + case APF_PROGRAM_ID: + if (value != counterApfProgramId) { + note = " [ERROR: impossible]"; + } else if (value < mNumProgramUpdates) { + note = " [WARNING: OBSOLETE PROGRAM]"; + } else if (value > mNumProgramUpdates) { + note = " [ERROR: INVALID FUTURE ID]"; + } + break; + default: + break; } - final Set<Counter> skipCheckCounters = Set.of(FILTER_AGE_SECONDS, - FILTER_AGE_16384THS); - if (!skipCheckCounters.contains(c)) { + // Only print non-zero counters (or those with a note) + if (value != 0 || !note.equals("")) { + pw.println(c.toString() + ": " + value + note); + } + + if (checkValueIncreases) { // If the counter's value decreases, it may have been cleaned up or there // may be a bug. long oldValue = mApfCounterTracker.getCounters().getOrDefault(c, 0L);
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java index 4b94968..76de04d 100644 --- a/src/android/net/dhcp/DhcpClient.java +++ b/src/android/net/dhcp/DhcpClient.java
@@ -104,7 +104,6 @@ import com.android.net.module.util.PacketReader; import com.android.net.module.util.arp.ArpPacket; import com.android.networkstack.R; -import com.android.networkstack.apishim.CaptivePortalDataShimImpl; import com.android.networkstack.apishim.SocketUtilsShimImpl; import com.android.networkstack.metrics.IpProvisioningMetrics; import com.android.networkstack.util.NetworkStackUtils; @@ -308,9 +307,7 @@ final ByteArrayOutputStream params = new ByteArrayOutputStream(DEFAULT_REQUESTED_PARAMS.length + numOptionalParams); params.write(DEFAULT_REQUESTED_PARAMS, 0, DEFAULT_REQUESTED_PARAMS.length); - if (isCapportApiEnabled()) { - params.write(DHCP_CAPTIVE_PORTAL); - } + params.write(DHCP_CAPTIVE_PORTAL); params.write(DHCP_IPV6_ONLY_PREFERRED); // Customized DHCP options to be put in PRL. for (DhcpOption option : mConfiguration.options) { @@ -323,10 +320,6 @@ return params.toByteArray(); } - private static boolean isCapportApiEnabled() { - return CaptivePortalDataShimImpl.isSupported(); - } - // DHCP flag that means "yes, we support unicast." private static final boolean DO_UNICAST = false; @@ -657,7 +650,6 @@ private byte[] getOptionsToSkip() { final ByteArrayOutputStream optionsToSkip = new ByteArrayOutputStream(2); - if (!isCapportApiEnabled()) optionsToSkip.write(DHCP_CAPTIVE_PORTAL); if (!mConfiguration.isWifiManagedProfile) { optionsToSkip.write(DHCP_DOMAIN_SEARCHLIST); }
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java index 0c90fe6..868c63b 100644 --- a/src/android/net/ip/IpClient.java +++ b/src/android/net/ip/IpClient.java
@@ -166,6 +166,7 @@ import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.WakeupMessage; +import com.android.modules.expresslog.Counter; import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.ConnectivityUtils; @@ -2585,7 +2586,10 @@ @Override public void notifyLost(String logMsg, NudEventType type) { maybeStoreNudFailureToDatabase(type); - if (mIgnoreNudFailure) return; + if (mIgnoreNudFailure) { + Counter.logIncrement("core_networking.value_nud_failure_ignored"); + return; + } final int version = mCallback.getInterfaceVersion(); if (version >= VERSION_ADDED_REACHABILITY_FAILURE) { final int reason = nudEventTypeToInt(type); @@ -3296,6 +3300,7 @@ sinceTimes[2] = now - SIX_HOURS_IN_MS; mIpMemoryStore.retrieveNetworkEventCount(mCluster, sinceTimes, NETWORK_EVENT_NUD_FAILURE_TYPES, mListener); + Counter.logIncrement("core_networking.value_nud_failure_queried"); } @Override
diff --git a/src/android/net/util/RawPacketTracker.java b/src/android/net/util/RawPacketTracker.java new file mode 100644 index 0000000..e73834b --- /dev/null +++ b/src/android/net/util/RawPacketTracker.java
@@ -0,0 +1,242 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.net.util; + +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; + +import android.net.ip.ConnectivityPacketTracker; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.util.ArrayMap; +import android.util.LocalLog; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.net.module.util.InterfaceParams; + +import java.util.Objects; + +/** + * Tracks and manages raw packet captures on a network interface. + * + * <p>This class is not a thread-safe and should be only run on the handler thread. + * It utilizes a dedicated {@link HandlerThread} to perform capture operations, allowing + * the caller to interact with it asynchronously through methods like + * {@link #startCapture(String, long)}, {@link #stopCapture(String)}, + * and {@link #getMatchedPacketCount(String, String)}.</p> + * + */ +public class RawPacketTracker { + /** + * Dependencies class for testing. + */ + @VisibleForTesting(visibility = PRIVATE) + static class Dependencies { + public @NonNull ConnectivityPacketTracker createPacketTracker( + Handler handler, InterfaceParams ifParams, int maxPktRecords) { + return new ConnectivityPacketTracker( + handler, ifParams, new LocalLog(maxPktRecords)); + } + + public @NonNull HandlerThread createHandlerThread() { + final HandlerThread handlerThread = new HandlerThread(TAG + "-handler"); + handlerThread.start(); + return handlerThread; + } + + public @NonNull Looper getLooper(HandlerThread handlerThread) { + return handlerThread.getLooper(); + } + } + + // Maximum number of packet records to store. + private static final int MAX_PACKET_RECORDS = 100; + // Maximum duration for a packet capture session in milliseconds. + public static final long MAX_CAPTURE_TIME_MS = 300_000; + @VisibleForTesting(visibility = PRIVATE) + public static final int CMD_STOP_CAPTURE = 1; + private static final String TAG = RawPacketTracker.class.getSimpleName(); + + private final @NonNull HandlerThread mHandlerThread; + private final @NonNull Dependencies mDeps; + private final @NonNull Handler mHandler; + + /** + * A map that stores ConnectivityPacketTracker objects, keyed by their associated + * network interface name, e.g: wlan0. This allows for tracking connectivity + * packets on a per-interface basis. This is only accessed by handler thread. + */ + private final ArrayMap<String, ConnectivityPacketTracker> mTrackerMap = new ArrayMap<>(); + + public RawPacketTracker() { + this(new Dependencies()); + } + + @VisibleForTesting(visibility = PRIVATE) + public RawPacketTracker( + @NonNull Dependencies deps + ) { + mDeps = deps; + mHandlerThread = deps.createHandlerThread(); + mHandler = new RawPacketTrackerHandler(deps.getLooper(mHandlerThread), this); + } + + private static class RawPacketTrackerHandler extends Handler { + private final RawPacketTracker mRawPacketTracker; + private RawPacketTrackerHandler( + @NonNull Looper looper, + @NonNull RawPacketTracker rawPacketTracker) { + super(looper); + mRawPacketTracker = rawPacketTracker; + } + + @Override + public void handleMessage(Message msg) { + final String ifaceName; + switch (msg.what) { + case CMD_STOP_CAPTURE: + ifaceName = (String) msg.obj; + mRawPacketTracker.processStopCapture(ifaceName); + break; + default: + Log.e(TAG, "unrecognized message: " + msg.what); + } + } + } + + /** + * Starts capturing packets on the specified network interface. + * + * <p>Initiates a packet capture session if one is not already running for the given interface. + * A capture timeout is set to automatically stop the capture after {@code maxCaptureTimeMs} + * milliseconds. If a previous stop capture event was scheduled, it is canceled.</p> + * + * @param ifaceName The name of the network interface to capture packets on. + * @param maxCaptureTimeMs The maximum capture duration in milliseconds. + * @throws IllegalArgumentException If {@code maxCaptureTimeMs} is less than or equal to 0. + * @throws RuntimeException If a capture is already running on the specified interface. + * @throws IllegalStateException If this method is not running on handler thread + */ + public void startCapture( + String ifaceName, long maxCaptureTimeMs + ) throws IllegalArgumentException, RuntimeException, IllegalStateException { + ensureRunOnHandlerThread(); + if (maxCaptureTimeMs <= 0) { + throw new IllegalArgumentException("maxCaptureTimeMs " + maxCaptureTimeMs + " <= 0"); + } + + if (mTrackerMap.containsKey(ifaceName)) { + throw new RuntimeException(ifaceName + " is already capturing"); + } + + final InterfaceParams ifParams = InterfaceParams.getByName(ifaceName); + Objects.requireNonNull(ifParams, "invalid interface " + ifaceName); + + final ConnectivityPacketTracker tracker = + mDeps.createPacketTracker(mHandler, ifParams, MAX_PACKET_RECORDS); + tracker.start(TAG + "." + ifaceName); + mTrackerMap.putIfAbsent(ifaceName, tracker); + tracker.setCapture(true); + + // remove scheduled stop events if it already in the queue + mHandler.removeMessages(CMD_STOP_CAPTURE, ifaceName); + + // capture up to configured capture time and stop capturing + final Message stopMsg = mHandler.obtainMessage(CMD_STOP_CAPTURE, ifaceName); + mHandler.sendMessageDelayed(stopMsg, maxCaptureTimeMs); + } + + /** + * Stops capturing packets on the specified network interface. + * + * <p>Terminates the packet capture session if one is active for the given interface. + * Any pending stop capture events for the interface are canceled.</p> + * + * @param ifaceName The name of the network interface to stop capturing on. + * @throws RuntimeException If no capture is running on the specified interface. + * @throws IllegalStateException If this method is not running on handler thread + */ + public void stopCapture(String ifaceName) throws RuntimeException, IllegalStateException { + ensureRunOnHandlerThread(); + if (!mTrackerMap.containsKey(ifaceName)) { + throw new RuntimeException(ifaceName + " is already stopped"); + } + + final Message msg = mHandler.obtainMessage(CMD_STOP_CAPTURE, ifaceName); + // remove scheduled stop events if it already in the queue + mHandler.removeMessages(CMD_STOP_CAPTURE, ifaceName); + mHandler.sendMessage(msg); + } + + /** + * Returns the {@link Handler} associated with this RawTracker. + * + * <p>This handler is used for posting tasks to the RawTracker's internal thread. + * You can use it to execute code that needs to interact with the RawTracker + * in a thread-safe manner. + * + * @return The non-null {@link Handler} instance. + */ + public @NonNull Handler getHandler() { + return mHandler; + } + + /** + * Retrieves the number of captured packets matching a specific pattern. + * + * <p>Queries the packet capture data for the specified interface and counts the occurrences + * of packets that match the provided {@code packet} string. The count is performed + * asynchronously on the capture thread.</p> + * + * @param ifaceName The name of the network interface. + * @param packetPattern The packet pattern to match. + * @return The number of matched packets, or 0 if an error occurs or no matching packets are + * found. + * @throws RuntimeException If no capture is running on the specified interface. + * @throws IllegalStateException If this method is not running on handler thread + */ + public int getMatchedPacketCount( + String ifaceName, String packetPattern + ) throws RuntimeException, IllegalStateException { + ensureRunOnHandlerThread(); + final ConnectivityPacketTracker tracker; + tracker = mTrackerMap.getOrDefault(ifaceName, null); + if (tracker == null) { + throw new RuntimeException(ifaceName + " is not capturing"); + } + + return tracker.getMatchedPacketCount(packetPattern); + } + + private void processStopCapture(String ifaceName) { + final ConnectivityPacketTracker tracker = mTrackerMap.get(ifaceName); + mTrackerMap.remove(ifaceName); + tracker.setCapture(false); + } + + private void ensureRunOnHandlerThread() { + if (mHandler.getLooper() != Looper.myLooper()) { + throw new IllegalStateException( + "Not running on Handler thread: " + Thread.currentThread().getName() + ); + } + } +}
diff --git a/src/android/net/util/RawSocketUtils.java b/src/android/net/util/RawSocketUtils.java index a6c8a40..5823dc4 100644 --- a/src/android/net/util/RawSocketUtils.java +++ b/src/android/net/util/RawSocketUtils.java
@@ -92,9 +92,9 @@ } @RequiresPermission(NETWORK_SETTINGS) - private static void enforceTetheredInterface(@NonNull Context context, + public static void enforceTetheredInterface(@NonNull Context context, @NonNull String interfaceName) - throws ExecutionException, InterruptedException, TimeoutException { + throws ExecutionException, InterruptedException, TimeoutException, SecurityException { final TetheringManager tm = context.getSystemService(TetheringManager.class); final CompletableFuture<List<String>> tetheredInterfaces = new CompletableFuture<>(); final TetheringManager.TetheringEventCallback callback =
diff --git a/src/com/android/server/NetworkStackService.java b/src/com/android/server/NetworkStackService.java index 4d10c3b..76b5e38 100644 --- a/src/com/android/server/NetworkStackService.java +++ b/src/com/android/server/NetworkStackService.java
@@ -19,6 +19,7 @@ import static android.net.dhcp.IDhcpServer.STATUS_INVALID_ARGUMENT; import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; import static android.net.dhcp.IDhcpServer.STATUS_UNKNOWN_ERROR; +import static android.net.util.RawPacketTracker.MAX_CAPTURE_TIME_MS; import static android.net.util.RawSocketUtils.sendRawPacketDownStream; import static com.android.net.module.util.DeviceConfigUtils.getResBooleanConfig; @@ -50,6 +51,8 @@ import android.net.ip.IpClient; import android.net.networkstack.aidl.NetworkMonitorParameters; import android.net.shared.PrivateDnsConfig; +import android.net.util.RawPacketTracker; +import android.net.util.RawSocketUtils; import android.os.Build; import android.os.HandlerThread; import android.os.IBinder; @@ -67,6 +70,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.modules.utils.BasicShellCommandHandler; import com.android.net.module.util.DeviceConfigUtils; +import com.android.net.module.util.HandlerUtils; import com.android.net.module.util.SharedLog; import com.android.networkstack.NetworkStackNotifier; import com.android.networkstack.R; @@ -90,6 +94,8 @@ import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; /** * Android service used to start the network stack when bound to via an intent. @@ -99,6 +105,7 @@ public class NetworkStackService extends Service { private static final String TAG = NetworkStackService.class.getSimpleName(); private static NetworkStackConnector sConnector; + private static final RawPacketTracker sRawPacketTracker = new RawPacketTracker(); /** * Create a binder connector for the system server to communicate with the network stack. @@ -523,6 +530,8 @@ } private class ShellCmd extends BasicShellCommandHandler { + private static final long MAX_CAPTURE_CMD_WAITING_TIMEOUT_MS = 30_000L; + @Override public int onCommand(String cmd) { if (cmd == null) { @@ -567,6 +576,14 @@ } return 0; } + case "capture": + // Usage: cmd network_stack capture <cmd> + HandlerUtils.runWithScissorsForDump( + sRawPacketTracker.getHandler(), + () -> captureShellCommand(mContext, peekRemainingArgs()), + MAX_CAPTURE_CMD_WAITING_TIMEOUT_MS + ); + return 0; case "apf": // Usage: cmd network_stack apf <iface> <cmd> final String iface = getNextArg(); @@ -609,6 +626,18 @@ pw.println(" to tethering downstream for security considerations."); pw.println(" <packet_in_hex>: A valid hexadecimal representation of "); pw.println(" a packet starting from L2 header."); + pw.println(" capture <cmd>"); + pw.println(" APF utility commands for multi-devices tests."); + pw.println(" start <interface>"); + pw.println(" start capture packets in the received buffer."); + pw.println(" The capture is up to 300 sec, then it will stop."); + pw.println(" <interface>: Target interface name, note that this is limited"); + pw.println(" to tethering downstream for security considerations."); + pw.println(" stop <interface>"); + pw.println(" stop capture packets and clear the received buffer."); + pw.println(" matched-packet-counts <interface> <pkt-hex-string>"); + pw.println(" the <pkt-hex-string> starts from ether header."); + pw.println(" Expect to do full packet match."); pw.println(" apf <iface> <cmd>"); pw.println(" APF utility commands for integration tests."); pw.println(" <iface>: the network interface the provided command operates on."); @@ -628,6 +657,73 @@ pw.println(" read"); pw.println(" reads and returns the current state of APF memory."); } + + private void captureShellCommand( + @NonNull Context context, + @NonNull String[] args + ) { + if (args.length < 2) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + final String cmd = args[0]; + final String ifaceName = args[1]; + try { + RawSocketUtils.enforceTetheredInterface(context, ifaceName); + } catch (ExecutionException + | InterruptedException + | TimeoutException + | SecurityException e) { + throw new RuntimeException(e.getMessage()); + } + + final PrintWriter pw = getOutPrintWriter(); + switch(cmd) { + case "start": + // Usage : cmd network_stack capture start <interface> + if (args.length != 2) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + sRawPacketTracker.startCapture(ifaceName, MAX_CAPTURE_TIME_MS); + pw.println("success"); + break; + case "matched-packet-counts": + // Usage : cmd network_stack capture matched-packet-counts + // <interface> <packet-in-hex> + // for example, there is an usage to get matched arp reply packet count + // in hex string format on the wlan0 interface + // cmd network_stack capture matched-packet-counts wlan0 \ + // "00010203040501020304050608060001080006040002010203040506c0a80101" + + // "000102030405c0a80102" + if (args.length != 3) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + final String packetInHex = args[2]; + + // limit the input hex string up to 3000 (1500 bytes) + if (packetInHex.length() > 3000) { + throw new IllegalArgumentException("Packet Hex String over the limit"); + } + + final int pktCnt = + sRawPacketTracker.getMatchedPacketCount(ifaceName, packetInHex); + pw.println(pktCnt); + break; + case "stop": + // Usage : cmd network_stack capture stop <interface> + if (args.length != 2) { + throw new IllegalArgumentException("Incorrect number of arguments"); + } + + sRawPacketTracker.stopCapture(ifaceName); + pw.println("success"); + break; + default: + throw new IllegalArgumentException("Invalid apf command: " + cmd); + } + } } /**
diff --git a/src/com/android/server/connectivity/NetworkMonitor.java b/src/com/android/server/connectivity/NetworkMonitor.java index 60bb927..1a36f25 100755 --- a/src/com/android/server/connectivity/NetworkMonitor.java +++ b/src/com/android/server/connectivity/NetworkMonitor.java
@@ -177,7 +177,6 @@ import com.android.networkstack.apishim.common.NetworkAgentConfigShim; import com.android.networkstack.apishim.common.NetworkInformationShim; import com.android.networkstack.apishim.common.ShimUtils; -import com.android.networkstack.apishim.common.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.metrics.NetworkValidationMetrics; @@ -611,13 +610,6 @@ } catch (RemoteException e) { version = 0; } - // The AIDL was freezed from Q beta 5 but it's unfreezing from R before releasing. In order - // to distinguish the behavior between R and Q beta 5 and before Q beta 5, add SDK and - // CODENAME check here. Basically, it's only expected to return 0 for Q beta 4 and below - // because the test result has changed. - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q - && Build.VERSION.CODENAME.equals("REL") - && version == Build.VERSION_CODES.CUR_DEVELOPMENT) version = 0; return version; } @@ -3359,11 +3351,6 @@ } catch (JSONException e) { validationLog("Could not parse capport API JSON: " + e.getMessage()); return null; - } catch (UnsupportedApiLevelException e) { - // This should never happen because LinkProperties would not have a capport URL - // before R. - validationLog("Platform API too low to support capport API"); - return null; } }
diff --git a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java index f7e1c4d..469a944 100644 --- a/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java +++ b/tests/integration/common/android/net/ip/IpClientIntegrationTestCommon.java
@@ -86,6 +86,7 @@ import static com.android.net.module.util.NetworkStackConstants.NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED; import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_AUTONOMOUS; import static com.android.net.module.util.NetworkStackConstants.PIO_FLAG_ON_LINK; +import static com.android.net.module.util.netlink.NetlinkConstants.IFF_UP; import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DEFAULT_ROUTER_VERSION; import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_INCOMPLETE_IPV6_DNS_SERVER_VERSION; @@ -219,6 +220,7 @@ import com.android.net.module.util.ip.IpNeighborMonitor; import com.android.net.module.util.ip.IpNeighborMonitor.NeighborEventConsumer; import com.android.net.module.util.netlink.NetlinkUtils; +import com.android.net.module.util.netlink.RtNetlinkLinkMessage; import com.android.net.module.util.netlink.StructNdOptPref64; import com.android.net.module.util.structs.EthernetHeader; import com.android.net.module.util.structs.IaPrefixOption; @@ -313,6 +315,7 @@ // TODO: move to NetlinkConstants, NetworkStackConstants, or OsConstants. private static final int IFA_F_STABLE_PRIVACY = 0x800; + private static final int IFNAMSIZ = 16; // To fix below AndroidLint warning: // [InlinedApi] Field requires version 3 of the U Extensions SDK (current min is 0). private static final int RTN_UNREACHABLE = @@ -5733,6 +5736,53 @@ } @Test + @SignatureRequiredTest( + reason = "NetlinkUtils.sendRtmSetLinkStateRequest requires CAP_NET_ADMIN") + public void testSendRtmSetLinkStateMethod() throws Exception { + doDualStackProvisioning(); + + // Check if the interface has been up. + assertTrue(isIfaceUp(mIfaceName)); + + // Set the interface down. + assertTrue(NetlinkUtils.sendRtmSetLinkStateRequest(mIfaceName, false)); + assertFalse(isIfaceUp(mIfaceName)); + + // Set the interface up again. + assertTrue(NetlinkUtils.sendRtmSetLinkStateRequest(mIfaceName, true)); + assertTrue(isIfaceUp(mIfaceName)); + } + + private boolean isIfaceUp(@NonNull String ifaceName) { + final RtNetlinkLinkMessage msg = NetlinkUtils.getLinkRequest(ifaceName); + assertNotNull(msg); + return (msg.getIfinfoHeader().flags & IFF_UP) != 0; + } + + @Test + @SignatureRequiredTest(reason = "NetlinkUtils.sendRtmSetLinkNameRequest requires CAP_NET_ADMIN") + public void testSendRtmSetLinkNameMethod() { + final int ifaceIndex = Os.if_nametoindex(mIfaceName); + assertNotEquals(0, ifaceIndex); + + final String newName = findUnusedIfaceName(); + assertTrue(NetlinkUtils.sendRtmSetLinkNameRequest(mIfaceName, newName)); + assertEquals(ifaceIndex, Os.if_nametoindex(newName)); + } + + private String findUnusedIfaceName() { + int suffix = 0; + while (true) { + final String newName = mIfaceName + suffix; + assertTrue(newName.length() + 1 <= IFNAMSIZ); + if (Os.if_nametoindex(newName) == 0) { + return newName; + } + suffix++; + } + } + + @Test @SignatureRequiredTest(reason = "requires mocked netd to read/write IPv6 sysctl") public void testIpv6SysctlsRestAfterStoppingIpClient() throws Exception { ProvisioningConfiguration config = new ProvisioningConfiguration.Builder()
diff --git a/tests/unit/src/android/net/util/RawPacketTrackerTest.kt b/tests/unit/src/android/net/util/RawPacketTrackerTest.kt new file mode 100644 index 0000000..cecd7a0 --- /dev/null +++ b/tests/unit/src/android/net/util/RawPacketTrackerTest.kt
@@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.net.util + +import android.net.ip.ConnectivityPacketTracker +import android.os.HandlerThread +import androidx.test.filters.SmallTest +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.FunctionalUtils.ThrowingSupplier +import com.android.testutils.assertThrows +import com.android.testutils.visibleOnHandlerThread +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.mock +import org.mockito.Mockito.timeout +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +/** + * Test for RawPacketTracker. + */ +@SmallTest +@DevSdkIgnoreRunner.MonitorThreadLeak +class RawPacketTrackerTest { + companion object { + private const val TEST_TIMEOUT_MS: Long = 1000 + private const val TEST_MAX_CAPTURE_TIME_MS: Long = 1000 + private const val TAG = "RawPacketTrackerTest" + } + + private val deps = mock(RawPacketTracker.Dependencies::class.java) + private val tracker = mock(ConnectivityPacketTracker::class.java) + private val ifaceName = "lo" + private val handlerThread by lazy { + HandlerThread("$TAG-handler-thread").apply { start() } + } + private lateinit var rawTracker: RawPacketTracker + + @Before + fun setUp() { + doReturn(handlerThread).`when`(deps).createHandlerThread() + doReturn(handlerThread.looper).`when`(deps).getLooper(any()) + doReturn(tracker).`when`(deps).createPacketTracker(any(), any(), anyInt()) + rawTracker = RawPacketTracker(deps) + } + + @After + fun tearDown() { + Mockito.framework().clearInlineMocks() + handlerThread.quitSafely() + handlerThread.join() + } + + @Test + fun testStartCapture() { + // start capturing + startCaptureOnHandler(ifaceName) + verifySetCapture(true, 1) + + assertTrue(rawTracker.handler.hasMessages(RawPacketTracker.CMD_STOP_CAPTURE)) + } + + @Test + fun testInvalidStartCapture() { + // start capturing with negative timeout + assertThrows(IllegalArgumentException::class.java) { + startCaptureOnHandler(ifaceName, -1) + } + } + + @Test + fun testStopCapture() { + // start capturing + startCaptureOnHandler(ifaceName) + // simulate capture status for stop capturing + verifySetCapture(true, 1) + + // stop capturing + stopCaptureOnHandler(ifaceName) + verifySetCapture(false, 1) + verifyZeroInteractions(tracker) + } + + @Test + fun testDuplicatedStartAndStop() { + // start capture with a long timeout + startCaptureOnHandler(ifaceName, 10_000) + verifySetCapture(true, 1) + + // start capturing for multiple times + for (i in 1..10) { + assertThrows(RuntimeException::class.java) { + startCaptureOnHandler(ifaceName) + } + } + + // expect no duplicated start capture + verifySetCapture(true, 0) + + // stop capturing for multiple times + stopCaptureOnHandler(ifaceName) + verifySetCapture(false, 1) + for (i in 1..10) { + assertThrows(RuntimeException::class.java) { + stopCaptureOnHandler(ifaceName) + } + } + + verifySetCapture(false, 0) + verifyZeroInteractions(tracker) + } + + @Test + fun testMatchedPacketCount() { + val matchedPkt = "12345" + val notMatchedPkt = "54321" + + // simulate get matched packet count + doReturn(1).`when`(tracker).getMatchedPacketCount(matchedPkt) + // simulate get not matched packet count + doReturn(0).`when`(tracker).getMatchedPacketCount(notMatchedPkt) + + // start capture + startCaptureOnHandler(ifaceName) + + assertEquals(1, getMatchedPktCntOnHandler(ifaceName, matchedPkt)) + assertEquals(0, getMatchedPktCntOnHandler(ifaceName, notMatchedPkt)) + + // for non-existed interface + val nonExistedIface = "non-existed-iface" + assertThrows(RuntimeException::class.java) { + getMatchedPktCntOnHandler(nonExistedIface, matchedPkt) + getMatchedPktCntOnHandler(nonExistedIface, notMatchedPkt) + } + + // stop capture + stopCaptureOnHandler(ifaceName) + + // expect no matched packet after stop capturing + assertThrows(RuntimeException::class.java) { + getMatchedPktCntOnHandler(ifaceName, matchedPkt) + getMatchedPktCntOnHandler(ifaceName, notMatchedPkt) + } + } + + private fun startCaptureOnHandler( + ifaceName: String, maxCaptureTime: Long = TEST_MAX_CAPTURE_TIME_MS + ) { + visibleOnHandlerThread(rawTracker.handler) { + rawTracker.startCapture(ifaceName, maxCaptureTime) + } + } + + private fun stopCaptureOnHandler(ifaceName: String) { + visibleOnHandlerThread(rawTracker.handler) { + rawTracker.stopCapture(ifaceName) + } + } + + private fun getMatchedPktCntOnHandler(ifaceName: String, packetPattern: String): Int { + return visibleOnHandlerThread(rawTracker.handler, ThrowingSupplier { + rawTracker.getMatchedPacketCount(ifaceName, packetPattern) + }) + } + + private fun verifySetCapture( + isCapture: Boolean, + receiveCnt: Int + ) { + verify(tracker, timeout(TEST_TIMEOUT_MS).times(receiveCnt)).setCapture(eq(isCapture)) + clearInvocations<Any>(tracker) + } +} \ No newline at end of file