From ce1468a2a9ff1656c3c186225b938b006c81369c Mon Sep 17 00:00:00 2001 From: Yan Yan <evitayan@google.com> Date: Wed, 13 Dec 2023 23:48:55 +0000 Subject: [PATCH] Enable NetworkMetricMonitor and support penalizing networks This CL integrates the IpSecPacketLossDetector into VCN route selection. With this change, when a data stall is reported, the network candidate will be penalized and then deprioritized during network selection. A network candidate will stop being penalized until it hitting a timeout or passing the validation. Bug: 282996138 Test: atest FrameworksVcnTests(new tests), CtsVcnTestCases Change-Id: Ifabd6fdea1d5a4fea40cf929dbab7c26d37274ac --- core/java/android/net/vcn/VcnManager.java | 14 +- .../server/vcn/VcnGatewayConnection.java | 6 + .../UnderlyingNetworkController.java | 73 +++++- .../UnderlyingNetworkEvaluator.java | 229 ++++++++++++++++- ...cnGatewayConnectionConnectedStateTest.java | 3 + .../vcn/VcnGatewayConnectionTestBase.java | 2 + .../NetworkEvaluationTestBase.java | 6 + .../UnderlyingNetworkControllerTest.java | 79 +++++- .../UnderlyingNetworkEvaluatorTest.java | 241 +++++++++++++++++- 9 files changed, 625 insertions(+), 28 deletions(-) diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 561db9c8a8ce..83b7edaec72d 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -80,8 +80,6 @@ public class VcnManager { * <p>The VCN will only migrate to a Carrier WiFi network that has a signal strength greater * than, or equal to this threshold. * - * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup. - * * @hide */ @NonNull @@ -94,8 +92,6 @@ public class VcnManager { * <p>If the VCN's selected Carrier WiFi network has a signal strength less than this threshold, * the VCN will attempt to migrate away from the Carrier WiFi network. * - * <p>WARNING: The VCN does not listen for changes to this key made after VCN startup. - * * @hide */ @NonNull @@ -120,6 +116,15 @@ public class VcnManager { public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY = "vcn_network_selection_ipsec_packet_loss_percent_threshold"; + /** + * Key for the list of timeouts in minute to stop penalizing an underlying network candidate + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY = + "vcn_network_selection_penalty_timeout_minutes_list"; + // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz /** @@ -168,6 +173,7 @@ public class VcnManager { VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY, VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY, VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY, + VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY, VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY, VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY, VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY, diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index fcc0de1c2258..3094b182093b 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -1910,6 +1910,12 @@ public class VcnGatewayConnection extends StateMachine { // Transforms do not need to be persisted; the IkeSession will keep them alive mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform); + if (direction == IpSecManager.DIRECTION_IN + && mVcnContext.isFlagNetworkMetricMonitorEnabled() + && mVcnContext.isFlagIpSecTransformStateEnabled()) { + mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform); + } + // For inbound transforms, additionally allow forwarded traffic to bridge to DUN (as // needed) final Set<Integer> exposedCaps = mConnectionConfig.getAllExposedCapabilities(); diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java index 48df44b7c4ac..3f8d39e72e89 100644 --- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java +++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java @@ -30,6 +30,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.net.ConnectivityManager; import android.net.ConnectivityManager.NetworkCallback; +import android.net.IpSecTransform; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; @@ -52,6 +53,7 @@ import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.util.IndentingPrintWriter; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.VcnContext; +import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; import com.android.server.vcn.util.LogUtils; import java.util.ArrayList; @@ -201,6 +203,14 @@ public class UnderlyingNetworkController { NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback; List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks); mCellBringupCallbacks.clear(); + + if (mVcnContext.isFlagNetworkMetricMonitorEnabled() + && mVcnContext.isFlagIpSecTransformStateEnabled()) { + for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { + evaluator.close(); + } + } + mUnderlyingNetworkRecords.clear(); // Register new callbacks. Make-before-break; always register new callbacks before removal @@ -417,11 +427,42 @@ public class UnderlyingNetworkController { if (oldSnapshot .getAllSubIdsInGroup(mSubscriptionGroup) .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) { + + if (mVcnContext.isFlagNetworkMetricMonitorEnabled() + && mVcnContext.isFlagIpSecTransformStateEnabled()) { + reevaluateNetworks(); + } return; } registerOrUpdateNetworkRequests(); } + /** + * Pass the IpSecTransform of the VCN to UnderlyingNetworkController for metric monitoring + * + * <p>Caller MUST call it when IpSecTransforms have been created for VCN creation or migration + */ + public void updateInboundTransform( + @NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) { + if (!mVcnContext.isFlagNetworkMetricMonitorEnabled() + || !mVcnContext.isFlagIpSecTransformStateEnabled()) { + logWtf("#updateInboundTransform: unexpected call; flags missing"); + return; + } + + Objects.requireNonNull(currentNetwork, "currentNetwork is null"); + Objects.requireNonNull(transform, "transform is null"); + + if (mCurrentRecord == null + || mRouteSelectionCallback == null + || !Objects.equals(currentNetwork.network, mCurrentRecord.network)) { + // The caller (VcnGatewayConnection) is out-of-dated. Ignore this call. + return; + } + + mUnderlyingNetworkRecords.get(mCurrentRecord.network).setInboundTransform(transform); + } + /** Tears down this Tracker, and releases all underlying network requests. */ public void teardown() { mVcnContext.ensureRunningOnLooperThread(); @@ -438,7 +479,7 @@ public class UnderlyingNetworkController { private TreeSet<UnderlyingNetworkEvaluator> getSortedUnderlyingNetworks() { TreeSet<UnderlyingNetworkEvaluator> sorted = - new TreeSet<>(UnderlyingNetworkEvaluator.getComparator()); + new TreeSet<>(UnderlyingNetworkEvaluator.getComparator(mVcnContext)); for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { if (evaluator.getPriorityClass() != NetworkPriorityClassifier.PRIORITY_INVALID) { @@ -525,11 +566,17 @@ public class UnderlyingNetworkController { mConnectionConfig.getVcnUnderlyingNetworkPriorities(), mSubscriptionGroup, mLastSnapshot, - mCarrierConfig)); + mCarrierConfig, + new NetworkEvaluatorCallbackImpl())); } @Override public void onLost(@NonNull Network network) { + if (mVcnContext.isFlagNetworkMetricMonitorEnabled() + && mVcnContext.isFlagIpSecTransformStateEnabled()) { + mUnderlyingNetworkRecords.get(network).close(); + } + mUnderlyingNetworkRecords.remove(network); reevaluateNetworks(); @@ -598,6 +645,21 @@ public class UnderlyingNetworkController { } } + @VisibleForTesting + class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback { + @Override + public void onEvaluationResultChanged() { + if (!mVcnContext.isFlagNetworkMetricMonitorEnabled() + || !mVcnContext.isFlagIpSecTransformStateEnabled()) { + logWtf("#onEvaluationResultChanged: unexpected call; flags missing"); + return; + } + + mVcnContext.ensureRunningOnLooperThread(); + reevaluateNetworks(); + } + } + private String getLogPrefix() { return "(" + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup) @@ -690,21 +752,22 @@ public class UnderlyingNetworkController { @VisibleForTesting(visibility = Visibility.PRIVATE) public static class Dependencies { - /** Construct a new UnderlyingNetworkEvaluator */ public UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator( @NonNull VcnContext vcnContext, @NonNull Network network, @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates, @NonNull ParcelUuid subscriptionGroup, @NonNull TelephonySubscriptionSnapshot lastSnapshot, - @Nullable PersistableBundleWrapper carrierConfig) { + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkEvaluatorCallback evaluatorCallback) { return new UnderlyingNetworkEvaluator( vcnContext, network, underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, - carrierConfig); + carrierConfig, + evaluatorCallback); } } } diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java index c124a1976ac6..2f4cf5e5d8c7 100644 --- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java +++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java @@ -16,23 +16,32 @@ package com.android.server.vcn.routeselection; +import static com.android.server.VcnManagementService.LOCAL_LOG; import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; import android.annotation.NonNull; import android.annotation.Nullable; +import android.net.IpSecTransform; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; +import android.net.vcn.VcnManager; import android.net.vcn.VcnUnderlyingNetworkTemplate; +import android.os.Handler; import android.os.ParcelUuid; +import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; import com.android.internal.util.IndentingPrintWriter; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; import com.android.server.vcn.VcnContext; +import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * UnderlyingNetworkEvaluator evaluates the quality and priority class of a network candidate for @@ -43,20 +52,41 @@ import java.util.Objects; public class UnderlyingNetworkEvaluator { private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName(); + private static final int[] PENALTY_TIMEOUT_MINUTES_DEFAULT = new int[] {5}; + @NonNull private final VcnContext mVcnContext; + @NonNull private final Handler mHandler; + @NonNull private final Object mCancellationToken = new Object(); + @NonNull private final UnderlyingNetworkRecord.Builder mNetworkRecordBuilder; + @NonNull private final NetworkEvaluatorCallback mEvaluatorCallback; + @NonNull private final List<NetworkMetricMonitor> mMetricMonitors = new ArrayList<>(); + + @NonNull private final Dependencies mDependencies; + + // TODO: Support back-off timeouts + private long mPenalizedTimeoutMs; + private boolean mIsSelected; + private boolean mIsPenalized; private int mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID; + @VisibleForTesting(visibility = Visibility.PRIVATE) public UnderlyingNetworkEvaluator( @NonNull VcnContext vcnContext, @NonNull Network network, @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates, @NonNull ParcelUuid subscriptionGroup, @NonNull TelephonySubscriptionSnapshot lastSnapshot, - @Nullable PersistableBundleWrapper carrierConfig) { + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkEvaluatorCallback evaluatorCallback, + @NonNull Dependencies dependencies) { mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext"); + mHandler = new Handler(mVcnContext.getLooper()); + + mDependencies = Objects.requireNonNull(dependencies, "Missing dependencies"); + mEvaluatorCallback = Objects.requireNonNull(evaluatorCallback, "Missing deps"); Objects.requireNonNull(underlyingNetworkTemplates, "Missing underlyingNetworkTemplates"); Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); @@ -66,9 +96,76 @@ public class UnderlyingNetworkEvaluator { new UnderlyingNetworkRecord.Builder( Objects.requireNonNull(network, "Missing network")); mIsSelected = false; + mIsPenalized = false; + mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig); updatePriorityClass( underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + if (isIpSecPacketLossDetectorEnabled()) { + try { + mMetricMonitors.add( + mDependencies.newIpSecPacketLossDetector( + mVcnContext, + mNetworkRecordBuilder.getNetwork(), + carrierConfig, + new MetricMonitorCallbackImpl())); + } catch (IllegalAccessException e) { + // No action. Do not add anything to mMetricMonitors + } + } + } + + public UnderlyingNetworkEvaluator( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @NonNull List<VcnUnderlyingNetworkTemplate> underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkEvaluatorCallback evaluatorCallback) { + this( + vcnContext, + network, + underlyingNetworkTemplates, + subscriptionGroup, + lastSnapshot, + carrierConfig, + evaluatorCallback, + new Dependencies()); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + /** Get an IpSecPacketLossDetector instance */ + public IpSecPacketLossDetector newIpSecPacketLossDetector( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkMetricMonitor.NetworkMetricMonitorCallback callback) + throws IllegalAccessException { + return new IpSecPacketLossDetector(vcnContext, network, carrierConfig, callback); + } + } + + /** Callback to notify caller to reevaluate network selection */ + public interface NetworkEvaluatorCallback { + /** + * Called when mIsPenalized changed + * + * <p>When receiving this call, UnderlyingNetworkController should reevaluate all network + * candidates for VCN underlying network selection + */ + void onEvaluationResultChanged(); + } + + private class MetricMonitorCallbackImpl + implements NetworkMetricMonitor.NetworkMetricMonitorCallback { + public void onValidationResultReceived() { + mVcnContext.ensureRunningOnLooperThread(); + + handleValidationResult(); + } } private void updatePriorityClass( @@ -91,8 +188,25 @@ public class UnderlyingNetworkEvaluator { } } - public static Comparator<UnderlyingNetworkEvaluator> getComparator() { + private boolean isIpSecPacketLossDetectorEnabled() { + return isIpSecPacketLossDetectorEnabled(mVcnContext); + } + + private static boolean isIpSecPacketLossDetectorEnabled(VcnContext vcnContext) { + return vcnContext.isFlagIpSecTransformStateEnabled() + && vcnContext.isFlagNetworkMetricMonitorEnabled(); + } + + /** Get the comparator for UnderlyingNetworkEvaluator */ + public static Comparator<UnderlyingNetworkEvaluator> getComparator(VcnContext vcnContext) { return (left, right) -> { + if (isIpSecPacketLossDetectorEnabled(vcnContext)) { + if (left.mIsPenalized != right.mIsPenalized) { + // A penalized network should have lower priority which means a larger index + return left.mIsPenalized ? 1 : -1; + } + } + final int leftIndex = left.mPriorityClass; final int rightIndex = right.mPriorityClass; @@ -112,6 +226,64 @@ public class UnderlyingNetworkEvaluator { }; } + private static long getPenaltyTimeoutMs(@Nullable PersistableBundleWrapper carrierConfig) { + final int[] timeoutMinuteList; + + if (carrierConfig != null) { + timeoutMinuteList = + carrierConfig.getIntArray( + VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY, + PENALTY_TIMEOUT_MINUTES_DEFAULT); + } else { + timeoutMinuteList = PENALTY_TIMEOUT_MINUTES_DEFAULT; + } + + // TODO: Add the support of back-off timeouts and return the full list + return TimeUnit.MINUTES.toMillis(timeoutMinuteList[0]); + } + + private void handleValidationResult() { + final boolean wasPenalized = mIsPenalized; + mIsPenalized = false; + for (NetworkMetricMonitor monitor : mMetricMonitors) { + mIsPenalized |= monitor.isValidationFailed(); + } + + if (wasPenalized == mIsPenalized) { + return; + } + + logInfo( + "#handleValidationResult: wasPenalized " + + wasPenalized + + " mIsPenalized " + + mIsPenalized); + + if (mIsPenalized) { + mHandler.postDelayed( + new ExitPenaltyBoxRunnable(), mCancellationToken, mPenalizedTimeoutMs); + } else { + // Exit the penalty box + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + } + mEvaluatorCallback.onEvaluationResultChanged(); + } + + public class ExitPenaltyBoxRunnable implements Runnable { + @Override + public void run() { + if (!mIsPenalized) { + logWtf("Evaluator not being penalized but ExitPenaltyBoxRunnable was scheduled"); + return; + } + + // TODO: There might be a future metric monitor (e.g. ping) that will require the + // validation to pass before exiting the penalty box. + mIsPenalized = false; + mEvaluatorCallback.onEvaluationResultChanged(); + } + } + /** Set the NetworkCapabilities */ public void setNetworkCapabilities( @NonNull NetworkCapabilities nc, @@ -162,6 +334,10 @@ public class UnderlyingNetworkEvaluator { updatePriorityClass( underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.setIsSelectedUnderlyingNetwork(isSelected); + } } /** @@ -174,6 +350,35 @@ public class UnderlyingNetworkEvaluator { @Nullable PersistableBundleWrapper carrierConfig) { updatePriorityClass( underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + // The already scheduled event will not be affected. The followup events will be scheduled + // with the new timeout + mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.setCarrierConfig(carrierConfig); + } + } + + /** Update the inbound IpSecTransform applied to the network */ + public void setInboundTransform(@NonNull IpSecTransform transform) { + if (!mIsSelected) { + logWtf("setInboundTransform on an unselected evaluator"); + return; + } + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.setInboundTransform(transform); + } + } + + /** Close the evaluator and stop all the underlying network metric monitors */ + public void close() { + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.close(); + } } /** Return whether this network evaluator is valid */ @@ -196,6 +401,11 @@ public class UnderlyingNetworkEvaluator { return mPriorityClass; } + /** Return whether the network is being penalized */ + public boolean isPenalized() { + return mIsPenalized; + } + /** Dump the information of this instance */ public void dump(IndentingPrintWriter pw) { pw.println("UnderlyingNetworkEvaluator:"); @@ -211,7 +421,22 @@ public class UnderlyingNetworkEvaluator { pw.println("mIsSelected: " + mIsSelected); pw.println("mPriorityClass: " + mPriorityClass); + pw.println("mIsPenalized: " + mIsPenalized); pw.decreaseIndent(); } + + private String getLogPrefix() { + return "[Network " + mNetworkRecordBuilder.getNetwork() + "] "; + } + + private void logInfo(String msg) { + Slog.i(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[INFO ] " + TAG + getLogPrefix() + msg); + } + + private void logWtf(String msg) { + Slog.wtf(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[WTF ] " + TAG + getLogPrefix() + msg); + } } diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index f84616426389..20b7f1f14691 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -269,6 +269,7 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection @Test public void testCreatedTransformsAreApplied() throws Exception { verifyVcnTransformsApplied(mGatewayConnection, false /* expectForwardTransform */); + verify(mUnderlyingNetworkController).updateInboundTransform(any(), any()); } @Test @@ -327,6 +328,8 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(direction), anyInt(), any()); } + verify(mUnderlyingNetworkController).updateInboundTransform(any(), any()); + assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); final List<ChildSaProposal> saProposals = diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index 4c7b25aaa7c3..e29e4621d571 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -223,6 +223,8 @@ public class VcnGatewayConnectionTestBase { doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider(); doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags(); doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled(); + doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled(); + doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled(); doReturn(mUnderlyingNetworkController) .when(mDeps) diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java index 355c22156a78..6015e9318464 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java @@ -26,6 +26,8 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.Context; +import android.net.IpSecConfig; +import android.net.IpSecTransform; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; @@ -141,4 +143,8 @@ public abstract class NetworkEvaluationTestBase { mock(Handler.class)); setupSystemService(mContext, mPowerManager, Context.POWER_SERVICE, PowerManager.class); } + + protected IpSecTransform makeDummyIpSecTransform() throws Exception { + return new IpSecTransform(mContext, new IpSecConfig()); + } } diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java index 992f10275739..588624b56221 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java @@ -47,6 +47,8 @@ import static org.mockito.Mockito.when; import android.content.Context; import android.net.ConnectivityManager; +import android.net.IpSecConfig; +import android.net.IpSecTransform; import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; @@ -70,6 +72,7 @@ import com.android.server.vcn.routeselection.UnderlyingNetworkController.Depende import com.android.server.vcn.routeselection.UnderlyingNetworkController.NetworkBringupCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback; import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkListener; +import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; import org.junit.Before; import org.junit.Test; @@ -153,11 +156,13 @@ public class UnderlyingNetworkControllerTest { @Mock private CarrierConfigManager mCarrierConfigManager; @Mock private TelephonySubscriptionSnapshot mSubscriptionSnapshot; @Mock private UnderlyingNetworkControllerCallback mNetworkControllerCb; + @Mock private NetworkEvaluatorCallback mEvaluatorCallback; @Mock private Network mNetwork; @Spy private Dependencies mDependencies = new Dependencies(); @Captor private ArgumentCaptor<UnderlyingNetworkListener> mUnderlyingNetworkListenerCaptor; + @Captor private ArgumentCaptor<NetworkEvaluatorCallback> mEvaluatorCallbackCaptor; private TestLooper mTestLooper; private VcnContext mVcnContext; @@ -176,7 +181,7 @@ public class UnderlyingNetworkControllerTest { mTestLooper.getLooper(), mVcnNetworkProvider, false /* isInTestMode */)); - resetVcnContext(); + resetVcnContext(mVcnContext); setupSystemService( mContext, @@ -202,10 +207,11 @@ public class UnderlyingNetworkControllerTest { .getVcnUnderlyingNetworkPriorities(), SUB_GROUP, mSubscriptionSnapshot, - null)); + null, + mEvaluatorCallback)); doReturn(mNetworkEvaluator) .when(mDependencies) - .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any()); + .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any(), any()); mUnderlyingNetworkController = new UnderlyingNetworkController( @@ -217,9 +223,11 @@ public class UnderlyingNetworkControllerTest { mDependencies); } - private void resetVcnContext() { - reset(mVcnContext); - doNothing().when(mVcnContext).ensureRunningOnLooperThread(); + private void resetVcnContext(VcnContext vcnContext) { + reset(vcnContext); + doNothing().when(vcnContext).ensureRunningOnLooperThread(); + doReturn(true).when(vcnContext).isFlagNetworkMetricMonitorEnabled(); + doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled(); } // Package private for use in NetworkPriorityClassifierTest @@ -245,11 +253,13 @@ public class UnderlyingNetworkControllerTest { final ConnectivityManager cm = mock(ConnectivityManager.class); setupSystemService(mContext, cm, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class); final VcnContext vcnContext = - new VcnContext( - mContext, - mTestLooper.getLooper(), - mVcnNetworkProvider, - true /* isInTestMode */); + spy( + new VcnContext( + mContext, + mTestLooper.getLooper(), + mVcnNetworkProvider, + true /* isInTestMode */)); + resetVcnContext(vcnContext); new UnderlyingNetworkController( vcnContext, @@ -554,6 +564,45 @@ public class UnderlyingNetworkControllerTest { verify(mNetworkEvaluator).reevaluate(any(), any(), any(), any()); } + @Test + public void testUpdateIpSecTransform() { + verifyRegistrationOnAvailableAndGetCallback(); + + final UnderlyingNetworkRecord expectedRecord = + getTestNetworkRecord( + mNetwork, + INITIAL_NETWORK_CAPABILITIES, + INITIAL_LINK_PROPERTIES, + false /* isBlocked */); + final IpSecTransform expectedTransform = new IpSecTransform(mContext, new IpSecConfig()); + + mUnderlyingNetworkController.updateInboundTransform(expectedRecord, expectedTransform); + verify(mNetworkEvaluator).setInboundTransform(expectedTransform); + } + + @Test + public void testOnEvaluationResultChanged() { + verifyRegistrationOnAvailableAndGetCallback(); + + // Verify #reevaluateNetworks is called by checking #getNetworkRecord + verify(mNetworkEvaluator).getNetworkRecord(); + + // Trigger the callback + verify(mDependencies) + .newUnderlyingNetworkEvaluator( + any(), + any(), + any(), + any(), + any(), + any(), + mEvaluatorCallbackCaptor.capture()); + mEvaluatorCallbackCaptor.getValue().onEvaluationResultChanged(); + + // Verify #reevaluateNetworks is called again + verify(mNetworkEvaluator, times(2)).getNetworkRecord(); + } + private UnderlyingNetworkListener verifyRegistrationOnAvailableAndGetCallback() { return verifyRegistrationOnAvailableAndGetCallback(INITIAL_NETWORK_CAPABILITIES); } @@ -682,7 +731,7 @@ public class UnderlyingNetworkControllerTest { cb.onBlockedStatusChanged(mNetwork, true /* isBlocked */); - verifyOnSelectedUnderlyingNetworkChanged(null); + verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null); } @Test @@ -690,6 +739,7 @@ public class UnderlyingNetworkControllerTest { UnderlyingNetworkListener cb = verifyRegistrationOnAvailableAndGetCallback(); cb.onLost(mNetwork); + verify(mNetworkEvaluator).close(); verify(mNetworkControllerCb).onSelectedUnderlyingNetworkChanged(null); } @@ -755,10 +805,11 @@ public class UnderlyingNetworkControllerTest { underlyingNetworkTemplates, SUB_GROUP, mSubscriptionSnapshot, - null)); + null, + mEvaluatorCallback)); doReturn(evaluator) .when(mDependencies) - .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any()); + .newUnderlyingNetworkEvaluator(any(), any(), any(), any(), any(), any(), any()); cb.onAvailable(network); cb.onCapabilitiesChanged(network, responseNetworkCaps); diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java index 985e70c9771e..aa81efe9a1ce 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluatorTest.java @@ -16,27 +16,65 @@ package com.android.server.vcn.routeselection; +import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY; + import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.PRIORITY_INVALID; import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyObject; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.net.IpSecTransform; import android.net.vcn.VcnGatewayConnectionConfig; -import android.os.PersistableBundle; + +import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback; +import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.Dependencies; +import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; + +import java.util.concurrent.TimeUnit; public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase { - private PersistableBundleWrapper mCarrierConfig; + private static final int PENALTY_TIMEOUT_MIN = 10; + private static final long PENALTY_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(PENALTY_TIMEOUT_MIN); + + @Mock private PersistableBundleWrapper mCarrierConfig; + @Mock private IpSecPacketLossDetector mIpSecPacketLossDetector; + @Mock private Dependencies mDependencies; + @Mock private NetworkEvaluatorCallback mEvaluatorCallback; + + @Captor private ArgumentCaptor<NetworkMetricMonitorCallback> mMetricMonitorCbCaptor; + + private UnderlyingNetworkEvaluator mNetworkEvaluator; @Before public void setUp() throws Exception { super.setUp(); - mCarrierConfig = new PersistableBundleWrapper(new PersistableBundle()); + + when(mDependencies.newIpSecPacketLossDetector(any(), any(), any(), any())) + .thenReturn(mIpSecPacketLossDetector); + + when(mCarrierConfig.getIntArray( + eq(VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY), anyObject())) + .thenReturn(new int[] {PENALTY_TIMEOUT_MIN}); + + mNetworkEvaluator = newValidUnderlyingNetworkEvaluator(); } private UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator() { @@ -46,7 +84,34 @@ public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase { VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, SUB_GROUP, mSubscriptionSnapshot, + mCarrierConfig, + mEvaluatorCallback, + mDependencies); + } + + private UnderlyingNetworkEvaluator newValidUnderlyingNetworkEvaluator() { + final UnderlyingNetworkEvaluator evaluator = newUnderlyingNetworkEvaluator(); + + evaluator.setNetworkCapabilities( + CELL_NETWORK_CAPABILITIES, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + evaluator.setLinkProperties( + LINK_PROPERTIES, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + evaluator.setIsBlocked( + false /* isBlocked */, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, mCarrierConfig); + + return evaluator; } @Test @@ -98,4 +163,174 @@ public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase { assertEquals(2, evaluator.getPriorityClass()); assertEquals(expectedRecord, evaluator.getNetworkRecord()); } + + private void checkSetSelectedNetwork(boolean isSelected) { + mNetworkEvaluator.setIsSelected( + isSelected, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + verify(mIpSecPacketLossDetector).setIsSelectedUnderlyingNetwork(isSelected); + } + + @Test + public void testSetIsSelected_selected() throws Exception { + checkSetSelectedNetwork(true /* isSelectedExpected */); + } + + @Test + public void testSetIsSelected_unselected() throws Exception { + checkSetSelectedNetwork(false /* isSelectedExpected */); + } + + @Test + public void testSetIpSecTransform_onSelectedNetwork() throws Exception { + final IpSecTransform transform = makeDummyIpSecTransform(); + + // Make the network selected + mNetworkEvaluator.setIsSelected( + true, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + mNetworkEvaluator.setInboundTransform(transform); + + verify(mIpSecPacketLossDetector).setInboundTransform(transform); + } + + @Test + public void testSetIpSecTransform_onUnSelectedNetwork() throws Exception { + mNetworkEvaluator.setIsSelected( + false, + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + mNetworkEvaluator.setInboundTransform(makeDummyIpSecTransform()); + + verify(mIpSecPacketLossDetector, never()).setInboundTransform(any()); + } + + @Test + public void close() throws Exception { + mNetworkEvaluator.close(); + + verify(mIpSecPacketLossDetector).close(); + mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS); + assertNull(mTestLooper.nextMessage()); + } + + private NetworkMetricMonitorCallback getMetricMonitorCbCaptor() throws Exception { + verify(mDependencies) + .newIpSecPacketLossDetector(any(), any(), any(), mMetricMonitorCbCaptor.capture()); + + return mMetricMonitorCbCaptor.getValue(); + } + + private void checkPenalizeNetwork() throws Exception { + assertFalse(mNetworkEvaluator.isPenalized()); + + // Validation failed + when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(true); + getMetricMonitorCbCaptor().onValidationResultReceived(); + + // Verify the evaluator is penalized + assertTrue(mNetworkEvaluator.isPenalized()); + verify(mEvaluatorCallback).onEvaluationResultChanged(); + } + + @Test + public void testRcvValidationResult_penalizeNetwork_penaltyTimeout() throws Exception { + checkPenalizeNetwork(); + + // Penalty timeout + mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS); + mTestLooper.dispatchAll(); + + // Verify the evaluator is not penalized + assertFalse(mNetworkEvaluator.isPenalized()); + verify(mEvaluatorCallback, times(2)).onEvaluationResultChanged(); + } + + @Test + public void testRcvValidationResult_penalizeNetwork_passValidation() throws Exception { + checkPenalizeNetwork(); + + // Validation passed + when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(false); + getMetricMonitorCbCaptor().onValidationResultReceived(); + + // Verify the evaluator is not penalized and penalty timeout is canceled + assertFalse(mNetworkEvaluator.isPenalized()); + verify(mEvaluatorCallback, times(2)).onEvaluationResultChanged(); + mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS); + assertNull(mTestLooper.nextMessage()); + } + + @Test + public void testRcvValidationResult_penalizeNetwork_closeEvaluator() throws Exception { + checkPenalizeNetwork(); + + mNetworkEvaluator.close(); + + // Verify penalty timeout is canceled + mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS); + assertNull(mTestLooper.nextMessage()); + } + + @Test + public void testRcvValidationResult_PenaltyStateUnchanged() throws Exception { + assertFalse(mNetworkEvaluator.isPenalized()); + + // Validation passed + when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(false); + getMetricMonitorCbCaptor().onValidationResultReceived(); + + // Verifications + assertFalse(mNetworkEvaluator.isPenalized()); + verify(mEvaluatorCallback, never()).onEvaluationResultChanged(); + } + + @Test + public void testSetCarrierConfig() throws Exception { + final int additionalTimeoutMin = 10; + when(mCarrierConfig.getIntArray( + eq(VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY), anyObject())) + .thenReturn(new int[] {PENALTY_TIMEOUT_MIN + additionalTimeoutMin}); + + // Update evaluator and penalize the network + mNetworkEvaluator.reevaluate( + VcnGatewayConnectionConfig.DEFAULT_UNDERLYING_NETWORK_TEMPLATES, + SUB_GROUP, + mSubscriptionSnapshot, + mCarrierConfig); + checkPenalizeNetwork(); + + // Verify penalty timeout is changed + mTestLooper.moveTimeForward(PENALTY_TIMEOUT_MS); + assertNull(mTestLooper.nextMessage()); + mTestLooper.moveTimeForward(TimeUnit.MINUTES.toMillis(additionalTimeoutMin)); + assertNotNull(mTestLooper.nextMessage()); + + // Verify NetworkMetricMonitor is notified + verify(mIpSecPacketLossDetector).setCarrierConfig(any()); + } + + @Test + public void testCompare() throws Exception { + when(mIpSecPacketLossDetector.isValidationFailed()).thenReturn(true); + getMetricMonitorCbCaptor().onValidationResultReceived(); + + final UnderlyingNetworkEvaluator penalized = mNetworkEvaluator; + final UnderlyingNetworkEvaluator notPenalized = newValidUnderlyingNetworkEvaluator(); + + assertEquals(penalized.getPriorityClass(), notPenalized.getPriorityClass()); + + final int result = + UnderlyingNetworkEvaluator.getComparator(mVcnContext) + .compare(penalized, notPenalized); + assertEquals(1, result); + } } -- GitLab