Support IPsec packet loss detector

This CL creates an abstract class NetworkMetricMonitor and a
subclass IpSecPacketLossDetector. IpSecPacketLossDetector
is responsible for continuously monitoring IPsec packet loss
and reporting to the caller when the data loss rate surpass
a threshold. This detector allows the VCN to switch from
the underlying network that has bad quality.

This CL also defines new carrier configs that specify the
data loss threshold and detecting frequency

Bug: 282996138
Test: atest FrameworksVcnTests(new tests), CtsVcnTestCases
Change-Id: I5f4b8e1821bdbb37f0a6de6e1584f8e3c87273f3
This commit is contained in:
Yan Yan 2023-12-08 18:48:01 +00:00
commit 421db12030
9 changed files with 1141 additions and 6 deletions

View file

@ -12,4 +12,11 @@ flag {
namespace: "vcn"
description: "Feature flag for adjustable safe mode timeout"
bug: "317406085"
}
flag{
name: "network_metric_monitor"
namespace: "vcn"
description: "Feature flag for enabling network metric monitor"
bug: "282996138"
}

View file

@ -102,6 +102,24 @@ public class VcnManager {
public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY =
"vcn_network_selection_wifi_exit_rssi_threshold";
/**
* Key for the interval to poll IpSecTransformState for packet loss monitoring
*
* @hide
*/
@NonNull
public static final String VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY =
"vcn_network_selection_poll_ipsec_state_interval_seconds";
/**
* Key for the threshold of IPSec packet loss rate
*
* @hide
*/
@NonNull
public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY =
"vcn_network_selection_ipsec_packet_loss_percent_threshold";
// TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz
/**
@ -148,6 +166,8 @@ public class VcnManager {
new String[] {
VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY,
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_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY,
VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY,
VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY,

View file

@ -34,6 +34,7 @@ public class VcnContext {
@NonNull private final Looper mLooper;
@NonNull private final VcnNetworkProvider mVcnNetworkProvider;
@NonNull private final FeatureFlags mFeatureFlags;
@NonNull private final com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
private final boolean mIsInTestMode;
public VcnContext(
@ -48,6 +49,7 @@ public class VcnContext {
// Auto-generated class
mFeatureFlags = new FeatureFlagsImpl();
mCoreNetFeatureFlags = new com.android.net.flags.FeatureFlagsImpl();
}
@NonNull
@ -69,6 +71,14 @@ public class VcnContext {
return mIsInTestMode;
}
public boolean isFlagNetworkMetricMonitorEnabled() {
return mFeatureFlags.networkMetricMonitor();
}
public boolean isFlagIpSecTransformStateEnabled() {
return mCoreNetFeatureFlags.ipsecTransformState();
}
@NonNull
public FeatureFlags getFeatureFlags() {
return mFeatureFlags;

View file

@ -0,0 +1,387 @@
/*
* Copyright (C) 2023 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.server.vcn.routeselection;
import static com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.IpSecTransformState;
import android.net.Network;
import android.net.vcn.VcnManager;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.OutcomeReceiver;
import android.os.PowerManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.VcnContext;
import java.util.BitSet;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* IpSecPacketLossDetector is responsible for continuously monitoring IPsec packet loss
*
* <p>When the packet loss rate surpass the threshold, IpSecPacketLossDetector will report it to the
* caller
*
* <p>IpSecPacketLossDetector will start monitoring when the network being monitored is selected AND
* an inbound IpSecTransform has been applied to this network.
*
* <p>This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state"
*/
public class IpSecPacketLossDetector extends NetworkMetricMonitor {
private static final String TAG = IpSecPacketLossDetector.class.getSimpleName();
@VisibleForTesting(visibility = Visibility.PRIVATE)
static final int PACKET_LOSS_UNAVALAIBLE = -1;
// For VoIP, losses between 5% and 10% of the total packet stream will affect the quality
// significantly (as per "Computer Networking for LANS to WANS: Hardware, Software and
// Security"). For audio and video streaming, above 10-12% packet loss is unacceptable (as per
// "ICTP-SDU: About PingER"). Thus choose 12% as a conservative default threshold to declare a
// validation failure.
private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT = 12;
private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20;
private long mPollIpSecStateIntervalMs;
private final int mPacketLossRatePercentThreshold;
@NonNull private final Handler mHandler;
@NonNull private final PowerManager mPowerManager;
@NonNull private final Object mCancellationToken = new Object();
@NonNull private final PacketLossCalculator mPacketLossCalculator;
@Nullable private IpSecTransformWrapper mInboundTransform;
@Nullable private IpSecTransformState mLastIpSecTransformState;
@VisibleForTesting(visibility = Visibility.PRIVATE)
public IpSecPacketLossDetector(
@NonNull VcnContext vcnContext,
@NonNull Network network,
@Nullable PersistableBundleWrapper carrierConfig,
@NonNull NetworkMetricMonitorCallback callback,
@NonNull Dependencies deps)
throws IllegalAccessException {
super(vcnContext, network, carrierConfig, callback);
Objects.requireNonNull(deps, "Missing deps");
if (!vcnContext.isFlagIpSecTransformStateEnabled()) {
// Caller error
logWtf("ipsecTransformState flag disabled");
throw new IllegalAccessException("ipsecTransformState flag disabled");
}
mHandler = new Handler(getVcnContext().getLooper());
mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class);
mPacketLossCalculator = deps.getPacketLossCalculator();
mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig);
// Register for system broadcasts to monitor idle mode change
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
getVcnContext()
.getContext()
.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals(
intent.getAction())
&& mPowerManager.isDeviceIdleMode()) {
mLastIpSecTransformState = null;
}
}
},
intentFilter,
null /* broadcastPermission not required */,
mHandler);
}
public IpSecPacketLossDetector(
@NonNull VcnContext vcnContext,
@NonNull Network network,
@Nullable PersistableBundleWrapper carrierConfig,
@NonNull NetworkMetricMonitorCallback callback)
throws IllegalAccessException {
this(vcnContext, network, carrierConfig, callback, new Dependencies());
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
public static class Dependencies {
public PacketLossCalculator getPacketLossCalculator() {
return new PacketLossCalculator();
}
}
private static long getPollIpSecStateIntervalMs(
@Nullable PersistableBundleWrapper carrierConfig) {
final int seconds;
if (carrierConfig != null) {
seconds =
carrierConfig.getInt(
VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY,
POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT);
} else {
seconds = POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT;
}
return TimeUnit.SECONDS.toMillis(seconds);
}
private static int getPacketLossRatePercentThreshold(
@Nullable PersistableBundleWrapper carrierConfig) {
if (carrierConfig != null) {
return carrierConfig.getInt(
VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY,
IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT);
}
return IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT;
}
@Override
protected void onSelectedUnderlyingNetworkChanged() {
if (!isSelectedUnderlyingNetwork()) {
mInboundTransform = null;
stop();
}
// No action when the underlying network got selected. Wait for the inbound transform to
// start the monitor
}
@Override
public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inboundTransform) {
Objects.requireNonNull(inboundTransform, "inboundTransform is null");
if (Objects.equals(inboundTransform, mInboundTransform)) {
return;
}
if (!isSelectedUnderlyingNetwork()) {
logWtf("setInboundTransform called but network not selected");
return;
}
// When multiple parallel inbound transforms are created, NetworkMetricMonitor will be
// enabled on the last one as a sample
mInboundTransform = inboundTransform;
start();
}
@Override
public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
// The already scheduled event will not be affected. The followup events will be scheduled
// with the new interval
mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig);
}
@Override
protected void start() {
super.start();
clearTransformStateAndPollingEvents();
mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L);
}
@Override
public void stop() {
super.stop();
clearTransformStateAndPollingEvents();
}
private void clearTransformStateAndPollingEvents() {
mHandler.removeCallbacksAndEqualMessages(mCancellationToken);
mLastIpSecTransformState = null;
}
@Override
public void close() {
super.close();
if (mInboundTransform != null) {
mInboundTransform.close();
}
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
@Nullable
public IpSecTransformState getLastTransformState() {
return mLastIpSecTransformState;
}
@VisibleForTesting(visibility = Visibility.PROTECTED)
@Nullable
public IpSecTransformWrapper getInboundTransformInternal() {
return mInboundTransform;
}
private class PollIpSecStateRunnable implements Runnable {
@Override
public void run() {
if (!isStarted()) {
logWtf("Monitor stopped but PollIpSecStateRunnable not removed from Handler");
return;
}
getInboundTransformInternal()
.getIpSecTransformState(
new HandlerExecutor(mHandler), new IpSecTransformStateReceiver());
// Schedule for next poll
mHandler.postDelayed(
new PollIpSecStateRunnable(), mCancellationToken, mPollIpSecStateIntervalMs);
}
}
private class IpSecTransformStateReceiver
implements OutcomeReceiver<IpSecTransformState, RuntimeException> {
@Override
public void onResult(@NonNull IpSecTransformState state) {
getVcnContext().ensureRunningOnLooperThread();
if (!isStarted()) {
return;
}
onIpSecTransformStateReceived(state);
}
@Override
public void onError(@NonNull RuntimeException error) {
getVcnContext().ensureRunningOnLooperThread();
// Nothing we can do here
logW("TransformStateReceiver#onError " + error.toString());
}
}
private void onIpSecTransformStateReceived(@NonNull IpSecTransformState state) {
if (mLastIpSecTransformState == null) {
// This is first time to poll the state
mLastIpSecTransformState = state;
return;
}
final int packetLossRate =
mPacketLossCalculator.getPacketLossRatePercentage(
mLastIpSecTransformState, state, getLogPrefix());
if (packetLossRate == PACKET_LOSS_UNAVALAIBLE) {
return;
}
final String logMsg =
"packetLossRate: "
+ packetLossRate
+ "% in the past "
+ (state.getTimestamp() - mLastIpSecTransformState.getTimestamp())
+ "ms";
mLastIpSecTransformState = state;
if (packetLossRate < mPacketLossRatePercentThreshold) {
logV(logMsg);
onValidationResultReceivedInternal(false /* isFailed */);
} else {
logInfo(logMsg);
onValidationResultReceivedInternal(true /* isFailed */);
}
}
@VisibleForTesting(visibility = Visibility.PRIVATE)
public static class PacketLossCalculator {
/** Calculate the packet loss rate between two timestamps */
public int getPacketLossRatePercentage(
@NonNull IpSecTransformState oldState,
@NonNull IpSecTransformState newState,
String logPrefix) {
logVIpSecTransform("oldState", oldState, logPrefix);
logVIpSecTransform("newState", newState, logPrefix);
final int replayWindowSize = oldState.getReplayBitmap().length * 8;
final long oldSeqHi = oldState.getRxHighestSequenceNumber();
final long oldSeqLow = Math.max(0L, oldSeqHi - replayWindowSize + 1);
final long newSeqHi = newState.getRxHighestSequenceNumber();
final long newSeqLow = Math.max(0L, newSeqHi - replayWindowSize + 1);
if (oldSeqHi == newSeqHi || newSeqHi < replayWindowSize) {
// The replay window did not proceed and all packets might have been delivered out
// of order
return PACKET_LOSS_UNAVALAIBLE;
}
// Get the expected packet count by assuming there is no packet loss. In this case, SA
// should receive all packets whose sequence numbers are smaller than the lower bound of
// the replay window AND the packets received within the window.
// When the lower bound is 0, it's not possible to tell whether packet with seqNo 0 is
// received or not. For simplicity just assume that packet is received.
final long newExpectedPktCnt = newSeqLow + getPacketCntInReplayWindow(newState);
final long oldExpectedPktCnt = oldSeqLow + getPacketCntInReplayWindow(oldState);
final long expectedPktCntDiff = newExpectedPktCnt - oldExpectedPktCnt;
final long actualPktCntDiff = newState.getPacketCount() - oldState.getPacketCount();
logV(
TAG,
logPrefix
+ " expectedPktCntDiff: "
+ expectedPktCntDiff
+ " actualPktCntDiff: "
+ actualPktCntDiff);
if (expectedPktCntDiff < 0
|| expectedPktCntDiff == 0
|| actualPktCntDiff < 0
|| actualPktCntDiff > expectedPktCntDiff) {
logWtf(TAG, "Impossible values for expectedPktCntDiff or" + " actualPktCntDiff");
return PACKET_LOSS_UNAVALAIBLE;
}
return 100 - (int) (actualPktCntDiff * 100 / expectedPktCntDiff);
}
}
private static void logVIpSecTransform(
String transformTag, IpSecTransformState state, String logPrefix) {
final String stateString =
" seqNo: "
+ state.getRxHighestSequenceNumber()
+ " | pktCnt: "
+ state.getPacketCount()
+ " | pktCntInWindow: "
+ getPacketCntInReplayWindow(state);
logV(TAG, logPrefix + " " + transformTag + stateString);
}
/** Get the number of received packets within the replay window */
private static long getPacketCntInReplayWindow(@NonNull IpSecTransformState state) {
return BitSet.valueOf(state.getReplayBitmap()).cardinality();
}
}

