Pull VcnNetworkProvider out into a separate class

This change makes the VcnNetworkProvider a separate class, and caches
all NetworkRequest(s) to ensure that VcnTunnel(s) satisfy all requests
that they can accept.

Bug: 163431879
Test: atest FrameworksVcnTests
Change-Id: I3b7695628d0153a33f7e7f40d839df1463d58b07
This commit is contained in:
Benedict Wong
2020-12-01 17:39:50 -08:00
parent 9cb5807845
commit db8cf2a13d
7 changed files with 377 additions and 40 deletions

View File

@@ -25,8 +25,6 @@ import android.annotation.NonNull;
import android.app.AppOpsManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.net.vcn.IVcnManagementService;
import android.net.vcn.VcnConfig;
import android.os.Binder;
@@ -50,6 +48,7 @@ import com.android.internal.annotations.VisibleForTesting.Visibility;
import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
import com.android.server.vcn.VcnNetworkProvider;
import com.android.server.vcn.util.PersistableBundleUtils;
import java.io.IOException;
@@ -390,6 +389,9 @@ public class VcnManagementService extends IVcnManagementService.Stub {
private void startVcnLocked(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) {
Slog.v(TAG, "Starting VCN config for subGrp: " + subscriptionGroup);
// TODO(b/176939047): Support multiple VCNs active at the same time, or limit to one active
// VCN.
final Vcn newInstance = mDeps.newVcn(mVcnContext, subscriptionGroup, config);
mVcns.put(subscriptionGroup, newInstance);
}
@@ -493,24 +495,4 @@ public class VcnManagementService extends IVcnManagementService.Stub {
return Collections.unmodifiableMap(mVcns);
}
}
/**
* Network provider for VCN networks.
*
* @hide
*/
public class VcnNetworkProvider extends NetworkProvider {
VcnNetworkProvider(Context context, Looper looper) {
super(context, looper, VcnNetworkProvider.class.getSimpleName());
}
@Override
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
synchronized (mLock) {
for (Vcn instance : mVcns.values()) {
instance.onNetworkRequested(request, score, providerId);
}
}
}
}
}

View File

