diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 7ea8e04580f71..7024e67a82042 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -16,33 +16,438 @@ package com.android.server.vcn; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import static com.android.server.VcnManagementService.VDBG; + import android.annotation.NonNull; import android.annotation.Nullable; +import android.net.ConnectivityManager; +import android.net.InetAddresses; +import android.net.IpPrefix; +import android.net.IpSecManager; +import android.net.IpSecManager.IpSecTunnelInterface; +import android.net.IpSecManager.ResourceUnavailableException; +import android.net.IpSecTransform; +import android.net.LinkAddress; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkAgent; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkInfo.DetailedState; +import android.net.RouteInfo; +import android.net.annotations.PolicyDirection; +import android.net.ipsec.ike.ChildSessionCallback; +import android.net.ipsec.ike.ChildSessionConfiguration; +import android.net.ipsec.ike.ChildSessionParams; +import android.net.ipsec.ike.IkeSession; +import android.net.ipsec.ike.IkeSessionCallback; +import android.net.ipsec.ike.IkeSessionConfiguration; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.exceptions.IkeException; +import android.net.ipsec.ike.exceptions.IkeProtocolException; import android.net.vcn.VcnGatewayConnectionConfig; import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.Message; import android.os.ParcelUuid; +import android.telephony.TelephonyManager; +import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord; import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback; +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; import java.util.Objects; +import java.util.concurrent.TimeUnit; /** * A single VCN Gateway Connection, providing a single public-facing VCN network. * *
This class handles mobility events, performs retries, and tracks safe-mode conditions. * + *
Internal state transitions are as follows: + * + * +----------------------------+ +------------------------------+ + * | DisconnectedState | Teardown or | DisconnectingState | + * | |<--no available--| | + * | Initial state. | underlying | Transitive state for tearing | + * +----------------------------+ networks | tearing down an IKE session. | + * | +------------------------------+ + * | ^ | + * Underlying Network Teardown requested | Not tearing down + * changed +--or retriable error--+ and has available + * | | occurred underlying network + * | ^ | + * v | v + * +----------------------------+ | +------------------------------+ + * | ConnectingState |<----------------| RetryTimeoutState | + * | | | | | + * | Transitive state for | | | Transitive state for | + * | starting IKE negotiation. |---+ | handling retriable errors. | + * +----------------------------+ | +------------------------------+ + * | | + * IKE session | + * negotiated | + * | | + * v | + * +----------------------------+ ^ + * | ConnectedState | | + * | | | + * | Stable state where | | + * | gateway connection is set | | + * | up, and Android Network is | | + * | connected. |---+ + * +----------------------------+ + *+ * * @hide */ -public class VcnGatewayConnection extends Handler implements UnderlyingNetworkTrackerCallback { +public class VcnGatewayConnection extends StateMachine { private static final String TAG = VcnGatewayConnection.class.getSimpleName(); + private static final InetAddress DUMMY_ADDR = InetAddresses.parseNumericAddress("192.0.2.0"); + private static final int ARG_NOT_PRESENT = Integer.MIN_VALUE; + + private static final String DISCONNECT_REASON_INTERNAL_ERROR = "Uncaught exception: "; + private static final String DISCONNECT_REASON_UNDERLYING_NETWORK_LOST = + "Underlying Network lost"; + private static final String DISCONNECT_REASON_TEARDOWN = "teardown() called on VcnTunnel"; + private static final int TOKEN_ANY = Integer.MIN_VALUE; + + private static final int NETWORK_LOSS_DISCONNECT_TIMEOUT_SECONDS = 30; + private static final int TEARDOWN_TIMEOUT_SECONDS = 5; + + private interface EventInfo {} + + /** + * Sent when there are changes to the underlying network (per the UnderlyingNetworkTracker). + * + *
May indicate an entirely new underlying network, OR a change in network properties. + * + *
Relevant in ALL states. + * + *
In the Connected state, this MAY indicate a mobility even occurred. + * + * @param arg1 The "any" token; this event is always applicable. + * @param obj @NonNull An EventUnderlyingNetworkChangedInfo instance with relevant data. + */ + private static final int EVENT_UNDERLYING_NETWORK_CHANGED = 1; + + private static class EventUnderlyingNetworkChangedInfo implements EventInfo { + @Nullable public final UnderlyingNetworkRecord newUnderlying; + + EventUnderlyingNetworkChangedInfo(@Nullable UnderlyingNetworkRecord newUnderlying) { + this.newUnderlying = newUnderlying; + } + + @Override + public int hashCode() { + return Objects.hash(newUnderlying); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventUnderlyingNetworkChangedInfo)) { + return false; + } + + final EventUnderlyingNetworkChangedInfo rhs = (EventUnderlyingNetworkChangedInfo) other; + return Objects.equals(newUnderlying, rhs.newUnderlying); + } + } + + /** + * Sent (delayed) to trigger an attempt to reestablish the tunnel. + * + *
Only relevant in the Retry-timeout state, discarded in all other states. + * + *
Upon receipt of this signal, the state machine will transition from the Retry-timeout + * state to the Connecting state. + * + * @param arg1 The "any" token; no sessions are active in the RetryTimeoutState. + */ + private static final int EVENT_RETRY_TIMEOUT_EXPIRED = 2; + + /** + * Sent when a gateway connection has been lost, either due to a IKE or child failure. + * + *
Relevant in all states that have an IKE session. + * + *
Upon receipt of this signal, the state machine will (unless loss of the session is + * expected) transition to the Disconnecting state, to ensure IKE session closure before + * retrying, or fully shutting down. + * + * @param arg1 The session token for the IKE Session that was lost, used to prevent out-of-date + * signals from propagating. + * @param obj @NonNull An EventSessionLostInfo instance with relevant data. + */ + private static final int EVENT_SESSION_LOST = 3; + + private static class EventSessionLostInfo implements EventInfo { + @Nullable public final Exception exception; + + EventSessionLostInfo(@NonNull Exception exception) { + this.exception = exception; + } + + @Override + public int hashCode() { + return Objects.hash(exception); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventSessionLostInfo)) { + return false; + } + + final EventSessionLostInfo rhs = (EventSessionLostInfo) other; + return Objects.equals(exception, rhs.exception); + } + } + + /** + * Sent when an IKE session has completely closed. + * + *
Relevant only in the Disconnecting State, used to identify that a session being torn down + * was fully closed. If this event is not fired within a timely fashion, the IKE session will be + * forcibly terminated. + * + *
Upon receipt of this signal, the state machine will (unless closure of the session is + * expected) transition to the Disconnected or RetryTimeout states, depending on whether the + * GatewayConnection is being fully torn down. + * + * @param arg1 The session token for the IKE Session that was lost, used to prevent out-of-date + * signals from propagating. + * @param obj @NonNull An EventSessionLostInfo instance with relevant data. + */ + private static final int EVENT_SESSION_CLOSED = 4; + + /** + * Sent when an IKE Child Transform was created, and should be applied to the tunnel. + * + *
Only relevant in the Connecting, Connected and Migrating states. This callback MUST be + * handled in the Connected or Migrating states, and should be deferred if necessary. + * + * @param arg1 The session token for the IKE Session that had a new child created, used to + * prevent out-of-date signals from propagating. + * @param obj @NonNull An EventTransformCreatedInfo instance with relevant data. + */ + private static final int EVENT_TRANSFORM_CREATED = 5; + + private static class EventTransformCreatedInfo implements EventInfo { + @PolicyDirection public final int direction; + @NonNull public final IpSecTransform transform; + + EventTransformCreatedInfo( + @PolicyDirection int direction, @NonNull IpSecTransform transform) { + this.direction = direction; + this.transform = Objects.requireNonNull(transform); + } + + @Override + public int hashCode() { + return Objects.hash(direction, transform); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventTransformCreatedInfo)) { + return false; + } + + final EventTransformCreatedInfo rhs = (EventTransformCreatedInfo) other; + return direction == rhs.direction && Objects.equals(transform, rhs.transform); + } + } + + /** + * Sent when an IKE Child Session was completely opened and configured successfully. + * + *
Only relevant in the Connected and Migrating states. + * + * @param arg1 The session token for the IKE Session for which a child was opened and configured + * successfully, used to prevent out-of-date signals from propagating. + * @param obj @NonNull An EventSetupCompletedInfo instance with relevant data. + */ + private static final int EVENT_SETUP_COMPLETED = 6; + + private static class EventSetupCompletedInfo implements EventInfo { + @NonNull public final ChildSessionConfiguration childSessionConfig; + + EventSetupCompletedInfo(@NonNull ChildSessionConfiguration childSessionConfig) { + this.childSessionConfig = Objects.requireNonNull(childSessionConfig); + } + + @Override + public int hashCode() { + return Objects.hash(childSessionConfig); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventSetupCompletedInfo)) { + return false; + } + + final EventSetupCompletedInfo rhs = (EventSetupCompletedInfo) other; + return Objects.equals(childSessionConfig, rhs.childSessionConfig); + } + } + + /** + * Sent when conditions (internal or external) require a disconnect. + * + *
Relevant in all states except the Disconnected state. + * + *
This signal is often fired with a timeout in order to prevent disconnecting during + * transient conditions, such as network switches. Upon the transient passing, the signal is + * canceled based on the disconnect reason. + * + *
Upon receipt of this signal, the state machine MUST tear down all active sessions, cancel + * any pending work items, and move to the Disconnected state. + * + * @param arg1 The "any" token; this signal is always honored. + * @param obj @NonNull An EventDisconnectRequestedInfo instance with relevant data. + */ + private static final int EVENT_DISCONNECT_REQUESTED = 7; + + private static class EventDisconnectRequestedInfo implements EventInfo { + /** The reason why the disconnect was requested. */ + @NonNull public final String reason; + + EventDisconnectRequestedInfo(@NonNull String reason) { + this.reason = Objects.requireNonNull(reason); + } + + @Override + public int hashCode() { + return Objects.hash(reason); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventDisconnectRequestedInfo)) { + return false; + } + + final EventDisconnectRequestedInfo rhs = (EventDisconnectRequestedInfo) other; + return reason.equals(rhs.reason); + } + } + + /** + * Sent (delayed) to trigger a forcible close of an IKE session. + * + *
Only relevant in the Disconnecting state, discarded in all other states. + * + *
Upon receipt of this signal, the state machine will transition from the Disconnecting + * state to the Disconnected state. + * + * @param arg1 The session token for the IKE Session that is being torn down, used to prevent + * out-of-date signals from propagating. + */ + private static final int EVENT_TEARDOWN_TIMEOUT_EXPIRED = 8; + + @NonNull private final DisconnectedState mDisconnectedState = new DisconnectedState(); + @NonNull private final DisconnectingState mDisconnectingState = new DisconnectingState(); + @NonNull private final ConnectingState mConnectingState = new ConnectingState(); + @NonNull private final ConnectedState mConnectedState = new ConnectedState(); + @NonNull private final RetryTimeoutState mRetryTimeoutState = new RetryTimeoutState(); + @NonNull private final VcnContext mVcnContext; @NonNull private final ParcelUuid mSubscriptionGroup; @NonNull private final UnderlyingNetworkTracker mUnderlyingNetworkTracker; @NonNull private final VcnGatewayConnectionConfig mConnectionConfig; @NonNull private final Dependencies mDeps; + @NonNull private final VcnUnderlyingNetworkTrackerCallback mUnderlyingNetworkTrackerCallback; + + @NonNull private final IpSecManager mIpSecManager; + @NonNull private final IpSecTunnelInterface mTunnelIface; + + /** Running state of this VcnGatewayConnection. */ + private boolean mIsRunning = true; + + /** + * The token used by the primary/current/active session. + * + *
This token MUST be updated when a new stateful/async session becomes the + * primary/current/active session. Example cases where the session changes are: + * + *
In the migrating state, where two sessions may be active, this value MUST represent the + * primary session. This is USUALLY the existing session, and is only switched to the new + * session when: + * + *
A new token MUST be used for all new IKE sessions. + */ + private int mNextToken = 0; + + /** + * The number of unsuccessful attempts since the last successful connection. + * + *
This number MUST be incremented each time the RetryTimeout state is entered, and cleared + * each time the Connected state is entered. + */ + private int mFailedAttempts = 0; + + /** + * The current underlying network. + * + *
Set in any states, always @NonNull in all states except Disconnected, null otherwise. + */ + private UnderlyingNetworkRecord mUnderlying; + + /** + * The active IKE session. + * + *
Set in Connecting or Migrating States, always @NonNull in Connecting, Connected, and + * Migrating states, null otherwise. + */ + private IkeSession mIkeSession; + + /** + * The last known child configuration. + * + *
Set in Connected and Migrating states, always @NonNull in Connected, Migrating + * states, @Nullable otherwise. + */ + private ChildSessionConfiguration mChildConfig; + + /** + * The active network agent. + * + *
Set in Connected state, always @NonNull in Connected, Migrating states, @Nullable + * otherwise. + */ + private NetworkAgent mNetworkAgent; + public VcnGatewayConnection( @NonNull VcnContext vcnContext, @NonNull ParcelUuid subscriptionGroup, @@ -55,30 +460,350 @@ public class VcnGatewayConnection extends Handler implements UnderlyingNetworkTr @NonNull ParcelUuid subscriptionGroup, @NonNull VcnGatewayConnectionConfig connectionConfig, @NonNull Dependencies deps) { - super(Objects.requireNonNull(vcnContext, "Missing vcnContext").getLooper()); + super(TAG, Objects.requireNonNull(vcnContext, "Missing vcnContext").getLooper()); mVcnContext = vcnContext; mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); mConnectionConfig = Objects.requireNonNull(connectionConfig, "Missing connectionConfig"); mDeps = Objects.requireNonNull(deps, "Missing deps"); + mUnderlyingNetworkTrackerCallback = new VcnUnderlyingNetworkTrackerCallback(); + mUnderlyingNetworkTracker = - mDeps.newUnderlyingNetworkTracker(mVcnContext, subscriptionGroup, this); + mDeps.newUnderlyingNetworkTracker( + mVcnContext, subscriptionGroup, mUnderlyingNetworkTrackerCallback); + mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class); + + IpSecTunnelInterface iface; + try { + iface = + mIpSecManager.createIpSecTunnelInterface( + DUMMY_ADDR, DUMMY_ADDR, new Network(-1)); + } catch (IOException | ResourceUnavailableException e) { + teardownAsynchronously(); + mTunnelIface = null; + + return; + } + + mTunnelIface = iface; + + addState(mDisconnectedState); + addState(mDisconnectingState); + addState(mConnectingState); + addState(mConnectedState); + addState(mRetryTimeoutState); + + setInitialState(mDisconnectedState); + setDbg(VDBG); + start(); } - /** Asynchronously tears down this GatewayConnection, and any resources used */ + /** + * Asynchronously tears down this GatewayConnection, and any resources used. + * + *
Once torn down, this VcnTunnel CANNOT be started again. + */ public void teardownAsynchronously() { mUnderlyingNetworkTracker.teardown(); + + // No need to call setInterfaceDown(); the IpSecInterface is being fully torn down. + if (mTunnelIface != null) { + mTunnelIface.close(); + } + + sendMessage( + EVENT_DISCONNECT_REQUESTED, + TOKEN_ANY, + new EventDisconnectRequestedInfo(DISCONNECT_REASON_TEARDOWN)); + quit(); + + // TODO: Notify VcnInstance (via callbacks) of permanent teardown of this tunnel, since this + // is also called asynchronously when a NetworkAgent becomes unwanted } - private static class Dependencies { + private class VcnUnderlyingNetworkTrackerCallback implements UnderlyingNetworkTrackerCallback { + @Override + public void onSelectedUnderlyingNetworkChanged( + @Nullable UnderlyingNetworkRecord underlying) { + // If underlying is null, all underlying networks have been lost. Disconnect VCN after a + // timeout. + if (underlying == null) { + sendMessageDelayed( + EVENT_DISCONNECT_REQUESTED, + TOKEN_ANY, + new EventDisconnectRequestedInfo(DISCONNECT_REASON_UNDERLYING_NETWORK_LOST), + TimeUnit.SECONDS.toMillis(NETWORK_LOSS_DISCONNECT_TIMEOUT_SECONDS)); + return; + } + + // Cancel any existing disconnect due to loss of underlying network + // getHandler() can return null if the state machine has already quit. Since this is + // called + // from other classes, this condition must be verified. + if (getHandler() != null) { + getHandler() + .removeEqualMessages( + EVENT_DISCONNECT_REQUESTED, + new EventDisconnectRequestedInfo( + DISCONNECT_REASON_UNDERLYING_NETWORK_LOST)); + } + sendMessage( + EVENT_UNDERLYING_NETWORK_CHANGED, + TOKEN_ANY, + new EventUnderlyingNetworkChangedInfo(underlying)); + } + } + + private void sendMessage(int what, int token, EventInfo data) { + super.sendMessage(what, token, ARG_NOT_PRESENT, data); + } + + private void sendMessage(int what, int token, int arg2, EventInfo data) { + super.sendMessage(what, token, arg2, data); + } + + private void sendMessageDelayed(int what, int token, EventInfo data, long timeout) { + super.sendMessageDelayed(what, token, ARG_NOT_PRESENT, data, timeout); + } + + private void sendMessageDelayed(int what, int token, int arg2, EventInfo data, long timeout) { + super.sendMessageDelayed(what, token, arg2, data, timeout); + } + + private void sessionLost(int token, @Nullable Exception exception) { + sendMessage(EVENT_SESSION_LOST, token, new EventSessionLostInfo(exception)); + } + + private void sessionClosed(int token, @Nullable Exception exception) { + // SESSION_LOST MUST be sent before SESSION_CLOSED to ensure that the SM moves to the + // Disconnecting state. + sessionLost(token, exception); + sendMessage(EVENT_SESSION_CLOSED, token); + } + + private void childTransformCreated( + int token, @NonNull IpSecTransform transform, int direction) { + sendMessage( + EVENT_TRANSFORM_CREATED, + token, + new EventTransformCreatedInfo(direction, transform)); + } + + private void childOpened(int token, @NonNull ChildSessionConfiguration childConfig) { + sendMessage(EVENT_SETUP_COMPLETED, token, new EventSetupCompletedInfo(childConfig)); + } + + private abstract class BaseState extends State { + protected void enterState() throws Exception {} + + protected abstract void processStateMsg(Message msg) throws Exception; + } + /** + * State representing the a disconnected VCN tunnel. + * + *
This is also is the initial state. + */ + private class DisconnectedState extends BaseState { + @Override + protected void processStateMsg(Message msg) {} + } + + private abstract class ActiveBaseState extends BaseState {} + + /** + * Transitive state representing a VCN that is tearing down an IKE session. + * + *
In this state, the IKE session is in the process of being torn down. If the IKE session + * does not complete teardown in a timely fashion, it will be killed (forcibly closed). + */ + private class DisconnectingState extends ActiveBaseState { + @Override + protected void processStateMsg(Message msg) {} + } + + /** + * Transitive state representing a VCN that is making an primary (non-handover) connection. + * + *
This state starts IKE negotiation, but defers transform application & network setup to the + * Connected state. + */ + private class ConnectingState extends ActiveBaseState { + @Override + protected void processStateMsg(Message msg) {} + } + + private abstract class ConnectedStateBase extends ActiveBaseState {} + + /** + * Stable state representing a VCN that has a functioning connection to the mobility anchor. + * + *
This state handles IPsec transform application (initial and rekey), NetworkAgent setup, + * and monitors for mobility events. + */ + class ConnectedState extends ConnectedStateBase { + @Override + protected void processStateMsg(Message msg) {} + } + + /** + * Transitive state representing a VCN that failed to establish a connection, and will retry. + * + *
This state will be exited upon a new underlying network being found, or timeout expiry.
+ */
+ class RetryTimeoutState extends ActiveBaseState {
+ @Override
+ protected void processStateMsg(Message msg) {}
+ }
+
+ // TODO: Remove this when migrating to new NetworkAgent API
+ private static NetworkInfo buildNetworkInfo(boolean isConnected) {
+ NetworkInfo info =
+ new NetworkInfo(
+ ConnectivityManager.TYPE_MOBILE,
+ TelephonyManager.NETWORK_TYPE_UNKNOWN,
+ "MOBILE",
+ "VCN");
+ info.setDetailedState(
+ isConnected ? DetailedState.CONNECTED : DetailedState.DISCONNECTED, null, null);
+
+ return info;
+ }
+
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ static NetworkCapabilities buildNetworkCapabilities(
+ @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig) {
+ final NetworkCapabilities caps = new NetworkCapabilities();
+
+ caps.addTransportType(TRANSPORT_CELLULAR);
+ caps.addCapability(NET_CAPABILITY_NOT_CONGESTED);
+ caps.addCapability(NET_CAPABILITY_NOT_SUSPENDED);
+
+ // Add exposed capabilities
+ for (int cap : gatewayConnectionConfig.getAllExposedCapabilities()) {
+ caps.addCapability(cap);
+ }
+
+ return caps;
+ }
+
+ private static LinkProperties buildConnectedLinkProperties(
+ @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig,
+ @NonNull IpSecTunnelInterface tunnelIface,
+ @NonNull ChildSessionConfiguration childConfig) {
+ final LinkProperties lp = new LinkProperties();
+
+ lp.setInterfaceName(tunnelIface.getInterfaceName());
+ for (LinkAddress addr : childConfig.getInternalAddresses()) {
+ lp.addLinkAddress(addr);
+ }
+ for (InetAddress addr : childConfig.getInternalDnsServers()) {
+ lp.addDnsServer(addr);
+ }
+
+ lp.addRoute(new RouteInfo(new IpPrefix(Inet4Address.ANY, 0), null));
+ lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null));
+
+ lp.setMtu(gatewayConnectionConfig.getMaxMtu());
+
+ return lp;
+ }
+
+ private class IkeSessionCallbackImpl implements IkeSessionCallback {
+ private final int mToken;
+
+ IkeSessionCallbackImpl(int token) {
+ mToken = token;
+ }
+
+ @Override
+ public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) {
+ Slog.v(TAG, "IkeOpened for token " + mToken);
+ // Nothing to do here.
+ }
+
+ @Override
+ public void onClosed() {
+ Slog.v(TAG, "IkeClosed for token " + mToken);
+ sessionClosed(mToken, null);
+ }
+
+ @Override
+ public void onClosedExceptionally(@NonNull IkeException exception) {
+ Slog.v(TAG, "IkeClosedExceptionally for token " + mToken, exception);
+ sessionClosed(mToken, exception);
+ }
+
+ @Override
+ public void onError(@NonNull IkeProtocolException exception) {
+ Slog.v(TAG, "IkeError for token " + mToken, exception);
+ // Non-fatal, log and continue.
+ }
+ }
+
+ private class ChildSessionCallbackImpl implements ChildSessionCallback {
+ private final int mToken;
+
+ ChildSessionCallbackImpl(int token) {
+ mToken = token;
+ }
+
+ @Override
+ public void onOpened(@NonNull ChildSessionConfiguration childConfig) {
+ Slog.v(TAG, "ChildOpened for token " + mToken);
+ childOpened(mToken, childConfig);
+ }
+
+ @Override
+ public void onClosed() {
+ Slog.v(TAG, "ChildClosed for token " + mToken);
+ sessionLost(mToken, null);
+ }
+
+ @Override
+ public void onClosedExceptionally(@NonNull IkeException exception) {
+ Slog.v(TAG, "ChildClosedExceptionally for token " + mToken, exception);
+ sessionLost(mToken, exception);
+ }
+
+ @Override
+ public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) {
+ Slog.v(TAG, "ChildTransformCreated; Direction: " + direction + "; token " + mToken);
+ childTransformCreated(mToken, transform, direction);
+ }
+
+ @Override
+ public void onIpSecTransformDeleted(@NonNull IpSecTransform transform, int direction) {
+ // Nothing to be done; no references to the IpSecTransform are held, and this transform
+ // will be closed by the IKE library.
+ Slog.v(TAG, "ChildTransformDeleted; Direction: " + direction + "; for token " + mToken);
+ }
+ }
+
+ /** External dependencies used by VcnGatewayConnection, for injection in tests. */
+ @VisibleForTesting(visibility = Visibility.PRIVATE)
+ public static class Dependencies {
+ /** Builds a new UnderlyingNetworkTracker. */
public UnderlyingNetworkTracker newUnderlyingNetworkTracker(
VcnContext vcnContext,
ParcelUuid subscriptionGroup,
UnderlyingNetworkTrackerCallback callback) {
return new UnderlyingNetworkTracker(vcnContext, subscriptionGroup, callback);
}
- }
- @Override
- public void onSelectedUnderlyingNetworkChanged(@Nullable UnderlyingNetworkRecord underlying) {}
+ /** Builds a new IkeSession. */
+ public IkeSession newIkeSession(
+ VcnContext vcnContext,
+ IkeSessionParams ikeSessionParams,
+ ChildSessionParams childSessionParams,
+ IkeSessionCallback ikeSessionCallback,
+ ChildSessionCallback childSessionCallback) {
+ return new IkeSession(
+ vcnContext.getContext(),
+ ikeSessionParams,
+ childSessionParams,
+ new HandlerExecutor(new Handler(vcnContext.getLooper())),
+ ikeSessionCallback,
+ childSessionCallback);
+ }
+ }
}
diff --git a/tests/vcn/Android.bp b/tests/vcn/Android.bp
index f967bf0d8f6b9..3c08d347b19a5 100644
--- a/tests/vcn/Android.bp
+++ b/tests/vcn/Android.bp
@@ -20,6 +20,7 @@ android_test {
"services.core",
],
libs: [
+ "android.net.ipsec.ike.stubs.module_lib",
"android.test.runner",
"android.test.base",
"android.test.mock",
diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
index e98b6ef2b3a68..dfd0c8a75172d 100644
--- a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
+++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java
@@ -33,12 +33,13 @@ import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class VcnGatewayConnectionConfigTest {
- private static final int[] EXPOSED_CAPS =
+ // Public for use in VcnGatewayConnectionTest
+ public static final int[] EXPOSED_CAPS =
new int[] {
NetworkCapabilities.NET_CAPABILITY_INTERNET, NetworkCapabilities.NET_CAPABILITY_MMS
};
- private static final int[] UNDERLYING_CAPS = new int[] {NetworkCapabilities.NET_CAPABILITY_DUN};
- private static final long[] RETRY_INTERVALS_MS =
+ public static final int[] UNDERLYING_CAPS = new int[] {NetworkCapabilities.NET_CAPABILITY_DUN};
+ public static final long[] RETRY_INTERVALS_MS =
new long[] {
TimeUnit.SECONDS.toMillis(5),
TimeUnit.SECONDS.toMillis(30),
@@ -47,10 +48,10 @@ public class VcnGatewayConnectionConfigTest {
TimeUnit.MINUTES.toMillis(15),
TimeUnit.MINUTES.toMillis(30)
};
- private static final int MAX_MTU = 1360;
+ public static final int MAX_MTU = 1360;
- // Package protected for use in VcnConfigTest
- static VcnGatewayConnectionConfig buildTestConfig() {
+ // Public for use in VcnGatewayConnectionTest
+ public static VcnGatewayConnectionConfig buildTestConfig() {
final VcnGatewayConnectionConfig.Builder builder =
new VcnGatewayConnectionConfig.Builder()
.setRetryInterval(RETRY_INTERVALS_MS)
diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
new file mode 100644
index 0000000000000..d741e5cf4b35d
--- /dev/null
+++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.net.NetworkCapabilities;
+import android.net.vcn.VcnGatewayConnectionConfigTest;
+import android.os.ParcelUuid;
+import android.os.test.TestLooper;
+import android.telephony.SubscriptionInfo;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/** Tests for TelephonySubscriptionTracker */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class VcnGatewayConnectionTest {
+ private static final ParcelUuid TEST_PARCEL_UUID = new ParcelUuid(UUID.randomUUID());
+ private static final int TEST_SIM_SLOT_INDEX = 1;
+ private static final int TEST_SUBSCRIPTION_ID_1 = 2;
+ private static final SubscriptionInfo TEST_SUBINFO_1 = mock(SubscriptionInfo.class);
+ private static final int TEST_SUBSCRIPTION_ID_2 = 3;
+ private static final SubscriptionInfo TEST_SUBINFO_2 = mock(SubscriptionInfo.class);
+ private static final Map