View file

@ -0,0 +1,269 @@
/*
* Copyright (C) 2023 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.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.IpSecTransformState;
import android.net.Network;
import android.os.OutcomeReceiver;
import android.util.CloseGuard;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.VcnContext;
import java.util.Objects;
import java.util.concurrent.Executor;
/**
* NetworkMetricMonitor is responsible for managing metric monitoring and tracking validation
* results.
*
* <p>This class is flag gated by "network_metric_monitor"
*/
public abstract class NetworkMetricMonitor implements AutoCloseable {
private static final String TAG = NetworkMetricMonitor.class.getSimpleName();
private static final boolean VDBG = false; // STOPSHIP: if true
@NonNull private final CloseGuard mCloseGuard = new CloseGuard();
@NonNull private final VcnContext mVcnContext;
@NonNull private final Network mNetwork;
@NonNull private final NetworkMetricMonitorCallback mCallback;
private boolean mIsSelectedUnderlyingNetwork;
private boolean mIsStarted;
private boolean mIsValidationFailed;
protected NetworkMetricMonitor(
@NonNull VcnContext vcnContext,
@NonNull Network network,
@Nullable PersistableBundleWrapper carrierConfig,
@NonNull NetworkMetricMonitorCallback callback)
throws IllegalAccessException {
if (!vcnContext.isFlagNetworkMetricMonitorEnabled()) {
// Caller error
logWtf("networkMetricMonitor flag disabled");
throw new IllegalAccessException("networkMetricMonitor flag disabled");
}
mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext");
mNetwork = Objects.requireNonNull(network, "Missing network");
mCallback = Objects.requireNonNull(callback, "Missing callback");
mIsSelectedUnderlyingNetwork = false;
mIsStarted = false;
mIsValidationFailed = false;
}
/** Callback to notify caller of the validation result */
public interface NetworkMetricMonitorCallback {
/** Called when there is a validation result is ready */
void onValidationResultReceived();
}
/**
* Start monitoring
*
* <p>This method might be called on a an already started monitor for updating monitor
* properties (e.g. IpSecTransform, carrier config)
*
* <p>Subclasses MUST call super.start() when overriding this method
*/
protected void start() {
mIsStarted = true;
}
/**
* Stop monitoring
*
* <p>Subclasses MUST call super.stop() when overriding this method
*/
public void stop() {
mIsValidationFailed = false;
mIsStarted = false;
}
/** Called by the subclasses when the validation result is ready */
protected void onValidationResultReceivedInternal(boolean isFailed) {
mIsValidationFailed = isFailed;
mCallback.onValidationResultReceived();
}
/** Called when the underlying network changes to selected or unselected */
protected abstract void onSelectedUnderlyingNetworkChanged();
/**
* Mark the network being monitored selected or unselected
*
* <p>Subclasses MUST call super when overriding this method
*/
public void setIsSelectedUnderlyingNetwork(boolean isSelectedUnderlyingNetwork) {
if (mIsSelectedUnderlyingNetwork == isSelectedUnderlyingNetwork) {
return;
}
mIsSelectedUnderlyingNetwork = isSelectedUnderlyingNetwork;
onSelectedUnderlyingNetworkChanged();
}
/** Wrapper that allows injection for testing purposes */
@VisibleForTesting(visibility = Visibility.PROTECTED)
public static class IpSecTransformWrapper {
@NonNull public final IpSecTransform ipSecTransform;
public IpSecTransformWrapper(@NonNull IpSecTransform ipSecTransform) {
this.ipSecTransform = ipSecTransform;
}
/** Poll an IpSecTransformState */
public void getIpSecTransformState(
@NonNull Executor executor,
@NonNull OutcomeReceiver<IpSecTransformState, RuntimeException> callback) {
ipSecTransform.getIpSecTransformState(executor, callback);
}
/** Close this instance and release the underlying resources */
public void close() {
ipSecTransform.close();
}
@Override
public int hashCode() {
return Objects.hash(ipSecTransform);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof IpSecTransformWrapper)) {
return false;
}
final IpSecTransformWrapper other = (IpSecTransformWrapper) o;
return Objects.equals(ipSecTransform, other.ipSecTransform);
}
}
/** Set the IpSecTransform that applied to the Network being monitored */
public void setInboundTransform(@NonNull IpSecTransform inTransform) {
setInboundTransformInternal(new IpSecTransformWrapper(inTransform));
}
/**
* Set the IpSecTransform that applied to the Network being monitored *
*
* <p>Subclasses MUST call super when overriding this method
*/
@VisibleForTesting(visibility = Visibility.PRIVATE)
public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inTransform) {
// Subclasses MUST override it if they care
}
/** Update the carrierconfig */
public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) {
// Subclasses MUST override it if they care
}
public boolean isValidationFailed() {
return mIsValidationFailed;
}
public boolean isSelectedUnderlyingNetwork() {
return mIsSelectedUnderlyingNetwork;
}
public boolean isStarted() {
return mIsStarted;
}
@NonNull
public VcnContext getVcnContext() {
return mVcnContext;
}
// Override methods for AutoCloseable. Subclasses MUST call super when overriding this method
@Override
public void close() {
mCloseGuard.close();
stop();
}
// Override #finalize() to use closeGuard for flagging that #close() was not called
@SuppressWarnings("Finalize")
@Override
protected void finalize() throws Throwable {
try {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
close();
} finally {
super.finalize();
}
}
private String getClassName() {
return this.getClass().getSimpleName();
}
protected String getLogPrefix() {
return " [Network " + mNetwork + "] ";
}
protected void logV(String msg) {
if (VDBG) {
Slog.v(getClassName(), getLogPrefix() + msg);
LOCAL_LOG.log("[VERBOSE ] " + getClassName() + getLogPrefix() + msg);
}
}
protected void logInfo(String msg) {
Slog.i(getClassName(), getLogPrefix() + msg);
LOCAL_LOG.log("[INFO ] " + getClassName() + getLogPrefix() + msg);
}
protected void logW(String msg) {
Slog.w(getClassName(), getLogPrefix() + msg);
LOCAL_LOG.log("[WARN ] " + getClassName() + getLogPrefix() + msg);
}
protected void logWtf(String msg) {
Slog.wtf(getClassName(), getLogPrefix() + msg);
LOCAL_LOG.log("[WTF ] " + getClassName() + getLogPrefix() + msg);
}
protected static void logV(String className, String msgWithPrefix) {
if (VDBG) {
Slog.wtf(className, msgWithPrefix);
LOCAL_LOG.log("[VERBOSE ] " + className + msgWithPrefix);
}
}
protected static void logWtf(String className, String msgWithPrefix) {
Slog.wtf(className, msgWithPrefix);
LOCAL_LOG.log("[WTF ] " + className + msgWithPrefix);
}
}