@@ -16,32 +16,69 @@
package com.android.server.vcn;
import android.annotation.NonNull;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.vcn.VcnConfig;
import android.net.vcn.VcnGatewayConnectionConfig;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelUuid;
import android.util.Slog;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Represents an single instance of a VCN.
*
* <p>Each Vcn instance manages all tunnels for a given subscription group, including per-capability
* networks, network selection, and multi-homing.
* <p>Each Vcn instance manages all {@link VcnGatewayConnection}(s) for a given subscription group,
* including per-capability networks, network selection, and multi-homing.
*
* @hide
*/
public class Vcn extends Handler {
private static final String TAG = Vcn.class.getSimpleName();
private static final int MSG_EVENT_BASE = 0;
private static final int MSG_CMD_BASE = 100;
/**
* A carrier app updated the configuration.
*
* <p>Triggers update of config, re-evaluating all active and underlying networks.
*
* @param obj VcnConfig
*/
private static final int MSG_EVENT_CONFIG_UPDATED = MSG_EVENT_BASE;
/**
* A NetworkRequest was added or updated.
*
* <p>Triggers an evaluation of all active networks, bringing up a new one if necessary.
*
* @param obj NetworkRequest
*/
private static final int MSG_EVENT_NETWORK_REQUESTED = MSG_EVENT_BASE + 1;
/** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */
private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE;
@NonNull private final VcnContext mVcnContext;
@NonNull private final ParcelUuid mSubscriptionGroup;
@NonNull private final Dependencies mDeps;
@NonNull private final VcnNetworkRequestListener mRequestListener;
@NonNull
private final Map<VcnGatewayConnectionConfig, VcnGatewayConnection> mVcnGatewayConnections =
new HashMap<>();
@NonNull private VcnConfig mConfig;
private boolean mIsRunning = true;
public Vcn(
@NonNull VcnContext vcnContext,
@NonNull ParcelUuid subscriptionGroup,
@@ -58,31 +95,123 @@ public class Vcn extends Handler {
mVcnContext = vcnContext;
mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup");
mDeps = Objects.requireNonNull(deps, "Missing deps");
mRequestListener = new VcnNetworkRequestListener();
mConfig = Objects.requireNonNull(config, "Missing config");
// Register to receive cached and future NetworkRequests
mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener);
}
/** Asynchronously updates the configuration and triggers a re-evaluation of Networks */
public void updateConfig(@NonNull VcnConfig config) {
Objects.requireNonNull(config, "Missing config");
// TODO: Proxy to handler, and make config there.
sendMessage(obtainMessage(MSG_EVENT_CONFIG_UPDATED, config));
}
/** Asynchronously tears down this Vcn instance, along with all tunnels and Networks */
/** Asynchronously tears down this Vcn instance, including VcnGatewayConnection(s) */
public void teardownAsynchronously() {
// TODO: Proxy to handler, and teardown there.
sendMessageAtFrontOfQueue(obtainMessage(MSG_CMD_TEARDOWN));
}
/** Notifies this Vcn instance of a new NetworkRequest */
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
Objects.requireNonNull(request, "Missing request");
private class VcnNetworkRequestListener implements VcnNetworkProvider.NetworkRequestListener {
@Override
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
Objects.requireNonNull(request, "Missing request");
// TODO: Proxy to handler, and handle there.
sendMessage(obtainMessage(MSG_EVENT_NETWORK_REQUESTED, score, providerId, request));
}
}
@Override
public void handleMessage(@NonNull Message msg) {
// TODO: Do something
if (!mIsRunning) {
return;
}
switch (msg.what) {
case MSG_EVENT_CONFIG_UPDATED:
handleConfigUpdated((VcnConfig) msg.obj);
break;
case MSG_EVENT_NETWORK_REQUESTED:
handleNetworkRequested((NetworkRequest) msg.obj, msg.arg1, msg.arg2);
break;
case MSG_CMD_TEARDOWN:
handleTeardown();
break;
default:
Slog.wtf(getLogTag(), "Unknown msg.what: " + msg.what);
}
}
private void handleConfigUpdated(@NonNull VcnConfig config) {
// TODO: Add a dump function in VcnConfig that omits PII. Until then, use hashCode()
Slog.v(getLogTag(), String.format("Config updated: config = %s", config.hashCode()));
mConfig = config;
// TODO: Reevaluate active VcnGatewayConnection(s)
}
private void handleTeardown() {
mVcnContext.getVcnNetworkProvider().unregisterListener(mRequestListener);
for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) {
gatewayConnection.teardownAsynchronously();
}
mIsRunning = false;
}
private void handleNetworkRequested(
@NonNull NetworkRequest request, int score, int providerId) {
if (score > getNetworkScore()) {
Slog.v(getLogTag(),
"Request " + request.requestId + " already satisfied by higher-scoring ("
+ score + ") network from provider " + providerId);
return;
}
// If preexisting VcnGatewayConnection(s) satisfy request, return
for (VcnGatewayConnectionConfig gatewayConnectionConfig : mVcnGatewayConnections.keySet()) {
if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) {
Slog.v(getLogTag(),
"Request " + request.requestId
+ " satisfied by existing VcnGatewayConnection");
return;
}
}
// If any supported (but not running) VcnGatewayConnection(s) can satisfy request, bring it
// up
for (VcnGatewayConnectionConfig gatewayConnectionConfig :
mConfig.getGatewayConnectionConfigs()) {
if (requestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) {
Slog.v(
getLogTag(),
"Bringing up new VcnGatewayConnection for request " + request.requestId);
final VcnGatewayConnection vcnGatewayConnection =
new VcnGatewayConnection(
mVcnContext, mSubscriptionGroup, gatewayConnectionConfig);
mVcnGatewayConnections.put(gatewayConnectionConfig, vcnGatewayConnection);
}
}
}
private boolean requestSatisfiedByGatewayConnectionConfig(
@NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) {
final NetworkCapabilities configCaps = new NetworkCapabilities();
for (int cap : config.getAllExposedCapabilities()) {
configCaps.addCapability(cap);
}
return request.networkCapabilities.satisfiedByNetworkCapabilities(configCaps);
}
private String getLogTag() {
return String.format("%s [%d]", TAG, mSubscriptionGroup.hashCode());
}
/** Retrieves the network score for a VCN Network */