View file

@ -0,0 +1,419 @@
/*
* Copyright (C) 2023 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.server.vcn.routeselection;
import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY;
import static android.net.vcn.VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY;
import static com.android.server.vcn.routeselection.IpSecPacketLossDetector.PACKET_LOSS_UNAVALAIBLE;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.BroadcastReceiver;
import android.content.Intent;
import android.net.IpSecTransformState;
import android.os.OutcomeReceiver;
import android.os.PowerManager;
import com.android.server.vcn.routeselection.IpSecPacketLossDetector.PacketLossCalculator;
import com.android.server.vcn.routeselection.NetworkMetricMonitor.IpSecTransformWrapper;
import com.android.server.vcn.routeselection.NetworkMetricMonitor.NetworkMetricMonitorCallback;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Spy;
import java.util.Arrays;
import java.util.BitSet;
import java.util.concurrent.TimeUnit;
public class IpSecPacketLossDetectorTest extends NetworkEvaluationTestBase {
private static final String TAG = IpSecPacketLossDetectorTest.class.getSimpleName();
private static final int REPLAY_BITMAP_LEN_BYTE = 512;
private static final int REPLAY_BITMAP_LEN_BIT = REPLAY_BITMAP_LEN_BYTE * 8;
private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD = 5;
private static final long POLL_IPSEC_STATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30L);
@Mock private IpSecTransformWrapper mIpSecTransform;
@Mock private NetworkMetricMonitorCallback mMetricMonitorCallback;
@Mock private PersistableBundleWrapper mCarrierConfig;
@Mock private IpSecPacketLossDetector.Dependencies mDependencies;
@Spy private PacketLossCalculator mPacketLossCalculator = new PacketLossCalculator();
@Captor private ArgumentCaptor<OutcomeReceiver> mTransformStateReceiverCaptor;
@Captor private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverCaptor;
private IpSecPacketLossDetector mIpSecPacketLossDetector;
private IpSecTransformState mTransformStateInitial;
@Before
public void setUp() throws Exception {
super.setUp();
mTransformStateInitial = newTransformState(0, 0, newReplayBitmap(0));
when(mCarrierConfig.getInt(
eq(VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY), anyInt()))
.thenReturn((int) TimeUnit.MILLISECONDS.toSeconds(POLL_IPSEC_STATE_INTERVAL_MS));
when(mCarrierConfig.getInt(
eq(VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY),
anyInt()))
.thenReturn(IPSEC_PACKET_LOSS_PERCENT_THRESHOLD);
when(mDependencies.getPacketLossCalculator()).thenReturn(mPacketLossCalculator);
mIpSecPacketLossDetector =
new IpSecPacketLossDetector(
mVcnContext,
mNetwork,
mCarrierConfig,
mMetricMonitorCallback,
mDependencies);
}
private static IpSecTransformState newTransformState(
long rxSeqNo, long packtCount, byte[] replayBitmap) {
return new IpSecTransformState.Builder()
.setRxHighestSequenceNumber(rxSeqNo)
.setPacketCount(packtCount)
.setReplayBitmap(replayBitmap)
.build();
}
private static byte[] newReplayBitmap(int receivedPktCnt) {
final BitSet bitSet = new BitSet(REPLAY_BITMAP_LEN_BIT);
for (int i = 0; i < receivedPktCnt; i++) {
bitSet.set(i);
}
return Arrays.copyOf(bitSet.toByteArray(), REPLAY_BITMAP_LEN_BYTE);
}
private void verifyStopped() {
assertFalse(mIpSecPacketLossDetector.isStarted());
assertFalse(mIpSecPacketLossDetector.isValidationFailed());
assertNull(mIpSecPacketLossDetector.getLastTransformState());
// No event scheduled
mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
assertNull(mTestLooper.nextMessage());
}
@Test
public void testInitialization() throws Exception {
assertFalse(mIpSecPacketLossDetector.isSelectedUnderlyingNetwork());
verifyStopped();
}
private OutcomeReceiver<IpSecTransformState, RuntimeException>
startMonitorAndCaptureStateReceiver() {
mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
// Trigger the runnable
mTestLooper.dispatchAll();
verify(mIpSecTransform)
.getIpSecTransformState(any(), mTransformStateReceiverCaptor.capture());
return mTransformStateReceiverCaptor.getValue();
}
@Test
public void testStartMonitor() throws Exception {
final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
startMonitorAndCaptureStateReceiver();
assertTrue(mIpSecPacketLossDetector.isStarted());
assertFalse(mIpSecPacketLossDetector.isValidationFailed());
assertTrue(mIpSecPacketLossDetector.isSelectedUnderlyingNetwork());
assertEquals(mIpSecTransform, mIpSecPacketLossDetector.getInboundTransformInternal());
// Mock receiving a state
xfrmStateReceiver.onResult(mTransformStateInitial);
// Verify the first polled state is stored
assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
verify(mPacketLossCalculator, never())
.getPacketLossRatePercentage(any(), any(), anyString());
// Verify next poll is scheduled
assertNull(mTestLooper.nextMessage());
mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
assertNotNull(mTestLooper.nextMessage());
}
@Test
public void testStartedMonitor_enterDozeMoze() throws Exception {
final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
startMonitorAndCaptureStateReceiver();
// Mock receiving a state
xfrmStateReceiver.onResult(mTransformStateInitial);
assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
// Mock entering doze mode
final Intent intent = mock(Intent.class);
when(intent.getAction()).thenReturn(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED);
when(mPowerManagerService.isDeviceIdleMode()).thenReturn(true);
verify(mContext).registerReceiver(mBroadcastReceiverCaptor.capture(), any(), any(), any());
final BroadcastReceiver broadcastReceiver = mBroadcastReceiverCaptor.getValue();
broadcastReceiver.onReceive(mContext, intent);
assertNull(mIpSecPacketLossDetector.getLastTransformState());
}
@Test
public void testStartedMonitor_updateInboundTransform() throws Exception {
final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
startMonitorAndCaptureStateReceiver();
// Mock receiving a state
xfrmStateReceiver.onResult(mTransformStateInitial);
assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
// Update the inbound transform
final IpSecTransformWrapper newTransform = mock(IpSecTransformWrapper.class);
mIpSecPacketLossDetector.setInboundTransformInternal(newTransform);
// Verifications
assertNull(mIpSecPacketLossDetector.getLastTransformState());
mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
mTestLooper.dispatchAll();
verify(newTransform).getIpSecTransformState(any(), any());
}
@Test
public void testStartedMonitor_updateCarrierConfig() throws Exception {
startMonitorAndCaptureStateReceiver();
final int additionalPollIntervalMs = (int) TimeUnit.SECONDS.toMillis(10L);
when(mCarrierConfig.getInt(
eq(VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY), anyInt()))
.thenReturn(
(int)
TimeUnit.MILLISECONDS.toSeconds(
POLL_IPSEC_STATE_INTERVAL_MS + additionalPollIntervalMs));
mIpSecPacketLossDetector.setCarrierConfig(mCarrierConfig);
mTestLooper.dispatchAll();
// The already scheduled event is still fired with the old timeout
mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
mTestLooper.dispatchAll();
// The next scheduled event will take 10 more seconds to fire
mTestLooper.moveTimeForward(POLL_IPSEC_STATE_INTERVAL_MS);
assertNull(mTestLooper.nextMessage());
mTestLooper.moveTimeForward(additionalPollIntervalMs);
assertNotNull(mTestLooper.nextMessage());
}
@Test
public void testStopMonitor() throws Exception {
mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
assertTrue(mIpSecPacketLossDetector.isStarted());
assertNotNull(mTestLooper.nextMessage());
// Unselect the monitor
mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(false /* setIsSelected */);
verifyStopped();
}
@Test
public void testClose() throws Exception {
mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(true /* setIsSelected */);
mIpSecPacketLossDetector.setInboundTransformInternal(mIpSecTransform);
assertTrue(mIpSecPacketLossDetector.isStarted());
assertNotNull(mTestLooper.nextMessage());
// Stop the monitor
mIpSecPacketLossDetector.close();
verifyStopped();
verify(mIpSecTransform).close();
}
@Test
public void testTransformStateReceiverOnResultWhenStopped() throws Exception {
final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
startMonitorAndCaptureStateReceiver();
xfrmStateReceiver.onResult(mTransformStateInitial);
// Unselect the monitor
mIpSecPacketLossDetector.setIsSelectedUnderlyingNetwork(false /* setIsSelected */);
verifyStopped();
xfrmStateReceiver.onResult(newTransformState(1, 1, newReplayBitmap(1)));
verify(mPacketLossCalculator, never())
.getPacketLossRatePercentage(any(), any(), anyString());
}
@Test
public void testTransformStateReceiverOnError() throws Exception {
final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
startMonitorAndCaptureStateReceiver();
xfrmStateReceiver.onResult(mTransformStateInitial);
xfrmStateReceiver.onError(new RuntimeException("Test"));
verify(mPacketLossCalculator, never())
.getPacketLossRatePercentage(any(), any(), anyString());
}
private void checkHandleLossRate(
int mockPacketLossRate, boolean isLastStateExpectedToUpdate, boolean isCallbackExpected)
throws Exception {
final OutcomeReceiver<IpSecTransformState, RuntimeException> xfrmStateReceiver =
startMonitorAndCaptureStateReceiver();
doReturn(mockPacketLossRate)
.when(mPacketLossCalculator)
.getPacketLossRatePercentage(any(), any(), anyString());
// Mock receiving two states with mTransformStateInitial and an arbitrary transformNew
final IpSecTransformState transformNew = newTransformState(1, 1, newReplayBitmap(1));
xfrmStateReceiver.onResult(mTransformStateInitial);
xfrmStateReceiver.onResult(transformNew);
// Verifications
verify(mPacketLossCalculator)
.getPacketLossRatePercentage(
eq(mTransformStateInitial), eq(transformNew), anyString());
if (isLastStateExpectedToUpdate) {
assertEquals(transformNew, mIpSecPacketLossDetector.getLastTransformState());
} else {
assertEquals(mTransformStateInitial, mIpSecPacketLossDetector.getLastTransformState());
}
if (isCallbackExpected) {
verify(mMetricMonitorCallback).onValidationResultReceived();
} else {
verify(mMetricMonitorCallback, never()).onValidationResultReceived();
}
}
@Test
public void testHandleLossRate_validationPass() throws Exception {
checkHandleLossRate(
2, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */);
}
@Test
public void testHandleLossRate_validationFail() throws Exception {
checkHandleLossRate(
22, true /* isLastStateExpectedToUpdate */, true /* isCallbackExpected */);
}
@Test
public void testHandleLossRate_resultUnavalaible() throws Exception {
checkHandleLossRate(
PACKET_LOSS_UNAVALAIBLE,
false /* isLastStateExpectedToUpdate */,
false /* isCallbackExpected */);
}
private void checkGetPacketLossRate(
IpSecTransformState oldState, IpSecTransformState newState, int expectedLossRate)
throws Exception {
assertEquals(
expectedLossRate,
mPacketLossCalculator.getPacketLossRatePercentage(oldState, newState, TAG));
}
private void checkGetPacketLossRate(
IpSecTransformState oldState,
int rxSeqNo,
int packetCount,
int packetInWin,
int expectedDataLossRate)
throws Exception {
final IpSecTransformState newState =
newTransformState(rxSeqNo, packetCount, newReplayBitmap(packetInWin));
checkGetPacketLossRate(oldState, newState, expectedDataLossRate);
}
@Test
public void testGetPacketLossRate_replayWindowUnchanged() throws Exception {
checkGetPacketLossRate(
mTransformStateInitial, mTransformStateInitial, PACKET_LOSS_UNAVALAIBLE);
checkGetPacketLossRate(mTransformStateInitial, 3000, 2000, 2000, PACKET_LOSS_UNAVALAIBLE);
}
@Test
public void testGetPacketLossRate_againstInitialState() throws Exception {
checkGetPacketLossRate(mTransformStateInitial, 7000, 7001, 4096, 0);
checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4096, 15);
checkGetPacketLossRate(mTransformStateInitial, 7000, 6000, 4000, 14);
}
@Test
public void testGetPktLossRate_oldHiSeqSmallerThanWinSize_overlappedWithNewWin()
throws Exception {
final IpSecTransformState oldState = newTransformState(2000, 1500, newReplayBitmap(1500));
checkGetPacketLossRate(oldState, 5000, 5001, 4096, 0);
checkGetPacketLossRate(oldState, 5000, 4000, 4096, 29);
checkGetPacketLossRate(oldState, 5000, 4000, 4000, 27);
}
@Test
public void testGetPktLossRate_oldHiSeqSmallerThanWinSize_notOverlappedWithNewWin()
throws Exception {
final IpSecTransformState oldState = newTransformState(2000, 1500, newReplayBitmap(1500));
checkGetPacketLossRate(oldState, 7000, 7001, 4096, 0);
checkGetPacketLossRate(oldState, 7000, 5000, 4096, 37);
checkGetPacketLossRate(oldState, 7000, 5000, 3000, 21);
}
@Test
public void testGetPktLossRate_oldHiSeqLargerThanWinSize_overlappedWithNewWin()
throws Exception {
final IpSecTransformState oldState = newTransformState(10000, 5000, newReplayBitmap(3000));
checkGetPacketLossRate(oldState, 12000, 8096, 4096, 0);
checkGetPacketLossRate(oldState, 12000, 7000, 4096, 36);
checkGetPacketLossRate(oldState, 12000, 7000, 3000, 0);
}
@Test
public void testGetPktLossRate_oldHiSeqLargerThanWinSize_notOverlappedWithNewWin()
throws Exception {
final IpSecTransformState oldState = newTransformState(10000, 5000, newReplayBitmap(3000));
checkGetPacketLossRate(oldState, 20000, 16096, 4096, 0);
checkGetPacketLossRate(oldState, 20000, 14000, 4096, 19);
checkGetPacketLossRate(oldState, 20000, 14000, 3000, 10);
}
}