View File

@@ -20,8 +20,6 @@ import android.annotation.NonNull;
import android.content.Context;
import android.os.Looper;
import com.android.server.VcnManagementService.VcnNetworkProvider;
import java.util.Objects;
/**

View File

@@ -65,8 +65,8 @@ public class VcnGatewayConnection extends Handler implements UnderlyingNetworkTr
mDeps.newUnderlyingNetworkTracker(mVcnContext, subscriptionGroup, this);
}
/** Tears down this GatewayConnection, and any resources used */
public void teardown() {
/** Asynchronously tears down this GatewayConnection, and any resources used */
public void teardownAsynchronously() {
mUnderlyingNetworkTracker.teardown();
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (C) 2020 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;
import android.annotation.NonNull;
import android.content.Context;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.os.Looper;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseArray;
import java.util.Objects;
import java.util.Set;
/**
* VCN Network Provider routes NetworkRequests to listeners to bring up tunnels as needed.
*
* <p>The VcnNetworkProvider provides a caching layer to ensure that all listeners receive all
* active NetworkRequest(s), including ones that were filed prior to listener registration.
*
* @hide
*/
public class VcnNetworkProvider extends NetworkProvider {
private static final String TAG = VcnNetworkProvider.class.getSimpleName();
private final Set<NetworkRequestListener> mListeners = new ArraySet<>();
private final SparseArray<NetworkRequestEntry> mRequests = new SparseArray<>();
public VcnNetworkProvider(Context context, Looper looper) {
super(context, looper, VcnNetworkProvider.class.getSimpleName());
}
// Package-private
void registerListener(@NonNull NetworkRequestListener listener) {
mListeners.add(listener);
// Send listener all cached requests
for (int i = 0; i < mRequests.size(); i++) {
notifyListenerForEvent(listener, mRequests.valueAt(i));
}
}
// Package-private
void unregisterListener(@NonNull NetworkRequestListener listener) {
mListeners.remove(listener);
}
private void notifyListenerForEvent(
@NonNull NetworkRequestListener listener, @NonNull NetworkRequestEntry entry) {
listener.onNetworkRequested(entry.mRequest, entry.mScore, entry.mProviderId);
}
@Override
public void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId) {
Slog.v(
TAG,
String.format(
"Network requested: Request = %s, score = %d, providerId = %d",
request, score, providerId));
final NetworkRequestEntry entry = new NetworkRequestEntry(request, score, providerId);
mRequests.put(request.requestId, entry);
// TODO(b/176939047): Intelligently route requests to prioritized VcnInstances (based on
// Default Data Sub, or similar)
for (NetworkRequestListener listener : mListeners) {
notifyListenerForEvent(listener, entry);
}
}
@Override
public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {
mRequests.remove(request.requestId);
}
private static class NetworkRequestEntry {
public final NetworkRequest mRequest;
public final int mScore;
public final int mProviderId;
private NetworkRequestEntry(@NonNull NetworkRequest request, int score, int providerId) {
mRequest = Objects.requireNonNull(request, "Missing request");
mScore = score;
mProviderId = providerId;
}
}
// package-private
interface NetworkRequestListener {
void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId);
}
}

View File

@@ -49,10 +49,10 @@ import android.telephony.TelephonyManager;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.VcnManagementService.VcnNetworkProvider;
import com.android.server.vcn.TelephonySubscriptionTracker;
import com.android.server.vcn.Vcn;
import com.android.server.vcn.VcnContext;
import com.android.server.vcn.VcnNetworkProvider;
import com.android.server.vcn.util.PersistableBundleUtils;
import org.junit.Test;
@@ -191,8 +191,7 @@ public class VcnManagementServiceTest {
public void testSystemReady() throws Exception {
mVcnMgmtSvc.systemReady();
verify(mConnMgr)
.registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class));
verify(mConnMgr).registerNetworkProvider(any(VcnNetworkProvider.class));
verify(mSubscriptionTracker).register();
}
@@ -309,7 +308,7 @@ public class VcnManagementServiceTest {
// Config cleared, SIM reloaded & config re-added right before teardown delay, staring new
// vcnInstance.
mTestLooper.moveTimeForward(
VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS - 1);
VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS / 2);
mTestLooper.dispatchAll();
mVcnMgmtSvc.clearVcnConfig(TEST_UUID_2);
final Vcn newInstance = startAndGetVcnInstance(TEST_UUID_2);

View File

@@ -0,0 +1,121 @@
/*
* Copyright (C) 2020 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;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.annotation.NonNull;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.os.test.TestLooper;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.server.vcn.VcnNetworkProvider.NetworkRequestListener;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
/** Tests for TelephonySubscriptionTracker */
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnNetworkProviderTest {
private static final int TEST_SCORE_UNSATISFIED = 0;
private static final int TEST_SCORE_HIGH = 100;
private static final int TEST_PROVIDER_ID = 1;
private static final int TEST_LEGACY_TYPE = ConnectivityManager.TYPE_MOBILE;
private static final NetworkRequest.Type TEST_REQUEST_TYPE = NetworkRequest.Type.REQUEST;
@NonNull private final Context mContext;
@NonNull private final TestLooper mTestLooper;
@NonNull private VcnNetworkProvider mVcnNetworkProvider;
@NonNull private NetworkRequestListener mListener;
public VcnNetworkProviderTest() {
mContext = mock(Context.class);
mTestLooper = new TestLooper();
}
@Before
public void setUp() throws Exception {
mVcnNetworkProvider = new VcnNetworkProvider(mContext, mTestLooper.getLooper());
mListener = mock(NetworkRequestListener.class);
}
@Test
public void testRequestsPassedToRegisteredListeners() throws Exception {
mVcnNetworkProvider.registerListener(mListener);
final NetworkRequest request = mock(NetworkRequest.class);
mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
verify(mListener).onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
}
@Test
public void testRequestsPassedToRegisteredListeners_satisfiedByHighScoringProvider()
throws Exception {
mVcnNetworkProvider.registerListener(mListener);
final NetworkRequest request = mock(NetworkRequest.class);
mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID);
verify(mListener).onNetworkRequested(request, TEST_SCORE_HIGH, TEST_PROVIDER_ID);
}
@Test
public void testUnregisterListener() throws Exception {
mVcnNetworkProvider.registerListener(mListener);
mVcnNetworkProvider.unregisterListener(mListener);
final NetworkRequest request = mock(NetworkRequest.class);
mVcnNetworkProvider.onNetworkRequested(request, TEST_SCORE_UNSATISFIED, TEST_PROVIDER_ID);
verifyNoMoreInteractions(mListener);
}
@Test
public void testCachedRequestsPassedOnRegister() throws Exception {
final List<NetworkRequest> requests = new ArrayList<>();
for (int i = 0; i < 10; i++) {
final NetworkRequest request =
new NetworkRequest(
new NetworkCapabilities(),
TEST_LEGACY_TYPE,
i /* requestId */,
TEST_REQUEST_TYPE);
requests.add(request);
mVcnNetworkProvider.onNetworkRequested(request, i, i + 1);
}
mVcnNetworkProvider.registerListener(mListener);
for (int i = 0; i < requests.size(); i++) {
final NetworkRequest request = requests.get(i);
verify(mListener).onNetworkRequested(request, i, i + 1);
}
verifyNoMoreInteractions(mListener);
}
}