View file

@ -20,6 +20,7 @@ import static com.android.server.vcn.VcnTestUtils.setupSystemService;
import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
@ -29,7 +30,12 @@ import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.TelephonyNetworkSpecifier;
import android.net.vcn.FeatureFlags;
import android.os.Handler;
import android.os.IPowerManager;
import android.os.IThermalService;
import android.os.ParcelUuid;
import android.os.PowerManager;
import android.os.test.TestLooper;
import android.telephony.TelephonyManager;
@ -90,32 +96,49 @@ public abstract class NetworkEvaluationTestBase {
protected static final LinkProperties LINK_PROPERTIES = getLinkPropertiesWithName("test_iface");
@Mock protected Context mContext;
@Mock protected Network mNetwork;
@Mock protected FeatureFlags mFeatureFlags;
@Mock protected com.android.net.flags.FeatureFlags mCoreNetFeatureFlags;
@Mock protected TelephonySubscriptionSnapshot mSubscriptionSnapshot;
@Mock protected TelephonyManager mTelephonyManager;
@Mock protected IPowerManager mPowerManagerService;
protected TestLooper mTestLooper;
protected VcnContext mVcnContext;
protected PowerManager mPowerManager;
@Before
public void setUp() {
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
final Context mockContext = mock(Context.class);
when(mNetwork.getNetId()).thenReturn(-1);
mTestLooper = new TestLooper();
mVcnContext =
spy(
new VcnContext(
mockContext,
mContext,
mTestLooper.getLooper(),
mock(VcnNetworkProvider.class),
false /* isInTestMode */));
doNothing().when(mVcnContext).ensureRunningOnLooperThread();
doReturn(true).when(mVcnContext).isFlagNetworkMetricMonitorEnabled();
doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled();
setupSystemService(
mockContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
mContext, mTelephonyManager, Context.TELEPHONY_SERVICE, TelephonyManager.class);
when(mTelephonyManager.createForSubscriptionId(SUB_ID)).thenReturn(mTelephonyManager);
when(mTelephonyManager.getNetworkOperator()).thenReturn(PLMN_ID);
when(mTelephonyManager.getSimSpecificCarrierId()).thenReturn(CARRIER_ID);
mPowerManager =
new PowerManager(
mContext,
mPowerManagerService,
mock(IThermalService.class),
mock(Handler.class));
setupSystemService(mContext, mPowerManager, Context.POWER_SERVICE, PowerManager.class);
}
}

View file

@ -57,7 +57,7 @@ public class NetworkPriorityClassifierTest extends NetworkEvaluationTestBase {
private UnderlyingNetworkRecord mCellNetworkRecord;
@Before
public void setUp() {
public void setUp() throws Exception {
super.setUp();
mWifiNetworkRecord = getTestNetworkRecord(WIFI_NETWORK_CAPABILITIES);

View file

@ -34,7 +34,7 @@ public class UnderlyingNetworkEvaluatorTest extends NetworkEvaluationTestBase {
private PersistableBundleWrapper mCarrierConfig;
@Before
public void setUp() {
public void setUp() throws Exception {
super.setUp();
mCarrierConfig = new PersistableBundleWrapper(new PersistableBundle());
}