| /* |
| * 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 android.net.NetworkCapabilities.NET_CAPABILITY_DUN; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED; |
| import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; |
| import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; |
| import static android.net.NetworkCapabilities.TRANSPORT_WIFI; |
| import static android.net.ipsec.ike.exceptions.IkeProtocolException.ERROR_TYPE_AUTHENTICATION_FAILED; |
| import static android.net.vcn.VcnManager.VCN_ERROR_CODE_CONFIG_ERROR; |
| import static android.net.vcn.VcnManager.VCN_ERROR_CODE_INTERNAL_ERROR; |
| import static android.net.vcn.VcnManager.VCN_ERROR_CODE_NETWORK_ERROR; |
| |
| import static com.android.server.VcnManagementService.LOCAL_LOG; |
| import static com.android.server.VcnManagementService.VDBG; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| 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.NetworkAgentConfig; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkProvider; |
| import android.net.NetworkScore; |
| import android.net.RouteInfo; |
| import android.net.TelephonyNetworkSpecifier; |
| import android.net.Uri; |
| 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.IkeTunnelConnectionParams; |
| import android.net.ipsec.ike.exceptions.IkeException; |
| import android.net.ipsec.ike.exceptions.IkeInternalException; |
| import android.net.ipsec.ike.exceptions.IkeProtocolException; |
| import android.net.vcn.VcnGatewayConnectionConfig; |
| import android.net.vcn.VcnTransportInfo; |
| import android.net.wifi.WifiInfo; |
| import android.os.Handler; |
| import android.os.HandlerExecutor; |
| import android.os.Message; |
| import android.os.ParcelUuid; |
| import android.os.PowerManager; |
| import android.os.PowerManager.WakeLock; |
| import android.os.Process; |
| import android.os.SystemClock; |
| import android.provider.Settings; |
| import android.telephony.TelephonyManager; |
| import android.util.ArraySet; |
| 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.internal.util.State; |
| import com.android.internal.util.StateMachine; |
| import com.android.internal.util.WakeupMessage; |
| import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; |
| import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord; |
| import com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkTrackerCallback; |
| import com.android.server.vcn.Vcn.VcnGatewayStatusCallback; |
| import com.android.server.vcn.util.LogUtils; |
| import com.android.server.vcn.util.MtuUtils; |
| import com.android.server.vcn.util.OneWayBoolean; |
| |
| import java.io.IOException; |
| import java.net.Inet4Address; |
| import java.net.Inet6Address; |
| import java.net.InetAddress; |
| import java.net.NetworkInterface; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Consumer; |
| |
| /** |
| * A single VCN Gateway Connection, providing a single public-facing VCN network. |
| * |
| * <p>This class handles mobility events, performs retries, and tracks safe-mode conditions. |
| * |
| * <pre>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. |---+ |
| * +----------------------------+ |
| * </pre> |
| * |
| * <p>All messages in VcnGatewayConnection <b>should</b> be enqueued using {@link |
| * #sendMessageAndAcquireWakeLock}. Careful consideration should be given to any uses of {@link |
| * #sendMessage} directly, as they are not guaranteed to be processed in a timely manner (due to the |
| * lack of WakeLocks). |
| * |
| * <p>Any attempt to remove messages from the Handler should be done using {@link |
| * #removeEqualMessages}. This is necessary to ensure that the WakeLock is correctly released when |
| * no messages remain in the Handler queue. |
| * |
| * @hide |
| */ |
| public class VcnGatewayConnection extends StateMachine { |
| private static final String TAG = VcnGatewayConnection.class.getSimpleName(); |
| |
| // Matches DataConnection.NETWORK_TYPE private constant, and magic string from |
| // ConnectivityManager#getNetworkTypeName() |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final String NETWORK_INFO_NETWORK_TYPE_STRING = "MOBILE"; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final String NETWORK_INFO_EXTRA_INFO = "VCN"; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final InetAddress DUMMY_ADDR = InetAddresses.parseNumericAddress("192.0.2.0"); |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final String TEARDOWN_TIMEOUT_ALARM = TAG + "_TEARDOWN_TIMEOUT_ALARM"; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final String DISCONNECT_REQUEST_ALARM = TAG + "_DISCONNECT_REQUEST_ALARM"; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final String RETRY_TIMEOUT_ALARM = TAG + "_RETRY_TIMEOUT_ALARM"; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final String SAFEMODE_TIMEOUT_ALARM = TAG + "_SAFEMODE_TIMEOUT_ALARM"; |
| |
| private static final int[] MERGED_CAPABILITIES = |
| new int[] {NET_CAPABILITY_NOT_METERED, NET_CAPABILITY_NOT_ROAMING}; |
| 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_NETWORK_AGENT_UNWANTED = |
| "NetworkAgent was unwanted"; |
| private static final String DISCONNECT_REASON_TEARDOWN = "teardown() called on VcnTunnel"; |
| private static final int TOKEN_ALL = Integer.MIN_VALUE; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final int NETWORK_LOSS_DISCONNECT_TIMEOUT_SECONDS = 30; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final int TEARDOWN_TIMEOUT_SECONDS = 5; |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static final int SAFEMODE_TIMEOUT_SECONDS = 30; |
| private static final int SAFEMODE_TIMEOUT_SECONDS_TEST_MODE = 10; |
| |
| private interface EventInfo {} |
| |
| /** |
| * Sent when there are changes to the underlying network (per the UnderlyingNetworkTracker). |
| * |
| * <p>May indicate an entirely new underlying network, OR a change in network properties. |
| * |
| * <p>Relevant in ALL states. |
| * |
| * <p>In the Connected state, this MAY indicate a mobility even occurred. |
| * |
| * @param arg1 The "all" 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. |
| * |
| * <p>Only relevant in the Retry-timeout state, discarded in all other states. |
| * |
| * <p>Upon receipt of this signal, the state machine will transition from the Retry-timeout |
| * state to the Connecting state. |
| * |
| * @param arg1 The "all" 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. |
| * |
| * <p>Relevant in all states that have an IKE session. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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. |
| * |
| * <p>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 VcnChildSessionConfiguration childSessionConfig; |
| |
| EventSetupCompletedInfo(@NonNull VcnChildSessionConfiguration 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. |
| * |
| * <p>Relevant in all states except the Disconnected state. |
| * |
| * <p>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. |
| * |
| * <p>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 "all" 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; |
| |
| public final boolean shouldQuit; |
| |
| EventDisconnectRequestedInfo(@NonNull String reason, boolean shouldQuit) { |
| this.reason = Objects.requireNonNull(reason); |
| this.shouldQuit = shouldQuit; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(reason, shouldQuit); |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object other) { |
| if (!(other instanceof EventDisconnectRequestedInfo)) { |
| return false; |
| } |
| |
| final EventDisconnectRequestedInfo rhs = (EventDisconnectRequestedInfo) other; |
| return reason.equals(rhs.reason) && shouldQuit == rhs.shouldQuit; |
| } |
| } |
| |
| /** |
| * Sent (delayed) to trigger a forcible close of an IKE session. |
| * |
| * <p>Only relevant in the Disconnecting state, discarded in all other states. |
| * |
| * <p>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; |
| |
| /** |
| * Sent when this VcnGatewayConnection is notified of a change in TelephonySubscriptions. |
| * |
| * <p>Relevant in all states. |
| * |
| * @param arg1 The "all" token; this signal is always honored. |
| */ |
| // TODO(b/178426520): implement handling of this event |
| private static final int EVENT_SUBSCRIPTIONS_CHANGED = 9; |
| |
| /** |
| * Sent when this VcnGatewayConnection has entered safe mode. |
| * |
| * <p>A VcnGatewayConnection enters safe mode when it takes over {@link |
| * #SAFEMODE_TIMEOUT_SECONDS} to enter {@link ConnectedState}. |
| * |
| * <p>When a VcnGatewayConnection enters safe mode, it will fire {@link |
| * VcnGatewayStatusCallback#onEnteredSafeMode()} to notify its Vcn. The Vcn will then shut down |
| * its VcnGatewayConnectin(s). |
| * |
| * <p>Relevant in DisconnectingState, ConnectingState, ConnectedState (if the Vcn Network is not |
| * validated yet), and RetryTimeoutState. |
| * |
| * @param arg1 The "all" token; this signal is always honored. |
| */ |
| private static final int EVENT_SAFE_MODE_TIMEOUT_EXCEEDED = 10; |
| |
| /** |
| * Sent when an IKE has completed migration, and created updated transforms for application. |
| * |
| * <p>Only relevant in the Connected state. |
| * |
| * @param arg1 The session token for the IKE Session that completed migration, used to prevent |
| * out-of-date signals from propagating. |
| * @param obj @NonNull An EventMigrationCompletedInfo instance with relevant data. |
| */ |
| private static final int EVENT_MIGRATION_COMPLETED = 11; |
| |
| private static class EventMigrationCompletedInfo implements EventInfo { |
| @NonNull public final IpSecTransform inTransform; |
| @NonNull public final IpSecTransform outTransform; |
| |
| EventMigrationCompletedInfo( |
| @NonNull IpSecTransform inTransform, @NonNull IpSecTransform outTransform) { |
| this.inTransform = Objects.requireNonNull(inTransform); |
| this.outTransform = Objects.requireNonNull(outTransform); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(inTransform, outTransform); |
| } |
| |
| @Override |
| public boolean equals(@Nullable Object other) { |
| if (!(other instanceof EventMigrationCompletedInfo)) { |
| return false; |
| } |
| |
| final EventMigrationCompletedInfo rhs = (EventMigrationCompletedInfo) other; |
| return Objects.equals(inTransform, rhs.inTransform) |
| && Objects.equals(outTransform, rhs.outTransform); |
| } |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| @NonNull |
| final DisconnectedState mDisconnectedState = new DisconnectedState(); |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| @NonNull |
| final DisconnectingState mDisconnectingState = new DisconnectingState(); |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| @NonNull |
| final ConnectingState mConnectingState = new ConnectingState(); |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| @NonNull |
| final ConnectedState mConnectedState = new ConnectedState(); |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| @NonNull |
| final RetryTimeoutState mRetryTimeoutState = new RetryTimeoutState(); |
| |
| @NonNull private TelephonySubscriptionSnapshot mLastSnapshot; |
| |
| @NonNull private final VcnContext mVcnContext; |
| @NonNull private final ParcelUuid mSubscriptionGroup; |
| @NonNull private final UnderlyingNetworkTracker mUnderlyingNetworkTracker; |
| @NonNull private final VcnGatewayConnectionConfig mConnectionConfig; |
| @NonNull private final VcnGatewayStatusCallback mGatewayStatusCallback; |
| @NonNull private final Dependencies mDeps; |
| @NonNull private final VcnUnderlyingNetworkTrackerCallback mUnderlyingNetworkTrackerCallback; |
| private final boolean mIsMobileDataEnabled; |
| |
| @NonNull private final IpSecManager mIpSecManager; |
| |
| @Nullable private IpSecTunnelInterface mTunnelIface = null; |
| |
| /** |
| * WakeLock to be held when processing messages on the Handler queue. |
| * |
| * <p>Used to prevent the device from going to sleep while there are VCN-related events to |
| * process for this VcnGatewayConnection. |
| * |
| * <p>Obtain a WakeLock when enquing messages onto the Handler queue. Once all messages in the |
| * Handler queue have been processed, the WakeLock can be released and cleared. |
| * |
| * <p>This WakeLock is also used for handling delayed messages by using WakeupMessages to send |
| * delayed messages to the Handler. When the WakeupMessage fires, it will obtain the WakeLock |
| * before enquing the delayed event to the Handler. |
| */ |
| @NonNull private final VcnWakeLock mWakeLock; |
| |
| /** |
| * Whether the VcnGatewayConnection is in the process of irreversibly quitting. |
| * |
| * <p>This variable is false for the lifecycle of the VcnGatewayConnection, until a command to |
| * teardown has been received. This may be flipped due to events such as the Network becoming |
| * unwanted, the owning VCN entering safe mode, or an irrecoverable internal failure. |
| * |
| * <p>WARNING: Assignments to this MUST ALWAYS (except for testing) use the or operator ("|="), |
| * otherwise the flag may be flipped back to false after having been set to true. This could |
| * lead to a case where the Vcn parent instance has commanded a teardown, but a spurious |
| * non-quitting disconnect request could flip this back to true. |
| */ |
| private OneWayBoolean mIsQuitting = new OneWayBoolean(); |
| |
| /** |
| * Whether the VcnGatewayConnection is in safe mode. |
| * |
| * <p>Upon hitting the safe mode timeout, this will be set to {@code true}. In safe mode, this |
| * VcnGatewayConnection will continue attempting to connect, and if a successful connection is |
| * made, safe mode will be exited. |
| */ |
| private boolean mIsInSafeMode = false; |
| |
| /** |
| * The token used by the primary/current/active session. |
| * |
| * <p>This token MUST be updated when a new stateful/async session becomes the |
| * primary/current/active session. Example cases where the session changes are: |
| * |
| * <ul> |
| * <li>Switching to an IKE session as the primary session |
| * </ul> |
| * |
| * <p>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: |
| * |
| * <ul> |
| * <li>The new session connects successfully, and becomes the primary session |
| * <li>The existing session is lost, and the remaining (new) session becomes the primary |
| * session |
| * </ul> |
| */ |
| private int mCurrentToken = -1; |
| |
| /** |
| * The number of unsuccessful attempts since the last successful connection. |
| * |
| * <p>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. |
| * |
| * <p>Set in any states, always @NonNull in all states except Disconnected, null otherwise. |
| */ |
| private UnderlyingNetworkRecord mUnderlying; |
| |
| /** |
| * The active IKE session. |
| * |
| * <p>Set in Connecting or Migrating States, always @NonNull in Connecting, Connected, and |
| * Migrating states, null otherwise. |
| */ |
| private VcnIkeSession mIkeSession; |
| |
| /** |
| * The last known child configuration. |
| * |
| * <p>Set in Connected and Migrating states, always @NonNull in Connected, Migrating |
| * states, @Nullable otherwise. |
| */ |
| private VcnChildSessionConfiguration mChildConfig; |
| |
| /** |
| * The active network agent. |
| * |
| * <p>Set in Connected state, always @NonNull in Connected, Migrating states, @Nullable |
| * otherwise. |
| */ |
| private VcnNetworkAgent mNetworkAgent; |
| |
| @Nullable private WakeupMessage mTeardownTimeoutAlarm; |
| @Nullable private WakeupMessage mDisconnectRequestAlarm; |
| @Nullable private WakeupMessage mRetryTimeoutAlarm; |
| @Nullable private WakeupMessage mSafeModeTimeoutAlarm; |
| |
| public VcnGatewayConnection( |
| @NonNull VcnContext vcnContext, |
| @NonNull ParcelUuid subscriptionGroup, |
| @NonNull TelephonySubscriptionSnapshot snapshot, |
| @NonNull VcnGatewayConnectionConfig connectionConfig, |
| @NonNull VcnGatewayStatusCallback gatewayStatusCallback, |
| boolean isMobileDataEnabled) { |
| this( |
| vcnContext, |
| subscriptionGroup, |
| snapshot, |
| connectionConfig, |
| gatewayStatusCallback, |
| isMobileDataEnabled, |
| new Dependencies()); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| VcnGatewayConnection( |
| @NonNull VcnContext vcnContext, |
| @NonNull ParcelUuid subscriptionGroup, |
| @NonNull TelephonySubscriptionSnapshot snapshot, |
| @NonNull VcnGatewayConnectionConfig connectionConfig, |
| @NonNull VcnGatewayStatusCallback gatewayStatusCallback, |
| boolean isMobileDataEnabled, |
| @NonNull Dependencies deps) { |
| super(TAG, Objects.requireNonNull(vcnContext, "Missing vcnContext").getLooper()); |
| mVcnContext = vcnContext; |
| mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); |
| mConnectionConfig = Objects.requireNonNull(connectionConfig, "Missing connectionConfig"); |
| mGatewayStatusCallback = |
| Objects.requireNonNull(gatewayStatusCallback, "Missing gatewayStatusCallback"); |
| mIsMobileDataEnabled = isMobileDataEnabled; |
| mDeps = Objects.requireNonNull(deps, "Missing deps"); |
| |
| mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot"); |
| |
| mUnderlyingNetworkTrackerCallback = new VcnUnderlyingNetworkTrackerCallback(); |
| |
| mWakeLock = |
| mDeps.newWakeLock(mVcnContext.getContext(), PowerManager.PARTIAL_WAKE_LOCK, TAG); |
| |
| mUnderlyingNetworkTracker = |
| mDeps.newUnderlyingNetworkTracker( |
| mVcnContext, |
| subscriptionGroup, |
| mLastSnapshot, |
| mUnderlyingNetworkTrackerCallback); |
| mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class); |
| |
| addState(mDisconnectedState); |
| addState(mDisconnectingState); |
| addState(mConnectingState); |
| addState(mConnectedState); |
| addState(mRetryTimeoutState); |
| |
| setInitialState(mDisconnectedState); |
| setDbg(VDBG); |
| start(); |
| } |
| |
| /** Queries whether this VcnGatewayConnection is in safe mode. */ |
| public boolean isInSafeMode() { |
| // Accessing internal state; must only be done on looper thread. |
| mVcnContext.ensureRunningOnLooperThread(); |
| |
| return mIsInSafeMode; |
| } |
| |
| /** |
| * Asynchronously tears down this GatewayConnection, and any resources used. |
| * |
| * <p>Once torn down, this VcnTunnel CANNOT be started again. |
| */ |
| public void teardownAsynchronously() { |
| logDbg("Triggering async teardown"); |
| sendDisconnectRequestedAndAcquireWakelock( |
| DISCONNECT_REASON_TEARDOWN, true /* shouldQuit */); |
| |
| // TODO: Notify VcnInstance (via callbacks) of permanent teardown of this tunnel, since this |
| // is also called asynchronously when a NetworkAgent becomes unwanted |
| } |
| |
| @Override |
| protected void onQuitting() { |
| logDbg("Quitting VcnGatewayConnection"); |
| |
| if (mNetworkAgent != null) { |
| logWtf("NetworkAgent was non-null in onQuitting"); |
| mNetworkAgent.unregister(); |
| mNetworkAgent = null; |
| } |
| |
| if (mIkeSession != null) { |
| logWtf("IkeSession was non-null in onQuitting"); |
| mIkeSession.kill(); |
| mIkeSession = null; |
| } |
| |
| // No need to call setInterfaceDown(); the IpSecInterface is being fully torn down. |
| if (mTunnelIface != null) { |
| mTunnelIface.close(); |
| } |
| |
| releaseWakeLock(); |
| |
| cancelTeardownTimeoutAlarm(); |
| cancelDisconnectRequestAlarm(); |
| cancelRetryTimeoutAlarm(); |
| cancelSafeModeAlarm(); |
| |
| mUnderlyingNetworkTracker.teardown(); |
| |
| mGatewayStatusCallback.onQuit(); |
| } |
| |
| /** |
| * Notify this Gateway that subscriptions have changed. |
| * |
| * <p>This snapshot should be used to update any keepalive requests necessary for potential |
| * underlying Networks in this Gateway's subscription group. |
| */ |
| public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) { |
| Objects.requireNonNull(snapshot, "Missing snapshot"); |
| mVcnContext.ensureRunningOnLooperThread(); |
| |
| mLastSnapshot = snapshot; |
| mUnderlyingNetworkTracker.updateSubscriptionSnapshot(mLastSnapshot); |
| |
| sendMessageAndAcquireWakeLock(EVENT_SUBSCRIPTIONS_CHANGED, TOKEN_ALL); |
| } |
| |
| private class VcnUnderlyingNetworkTrackerCallback implements UnderlyingNetworkTrackerCallback { |
| @Override |
| public void onSelectedUnderlyingNetworkChanged( |
| @Nullable UnderlyingNetworkRecord underlying) { |
| // TODO(b/180132994): explore safely removing this Thread check |
| mVcnContext.ensureRunningOnLooperThread(); |
| |
| logDbg( |
| "Selected underlying network changed: " |
| + (underlying == null ? null : underlying.network)); |
| |
| // TODO(b/179091925): Move the delayed-message handling to BaseState |
| |
| // If underlying is null, all underlying networks have been lost. Disconnect VCN after a |
| // timeout (or immediately if in airplane mode, since the device user has indicated that |
| // the radios should all be turned off). |
| if (underlying == null) { |
| if (mDeps.isAirplaneModeOn(mVcnContext)) { |
| sendMessageAndAcquireWakeLock( |
| EVENT_UNDERLYING_NETWORK_CHANGED, |
| TOKEN_ALL, |
| new EventUnderlyingNetworkChangedInfo(null)); |
| sendDisconnectRequestedAndAcquireWakelock( |
| DISCONNECT_REASON_UNDERLYING_NETWORK_LOST, false /* shouldQuit */); |
| return; |
| } |
| |
| setDisconnectRequestAlarm(); |
| } else { |
| // Received a new Network so any previous alarm is irrelevant - cancel + clear it, |
| // and cancel any queued EVENT_DISCONNECT_REQUEST messages |
| cancelDisconnectRequestAlarm(); |
| } |
| |
| sendMessageAndAcquireWakeLock( |
| EVENT_UNDERLYING_NETWORK_CHANGED, |
| TOKEN_ALL, |
| new EventUnderlyingNetworkChangedInfo(underlying)); |
| } |
| } |
| |
| private void acquireWakeLock() { |
| mVcnContext.ensureRunningOnLooperThread(); |
| |
| if (!mIsQuitting.getValue()) { |
| mWakeLock.acquire(); |
| |
| logVdbg("Wakelock acquired: " + mWakeLock); |
| } |
| } |
| |
| private void releaseWakeLock() { |
| mVcnContext.ensureRunningOnLooperThread(); |
| |
| mWakeLock.release(); |
| |
| logVdbg("Wakelock released: " + mWakeLock); |
| } |
| |
| /** |
| * Attempt to release mWakeLock - this can only be done if the Handler is null (meaning the |
| * StateMachine has been shutdown and thus has no business keeping the WakeLock) or if there are |
| * no more messags left to process in the Handler queue (at which point the WakeLock can be |
| * released until more messages must be processed). |
| */ |
| private void maybeReleaseWakeLock() { |
| final Handler handler = getHandler(); |
| if (handler == null || !handler.hasMessagesOrCallbacks()) { |
| releaseWakeLock(); |
| } |
| } |
| |
| @Override |
| public void sendMessage(int what) { |
| logWtf( |
| "sendMessage should not be used in VcnGatewayConnection. See" |
| + " sendMessageAndAcquireWakeLock()"); |
| super.sendMessage(what); |
| } |
| |
| @Override |
| public void sendMessage(int what, Object obj) { |
| logWtf( |
| "sendMessage should not be used in VcnGatewayConnection. See" |
| + " sendMessageAndAcquireWakeLock()"); |
| super.sendMessage(what, obj); |
| } |
| |
| @Override |
| public void sendMessage(int what, int arg1) { |
| logWtf( |
| "sendMessage should not be used in VcnGatewayConnection. See" |
| + " sendMessageAndAcquireWakeLock()"); |
| super.sendMessage(what, arg1); |
| } |
| |
| @Override |
| public void sendMessage(int what, int arg1, int arg2) { |
| logWtf( |
| "sendMessage should not be used in VcnGatewayConnection. See" |
| + " sendMessageAndAcquireWakeLock()"); |
| super.sendMessage(what, arg1, arg2); |
| } |
| |
| @Override |
| public void sendMessage(int what, int arg1, int arg2, Object obj) { |
| logWtf( |
| "sendMessage should not be used in VcnGatewayConnection. See" |
| + " sendMessageAndAcquireWakeLock()"); |
| super.sendMessage(what, arg1, arg2, obj); |
| } |
| |
| @Override |
| public void sendMessage(Message msg) { |
| logWtf( |
| "sendMessage should not be used in VcnGatewayConnection. See" |
| + " sendMessageAndAcquireWakeLock()"); |
| super.sendMessage(msg); |
| } |
| |
| // TODO(b/180146061): also override and Log.wtf() other Message handling methods |
| // In mind are sendMessageDelayed(), sendMessageAtFrontOfQueue, removeMessages, and |
| // removeDeferredMessages |
| |
| /** |
| * WakeLock-based alternative to {@link #sendMessage}. Use to guarantee that the device will not |
| * go to sleep before processing the sent message. |
| */ |
| private void sendMessageAndAcquireWakeLock(int what, int token) { |
| acquireWakeLock(); |
| super.sendMessage(what, token); |
| } |
| |
| /** |
| * WakeLock-based alternative to {@link #sendMessage}. Use to guarantee that the device will not |
| * go to sleep before processing the sent message. |
| */ |
| private void sendMessageAndAcquireWakeLock(int what, int token, EventInfo data) { |
| acquireWakeLock(); |
| super.sendMessage(what, token, ARG_NOT_PRESENT, data); |
| } |
| |
| /** |
| * WakeLock-based alternative to {@link #sendMessage}. Use to guarantee that the device will not |
| * go to sleep before processing the sent message. |
| */ |
| private void sendMessageAndAcquireWakeLock(int what, int token, int arg2, EventInfo data) { |
| acquireWakeLock(); |
| super.sendMessage(what, token, arg2, data); |
| } |
| |
| /** |
| * WakeLock-based alternative to {@link #sendMessage}. Use to guarantee that the device will not |
| * go to sleep before processing the sent message. |
| */ |
| private void sendMessageAndAcquireWakeLock(Message msg) { |
| acquireWakeLock(); |
| super.sendMessage(msg); |
| } |
| |
| /** |
| * Removes all messages matching the given parameters, and attempts to release mWakeLock if the |
| * Handler is empty. |
| * |
| * @param what the Message.what value to be removed |
| */ |
| private void removeEqualMessages(int what) { |
| removeEqualMessages(what, null /* obj */); |
| } |
| |
| /** |
| * Removes all messages matching the given parameters, and attempts to release mWakeLock if the |
| * Handler is empty. |
| * |
| * @param what the Message.what value to be removed |
| * @param obj the Message.obj to to be removed, or null if all messages matching Message.what |
| * should be removed |
| */ |
| private void removeEqualMessages(int what, @Nullable Object obj) { |
| final Handler handler = getHandler(); |
| if (handler != null) { |
| handler.removeEqualMessages(what, obj); |
| } |
| |
| maybeReleaseWakeLock(); |
| } |
| |
| private WakeupMessage createScheduledAlarm( |
| @NonNull String cmdName, Message delayedMessage, long delay) { |
| final Handler handler = getHandler(); |
| if (handler == null) { |
| logWarn( |
| "Attempted to schedule alarm after StateMachine has quit", |
| new IllegalStateException()); |
| return null; // StateMachine has already quit. |
| } |
| |
| // WakeupMessage uses Handler#dispatchMessage() to immediately handle the specified Runnable |
| // at the scheduled time. dispatchMessage() immediately executes and there may be queued |
| // events that resolve the scheduled alarm pending in the queue. So, use the Runnable to |
| // place the alarm event at the end of the queue with sendMessageAndAcquireWakeLock (which |
| // guarantees the device will stay awake). |
| final WakeupMessage alarm = |
| mDeps.newWakeupMessage( |
| mVcnContext, |
| handler, |
| cmdName, |
| () -> sendMessageAndAcquireWakeLock(delayedMessage)); |
| alarm.schedule(mDeps.getElapsedRealTime() + delay); |
| return alarm; |
| } |
| |
| private void setTeardownTimeoutAlarm() { |
| logVdbg("Setting teardown timeout alarm; mCurrentToken: " + mCurrentToken); |
| |
| // Safe to assign this alarm because it is either 1) already null, or 2) already fired. In |
| // either case, there is nothing to cancel. |
| if (mTeardownTimeoutAlarm != null) { |
| logWtf( |
| "mTeardownTimeoutAlarm should be null before being set; mCurrentToken: " |
| + mCurrentToken); |
| } |
| |
| final Message delayedMessage = obtainMessage(EVENT_TEARDOWN_TIMEOUT_EXPIRED, mCurrentToken); |
| mTeardownTimeoutAlarm = |
| createScheduledAlarm( |
| TEARDOWN_TIMEOUT_ALARM, |
| delayedMessage, |
| TimeUnit.SECONDS.toMillis(TEARDOWN_TIMEOUT_SECONDS)); |
| } |
| |
| private void cancelTeardownTimeoutAlarm() { |
| logVdbg("Cancelling teardown timeout alarm; mCurrentToken: " + mCurrentToken); |
| |
| if (mTeardownTimeoutAlarm != null) { |
| mTeardownTimeoutAlarm.cancel(); |
| mTeardownTimeoutAlarm = null; |
| } |
| |
| // Cancel any existing teardown timeouts |
| removeEqualMessages(EVENT_TEARDOWN_TIMEOUT_EXPIRED); |
| } |
| |
| private void setDisconnectRequestAlarm() { |
| logVdbg( |
| "Setting alarm to disconnect due to underlying network loss;" |
| + " mCurrentToken: " |
| + mCurrentToken); |
| |
| // Only schedule a NEW alarm if none is already set. |
| if (mDisconnectRequestAlarm != null) { |
| return; |
| } |
| |
| final Message delayedMessage = |
| obtainMessage( |
| EVENT_DISCONNECT_REQUESTED, |
| TOKEN_ALL, |
| 0 /* arg2 */, |
| new EventDisconnectRequestedInfo( |
| DISCONNECT_REASON_UNDERLYING_NETWORK_LOST, false /* shouldQuit */)); |
| mDisconnectRequestAlarm = |
| createScheduledAlarm( |
| DISCONNECT_REQUEST_ALARM, |
| delayedMessage, |
| TimeUnit.SECONDS.toMillis(NETWORK_LOSS_DISCONNECT_TIMEOUT_SECONDS)); |
| } |
| |
| private void cancelDisconnectRequestAlarm() { |
| logVdbg( |
| "Cancelling alarm to disconnect due to underlying network loss;" |
| + " mCurrentToken: " |
| + mCurrentToken); |
| |
| if (mDisconnectRequestAlarm != null) { |
| mDisconnectRequestAlarm.cancel(); |
| mDisconnectRequestAlarm = null; |
| } |
| |
| // Cancel any existing disconnect due to previous loss of underlying network |
| removeEqualMessages( |
| EVENT_DISCONNECT_REQUESTED, |
| new EventDisconnectRequestedInfo( |
| DISCONNECT_REASON_UNDERLYING_NETWORK_LOST, false /* shouldQuit */)); |
| } |
| |
| private void setRetryTimeoutAlarm(long delay) { |
| logVdbg("Setting retry alarm; mCurrentToken: " + mCurrentToken); |
| |
| // Safe to assign this alarm because it is either 1) already null, or 2) already fired. In |
| // either case, there is nothing to cancel. |
| if (mRetryTimeoutAlarm != null) { |
| logWtf( |
| "mRetryTimeoutAlarm should be null before being set; mCurrentToken: " |
| + mCurrentToken); |
| } |
| |
| final Message delayedMessage = obtainMessage(EVENT_RETRY_TIMEOUT_EXPIRED, mCurrentToken); |
| mRetryTimeoutAlarm = createScheduledAlarm(RETRY_TIMEOUT_ALARM, delayedMessage, delay); |
| } |
| |
| private void cancelRetryTimeoutAlarm() { |
| logVdbg("Cancel retry alarm; mCurrentToken: " + mCurrentToken); |
| |
| if (mRetryTimeoutAlarm != null) { |
| mRetryTimeoutAlarm.cancel(); |
| mRetryTimeoutAlarm = null; |
| } |
| |
| removeEqualMessages(EVENT_RETRY_TIMEOUT_EXPIRED); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void setSafeModeAlarm() { |
| logVdbg("Setting safe mode alarm; mCurrentToken: " + mCurrentToken); |
| |
| // Only schedule a NEW alarm if none is already set. |
| if (mSafeModeTimeoutAlarm != null) { |
| return; |
| } |
| |
| final Message delayedMessage = obtainMessage(EVENT_SAFE_MODE_TIMEOUT_EXCEEDED, TOKEN_ALL); |
| mSafeModeTimeoutAlarm = |
| createScheduledAlarm( |
| SAFEMODE_TIMEOUT_ALARM, |
| delayedMessage, |
| mVcnContext.isInTestMode() |
| ? TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS_TEST_MODE) |
| : TimeUnit.SECONDS.toMillis(SAFEMODE_TIMEOUT_SECONDS)); |
| } |
| |
| private void cancelSafeModeAlarm() { |
| logVdbg("Cancel safe mode alarm; mCurrentToken: " + mCurrentToken); |
| |
| if (mSafeModeTimeoutAlarm != null) { |
| mSafeModeTimeoutAlarm.cancel(); |
| mSafeModeTimeoutAlarm = null; |
| } |
| |
| removeEqualMessages(EVENT_SAFE_MODE_TIMEOUT_EXCEEDED); |
| } |
| |
| private void sessionLostWithoutCallback(int token, @Nullable Exception exception) { |
| sendMessageAndAcquireWakeLock( |
| EVENT_SESSION_LOST, token, new EventSessionLostInfo(exception)); |
| } |
| |
| private void sessionLost(int token, @Nullable Exception exception) { |
| // Only notify mGatewayStatusCallback if the session was lost with an error. All |
| // authentication and DNS failures are sent through |
| // IkeSessionCallback.onClosedExceptionally(), which calls sessionClosed() |
| if (exception != null) { |
| mGatewayStatusCallback.onGatewayConnectionError( |
| mConnectionConfig.getGatewayConnectionName(), |
| VCN_ERROR_CODE_INTERNAL_ERROR, |
| RuntimeException.class.getName(), |
| "Received " |
| + exception.getClass().getSimpleName() |
| + " with message: " |
| + exception.getMessage()); |
| } |
| |
| sessionLostWithoutCallback(token, exception); |
| } |
| |
| private static boolean isIkeAuthFailure(@NonNull Exception exception) { |
| if (!(exception instanceof IkeProtocolException)) { |
| return false; |
| } |
| |
| return ((IkeProtocolException) exception).getErrorType() |
| == ERROR_TYPE_AUTHENTICATION_FAILED; |
| } |
| |
| private void notifyStatusCallbackForSessionClosed(@NonNull Exception exception) { |
| final int errorCode; |
| final String exceptionClass; |
| final String exceptionMessage; |
| |
| if (isIkeAuthFailure(exception)) { |
| errorCode = VCN_ERROR_CODE_CONFIG_ERROR; |
| exceptionClass = exception.getClass().getName(); |
| exceptionMessage = exception.getMessage(); |
| } else if (exception instanceof IkeInternalException |
| && exception.getCause() instanceof IOException) { |
| errorCode = VCN_ERROR_CODE_NETWORK_ERROR; |
| exceptionClass = IOException.class.getName(); |
| exceptionMessage = exception.getCause().getMessage(); |
| } else { |
| errorCode = VCN_ERROR_CODE_INTERNAL_ERROR; |
| exceptionClass = RuntimeException.class.getName(); |
| exceptionMessage = |
| "Received " |
| + exception.getClass().getSimpleName() |
| + " with message: " |
| + exception.getMessage(); |
| } |
| |
| logDbg( |
| "Encountered error; code=" |
| + errorCode |
| + ", exceptionClass=" |
| + exceptionClass |
| + ", exceptionMessage=" |
| + exceptionMessage); |
| |
| mGatewayStatusCallback.onGatewayConnectionError( |
| mConnectionConfig.getGatewayConnectionName(), |
| errorCode, |
| exceptionClass, |
| exceptionMessage); |
| } |
| |
| private void sessionClosed(int token, @Nullable Exception exception) { |
| if (exception != null) { |
| notifyStatusCallbackForSessionClosed(exception); |
| } |
| |
| // SESSION_LOST MUST be sent before SESSION_CLOSED to ensure that the SM moves to the |
| // Disconnecting state. |
| sessionLostWithoutCallback(token, exception); |
| sendMessageAndAcquireWakeLock(EVENT_SESSION_CLOSED, token); |
| } |
| |
| private void migrationCompleted( |
| int token, @NonNull IpSecTransform inTransform, @NonNull IpSecTransform outTransform) { |
| sendMessageAndAcquireWakeLock( |
| EVENT_MIGRATION_COMPLETED, |
| token, |
| new EventMigrationCompletedInfo(inTransform, outTransform)); |
| } |
| |
| private void childTransformCreated( |
| int token, @NonNull IpSecTransform transform, int direction) { |
| sendMessageAndAcquireWakeLock( |
| EVENT_TRANSFORM_CREATED, |
| token, |
| new EventTransformCreatedInfo(direction, transform)); |
| } |
| |
| private void childOpened(int token, @NonNull VcnChildSessionConfiguration childConfig) { |
| sendMessageAndAcquireWakeLock( |
| EVENT_SETUP_COMPLETED, token, new EventSetupCompletedInfo(childConfig)); |
| } |
| |
| private abstract class BaseState extends State { |
| @Override |
| public void enter() { |
| try { |
| enterState(); |
| } catch (Exception e) { |
| logWtf("Uncaught exception", e); |
| sendDisconnectRequestedAndAcquireWakelock( |
| DISCONNECT_REASON_INTERNAL_ERROR + e.toString(), true /* shouldQuit */); |
| } |
| } |
| |
| protected void enterState() throws Exception {} |
| |
| /** |
| * Returns whether the given token is valid. |
| * |
| * <p>By default, States consider any and all token to be 'valid'. |
| * |
| * <p>States should override this method if they want to restrict message handling to |
| * specific tokens. |
| */ |
| protected boolean isValidToken(int token) { |
| return true; |
| } |
| |
| /** |
| * Top-level processMessage with safeguards to prevent crashing the System Server on non-eng |
| * builds. |
| * |
| * <p>Here be dragons: processMessage() is final to ensure that mWakeLock is released once |
| * the Handler queue is empty. Future changes (or overrides) to processMessage() to MUST |
| * ensure that mWakeLock is correctly released. |
| */ |
| @Override |
| public final boolean processMessage(Message msg) { |
| final int token = msg.arg1; |
| if (!isValidToken(token)) { |
| logDbg("Message called with obsolete token: " + token + "; what: " + msg.what); |
| return HANDLED; |
| } |
| |
| try { |
| processStateMsg(msg); |
| } catch (Exception e) { |
| logWtf("Uncaught exception", e); |
| sendDisconnectRequestedAndAcquireWakelock( |
| DISCONNECT_REASON_INTERNAL_ERROR + e.toString(), true /* shouldQuit */); |
| } |
| |
| // Attempt to release the WakeLock - only possible if the Handler queue is empty |
| maybeReleaseWakeLock(); |
| |
| return HANDLED; |
| } |
| |
| protected abstract void processStateMsg(Message msg) throws Exception; |
| |
| @Override |
| public void exit() { |
| try { |
| exitState(); |
| } catch (Exception e) { |
| logWtf("Uncaught exception", e); |
| sendDisconnectRequestedAndAcquireWakelock( |
| DISCONNECT_REASON_INTERNAL_ERROR + e.toString(), true /* shouldQuit */); |
| } |
| } |
| |
| protected void exitState() throws Exception {} |
| |
| protected void logUnhandledMessage(Message msg) { |
| // Log as unexpected all known messages, and log all else as unknown. |
| switch (msg.what) { |
| case EVENT_UNDERLYING_NETWORK_CHANGED: // Fallthrough |
| case EVENT_RETRY_TIMEOUT_EXPIRED: // Fallthrough |
| case EVENT_SESSION_LOST: // Fallthrough |
| case EVENT_SESSION_CLOSED: // Fallthrough |
| case EVENT_TRANSFORM_CREATED: // Fallthrough |
| case EVENT_SETUP_COMPLETED: // Fallthrough |
| case EVENT_DISCONNECT_REQUESTED: // Fallthrough |
| case EVENT_TEARDOWN_TIMEOUT_EXPIRED: // Fallthrough |
| case EVENT_SUBSCRIPTIONS_CHANGED: // Fallthrough |
| case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: // Fallthrough |
| case EVENT_MIGRATION_COMPLETED: |
| logUnexpectedEvent(msg.what); |
| break; |
| default: |
| logWtfUnknownEvent(msg.what); |
| break; |
| } |
| } |
| |
| protected void teardownNetwork() { |
| if (mNetworkAgent != null) { |
| mNetworkAgent.unregister(); |
| mNetworkAgent = null; |
| } |
| } |
| |
| protected void handleDisconnectRequested(EventDisconnectRequestedInfo info) { |
| // TODO(b/180526152): notify VcnStatusCallback for Network loss |
| |
| logDbg("Tearing down. Cause: " + info.reason); |
| if (info.shouldQuit) { |
| mIsQuitting.setTrue(); |
| } |
| |
| teardownNetwork(); |
| |
| if (mIkeSession == null) { |
| // Already disconnected, go straight to DisconnectedState |
| transitionTo(mDisconnectedState); |
| } else { |
| // Still need to wait for full closure |
| transitionTo(mDisconnectingState); |
| } |
| } |
| |
| protected void handleSafeModeTimeoutExceeded() { |
| mSafeModeTimeoutAlarm = null; |
| logDbg("Entering safe mode after timeout exceeded"); |
| |
| // Connectivity for this GatewayConnection is broken; tear down the Network. |
| teardownNetwork(); |
| mIsInSafeMode = true; |
| mGatewayStatusCallback.onSafeModeStatusChanged(); |
| } |
| |
| protected void logUnexpectedEvent(int what) { |
| logDbg( |
| "Unexpected event code " |
| + what |
| + " in state " |
| + this.getClass().getSimpleName()); |
| } |
| |
| protected void logWtfUnknownEvent(int what) { |
| logWtf("Unknown event code " + what + " in state " + this.getClass().getSimpleName()); |
| } |
| } |
| |
| /** |
| * State representing the a disconnected VCN tunnel. |
| * |
| * <p>This is also is the initial state. |
| */ |
| private class DisconnectedState extends BaseState { |
| @Override |
| protected void enterState() { |
| if (mIsQuitting.getValue()) { |
| quitNow(); // Ignore all queued events; cleanup is complete. |
| } |
| |
| if (mIkeSession != null || mNetworkAgent != null) { |
| logWtf("Active IKE Session or NetworkAgent in DisconnectedState"); |
| } |
| |
| cancelSafeModeAlarm(); |
| } |
| |
| @Override |
| protected void processStateMsg(Message msg) { |
| switch (msg.what) { |
| case EVENT_UNDERLYING_NETWORK_CHANGED: |
| // First network found; start tunnel |
| mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; |
| |
| if (mUnderlying != null) { |
| transitionTo(mConnectingState); |
| } |
| break; |
| case EVENT_DISCONNECT_REQUESTED: |
| if (((EventDisconnectRequestedInfo) msg.obj).shouldQuit) { |
| mIsQuitting.setTrue(); |
| |
| quitNow(); |
| } |
| break; |
| default: |
| logUnhandledMessage(msg); |
| break; |
| } |
| } |
| |
| @Override |
| protected void exitState() { |
| // Safe to blindly set up, as it is cancelled and cleared on entering this state |
| setSafeModeAlarm(); |
| } |
| } |
| |
| private abstract class ActiveBaseState extends BaseState { |
| @Override |
| protected boolean isValidToken(int token) { |
| return (token == TOKEN_ALL || token == mCurrentToken); |
| } |
| } |
| |
| /** |
| * Transitive state representing a VCN that is tearing down an IKE session. |
| * |
| * <p>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 { |
| /** |
| * Whether to skip the RetryTimeoutState and go straight to the ConnectingState. |
| * |
| * <p>This is used when an underlying network change triggered a restart on a new network. |
| * |
| * <p>Reset (to false) upon exit of the DisconnectingState. |
| */ |
| private boolean mSkipRetryTimeout = false; |
| |
| // TODO(b/178441390): Remove this in favor of resetting retry timers on UND_NET change. |
| public void setSkipRetryTimeout(boolean shouldSkip) { |
| mSkipRetryTimeout = shouldSkip; |
| } |
| |
| @Override |
| protected void enterState() throws Exception { |
| if (mIkeSession == null) { |
| logWtf("IKE session was already closed when entering Disconnecting state."); |
| sendMessageAndAcquireWakeLock(EVENT_SESSION_CLOSED, mCurrentToken); |
| return; |
| } |
| |
| // If underlying network has already been lost, save some time and just kill the session |
| if (mUnderlying == null) { |
| // Will trigger a EVENT_SESSION_CLOSED as IkeSession shuts down. |
| mIkeSession.kill(); |
| return; |
| } |
| |
| mIkeSession.close(); |
| |
| // Safe to blindly set up, as it is cancelled and cleared on exiting this state |
| setTeardownTimeoutAlarm(); |
| } |
| |
| @Override |
| protected void processStateMsg(Message msg) { |
| switch (msg.what) { |
| case EVENT_UNDERLYING_NETWORK_CHANGED: // Fallthrough |
| mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; |
| |
| // If we received a new underlying network, continue. |
| if (mUnderlying != null) { |
| break; |
| } |
| |
| // Fallthrough; no network exists to send IKE close session requests. |
| case EVENT_TEARDOWN_TIMEOUT_EXPIRED: |
| // Grace period ended. Kill session, triggering EVENT_SESSION_CLOSED |
| mIkeSession.kill(); |
| |
| break; |
| case EVENT_DISCONNECT_REQUESTED: |
| EventDisconnectRequestedInfo info = ((EventDisconnectRequestedInfo) msg.obj); |
| if (info.shouldQuit) { |
| mIsQuitting.setTrue(); |
| } |
| |
| teardownNetwork(); |
| |
| if (info.reason.equals(DISCONNECT_REASON_UNDERLYING_NETWORK_LOST)) { |
| // TODO(b/180526152): notify VcnStatusCallback for Network loss |
| |
| // Will trigger EVENT_SESSION_CLOSED immediately. |
| mIkeSession.kill(); |
| break; |
| } |
| |
| // Otherwise we are already in the process of shutting down. |
| break; |
| case EVENT_SESSION_CLOSED: |
| mIkeSession = null; |
| |
| if (!mIsQuitting.getValue() && mUnderlying != null) { |
| transitionTo(mSkipRetryTimeout ? mConnectingState : mRetryTimeoutState); |
| } else { |
| teardownNetwork(); |
| transitionTo(mDisconnectedState); |
| } |
| break; |
| case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: |
| handleSafeModeTimeoutExceeded(); |
| break; |
| default: |
| logUnhandledMessage(msg); |
| break; |
| } |
| } |
| |
| @Override |
| protected void exitState() throws Exception { |
| mSkipRetryTimeout = false; |
| |
| cancelTeardownTimeoutAlarm(); |
| } |
| } |
| |
| /** |
| * Transitive state representing a VCN that is making an primary (non-handover) connection. |
| * |
| * <p>This state starts IKE negotiation, but defers transform application & network setup to the |
| * Connected state. |
| */ |
| private class ConnectingState extends ActiveBaseState { |
| @Override |
| protected void enterState() { |
| if (mIkeSession != null) { |
| logWtf("ConnectingState entered with active session"); |
| |
| // Attempt to recover. |
| mIkeSession.kill(); |
| mIkeSession = null; |
| } |
| |
| mIkeSession = buildIkeSession(mUnderlying.network); |
| } |
| |
| @Override |
| protected void processStateMsg(Message msg) { |
| switch (msg.what) { |
| case EVENT_UNDERLYING_NETWORK_CHANGED: |
| final UnderlyingNetworkRecord oldUnderlying = mUnderlying; |
| mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; |
| |
| if (oldUnderlying == null) { |
| // This should never happen, but if it does, there's likely a nasty bug. |
| logWtf("Old underlying network was null in connected state. Bug?"); |
| } |
| |
| // If new underlying is null, all underlying networks have been lost; disconnect |
| if (mUnderlying == null) { |
| transitionTo(mDisconnectingState); |
| break; |
| } |
| |
| if (oldUnderlying != null |
| && mUnderlying.network.equals(oldUnderlying.network)) { |
| break; // Only network properties have changed; continue connecting. |
| } |
| // Else, retry on the new network. |
| |
| // Immediately come back to the ConnectingState (skip RetryTimeout, since this |
| // isn't a failure) |
| mDisconnectingState.setSkipRetryTimeout(true); |
| |
| // fallthrough - disconnect, and retry on new network. |
| case EVENT_SESSION_LOST: |
| transitionTo(mDisconnectingState); |
| break; |
| case EVENT_SESSION_CLOSED: |
| // Disconnecting state waits for EVENT_SESSION_CLOSED to shutdown, and this |
| // message may not be posted again. Defer to ensure immediate shutdown. |
| deferMessage(msg); |
| |
| transitionTo(mDisconnectingState); |
| break; |
| case EVENT_SETUP_COMPLETED: // fallthrough |
| case EVENT_TRANSFORM_CREATED: |
| // Child setup complete; move to ConnectedState for NetworkAgent registration |
| deferMessage(msg); |
| transitionTo(mConnectedState); |
| break; |
| case EVENT_DISCONNECT_REQUESTED: |
| handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj); |
| break; |
| case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: |
| handleSafeModeTimeoutExceeded(); |
| break; |
| default: |
| logUnhandledMessage(msg); |
| break; |
| } |
| } |
| } |
| |
| private abstract class ConnectedStateBase extends ActiveBaseState { |
| protected void updateNetworkAgent( |
| @NonNull IpSecTunnelInterface tunnelIface, |
| @NonNull VcnNetworkAgent agent, |
| @NonNull VcnChildSessionConfiguration childConfig) { |
| final NetworkCapabilities caps = |
| buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled); |
| final LinkProperties lp = |
| buildConnectedLinkProperties( |
| mConnectionConfig, tunnelIface, childConfig, mUnderlying); |
| |
| agent.sendNetworkCapabilities(caps); |
| agent.sendLinkProperties(lp); |
| |
| agent.setUnderlyingNetworks( |
| mUnderlying == null ? null : Collections.singletonList(mUnderlying.network)); |
| } |
| |
| protected VcnNetworkAgent buildNetworkAgent( |
| @NonNull IpSecTunnelInterface tunnelIface, |
| @NonNull VcnChildSessionConfiguration childConfig) { |
| final NetworkCapabilities caps = |
| buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled); |
| final LinkProperties lp = |
| buildConnectedLinkProperties( |
| mConnectionConfig, tunnelIface, childConfig, mUnderlying); |
| final NetworkAgentConfig nac = |
| new NetworkAgentConfig.Builder() |
| .setLegacyType(ConnectivityManager.TYPE_MOBILE) |
| .setLegacyTypeName(NETWORK_INFO_NETWORK_TYPE_STRING) |
| .setLegacySubType(TelephonyManager.NETWORK_TYPE_UNKNOWN) |
| .setLegacySubTypeName( |
| TelephonyManager.getNetworkTypeName( |
| TelephonyManager.NETWORK_TYPE_UNKNOWN)) |
| .setLegacyExtraInfo(NETWORK_INFO_EXTRA_INFO) |
| .build(); |
| |
| final VcnNetworkAgent agent = |
| mDeps.newNetworkAgent( |
| mVcnContext, |
| TAG, |
| caps, |
| lp, |
| Vcn.getNetworkScore(), |
| nac, |
| mVcnContext.getVcnNetworkProvider(), |
| (agentRef) -> { |
| // Only trigger teardown if the NetworkAgent hasn't been replaced or |
| // changed. This guards against two cases - the first where |
| // unwanted() may be called as a result of the |
| // NetworkAgent.unregister() call, which might trigger a teardown |
| // instead of just a Network disconnect, as well as the case where a |
| // new NetworkAgent replaces an old one before the unwanted() call |
| // is processed. |
| if (mNetworkAgent != agentRef) { |
| logDbg("unwanted() called on stale NetworkAgent"); |
| return; |
| } |
| |
| logDbg("NetworkAgent was unwanted"); |
| teardownAsynchronously(); |
| } /* networkUnwantedCallback */, |
| (status) -> { |
| if (mIsQuitting.getValue()) { |
| return; // Ignore; VcnGatewayConnection quitting or already quit |
| } |
| |
| switch (status) { |
| case NetworkAgent.VALIDATION_STATUS_VALID: |
| clearFailedAttemptCounterAndSafeModeAlarm(); |
| break; |
| case NetworkAgent.VALIDATION_STATUS_NOT_VALID: |
| // Will only set a new alarm if no safe mode alarm is |
| // currently scheduled. |
| setSafeModeAlarm(); |
| break; |
| default: |
| logWtf( |
| "Unknown validation status " |
| + status |
| + "; ignoring"); |
| break; |
| } |
| } /* validationStatusCallback */); |
| |
| agent.register(); |
| agent.setUnderlyingNetworks( |
| mUnderlying == null ? null : Collections.singletonList(mUnderlying.network)); |
| agent.markConnected(); |
| |
| return agent; |
| } |
| |
| protected void clearFailedAttemptCounterAndSafeModeAlarm() { |
| mVcnContext.ensureRunningOnLooperThread(); |
| |
| // Validated connection, clear failed attempt counter |
| mFailedAttempts = 0; |
| cancelSafeModeAlarm(); |
| |
| mIsInSafeMode = false; |
| mGatewayStatusCallback.onSafeModeStatusChanged(); |
| } |
| |
| protected void applyTransform( |
| int token, |
| @NonNull IpSecTunnelInterface tunnelIface, |
| @NonNull Network underlyingNetwork, |
| @NonNull IpSecTransform transform, |
| int direction) { |
| if (direction != IpSecManager.DIRECTION_IN && direction != IpSecManager.DIRECTION_OUT) { |
| logWtf("Applying transform for unexpected direction: " + direction); |
| } |
| |
| try { |
| tunnelIface.setUnderlyingNetwork(underlyingNetwork); |
| |
| // Transforms do not need to be persisted; the IkeSession will keep them alive |
| mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform); |
| |
| // For inbound transforms, additionally allow forwarded traffic to bridge to DUN (as |
| // needed) |
| final Set<Integer> exposedCaps = mConnectionConfig.getAllExposedCapabilities(); |
| if (direction == IpSecManager.DIRECTION_IN |
| && exposedCaps.contains(NET_CAPABILITY_DUN)) { |
| mIpSecManager.applyTunnelModeTransform( |
| tunnelIface, IpSecManager.DIRECTION_FWD, transform); |
| } |
| } catch (IOException e) { |
| logDbg("Transform application failed for network " + token, e); |
| sessionLost(token, e); |
| } |
| } |
| |
| protected void setupInterface( |
| int token, |
| @NonNull IpSecTunnelInterface tunnelIface, |
| @NonNull VcnChildSessionConfiguration childConfig, |
| @Nullable VcnChildSessionConfiguration oldChildConfig) { |
| try { |
| final Set<LinkAddress> newAddrs = |
| new ArraySet<>(childConfig.getInternalAddresses()); |
| final Set<LinkAddress> existingAddrs = new ArraySet<>(); |
| if (oldChildConfig != null) { |
| existingAddrs.addAll(oldChildConfig.getInternalAddresses()); |
| } |
| |
| final Set<LinkAddress> toAdd = new ArraySet<>(); |
| toAdd.addAll(newAddrs); |
| toAdd.removeAll(existingAddrs); |
| |
| final Set<LinkAddress> toRemove = new ArraySet<>(); |
| toRemove.addAll(existingAddrs); |
| toRemove.removeAll(newAddrs); |
| |
| for (LinkAddress address : toAdd) { |
| tunnelIface.addAddress(address.getAddress(), address.getPrefixLength()); |
| } |
| |
| for (LinkAddress address : toRemove) { |
| tunnelIface.removeAddress(address.getAddress(), address.getPrefixLength()); |
| } |
| } catch (IOException e) { |
| logDbg("Adding address to tunnel failed for token " + token, e); |
| sessionLost(token, e); |
| } |
| } |
| } |
| |
| /** |
| * Stable state representing a VCN that has a functioning connection to the mobility anchor. |
| * |
| * <p>This state handles IPsec transform application (initial and rekey), NetworkAgent setup, |
| * and monitors for mobility events. |
| */ |
| class ConnectedState extends ConnectedStateBase { |
| @Override |
| protected void enterState() throws Exception { |
| if (mTunnelIface == null) { |
| try { |
| // Requires a real Network object in order to be created; doing this any earlier |
| // means not having a real Network object, or picking an incorrect Network. |
| mTunnelIface = |
| mIpSecManager.createIpSecTunnelInterface( |
| DUMMY_ADDR, DUMMY_ADDR, mUnderlying.network); |
| } catch (IOException | ResourceUnavailableException e) { |
| teardownAsynchronously(); |
| } |
| } |
| } |
| |
| @Override |
| protected void processStateMsg(Message msg) { |
| switch (msg.what) { |
| case EVENT_UNDERLYING_NETWORK_CHANGED: |
| handleUnderlyingNetworkChanged(msg); |
| break; |
| case EVENT_SESSION_CLOSED: |
| // Disconnecting state waits for EVENT_SESSION_CLOSED to shutdown, and this |
| // message may not be posted again. Defer to ensure immediate shutdown. |
| deferMessage(msg); |
| transitionTo(mDisconnectingState); |
| break; |
| case EVENT_SESSION_LOST: |
| transitionTo(mDisconnectingState); |
| break; |
| case EVENT_TRANSFORM_CREATED: |
| final EventTransformCreatedInfo transformCreatedInfo = |
| (EventTransformCreatedInfo) msg.obj; |
| |
| applyTransform( |
| mCurrentToken, |
| mTunnelIface, |
| mUnderlying.network, |
| transformCreatedInfo.transform, |
| transformCreatedInfo.direction); |
| break; |
| case EVENT_SETUP_COMPLETED: |
| final VcnChildSessionConfiguration oldChildConfig = mChildConfig; |
| mChildConfig = ((EventSetupCompletedInfo) msg.obj).childSessionConfig; |
| |
| setupInterfaceAndNetworkAgent( |
| mCurrentToken, mTunnelIface, mChildConfig, oldChildConfig); |
| break; |
| case EVENT_DISCONNECT_REQUESTED: |
| handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj); |
| break; |
| case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: |
| handleSafeModeTimeoutExceeded(); |
| break; |
| case EVENT_MIGRATION_COMPLETED: |
| final EventMigrationCompletedInfo migrationCompletedInfo = |
| (EventMigrationCompletedInfo) msg.obj; |
| |
| handleMigrationCompleted(migrationCompletedInfo); |
| break; |
| default: |
| logUnhandledMessage(msg); |
| break; |
| } |
| } |
| |
| private void handleMigrationCompleted(EventMigrationCompletedInfo migrationCompletedInfo) { |
| logDbg("Migration completed: " + mUnderlying.network); |
| |
| applyTransform( |
| mCurrentToken, |
| mTunnelIface, |
| mUnderlying.network, |
| migrationCompletedInfo.inTransform, |
| IpSecManager.DIRECTION_IN); |
| |
| applyTransform( |
| mCurrentToken, |
| mTunnelIface, |
| mUnderlying.network, |
| migrationCompletedInfo.outTransform, |
| IpSecManager.DIRECTION_OUT); |
| |
| updateNetworkAgent(mTunnelIface, mNetworkAgent, mChildConfig); |
| } |
| |
| private void handleUnderlyingNetworkChanged(@NonNull Message msg) { |
| final UnderlyingNetworkRecord oldUnderlying = mUnderlying; |
| mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; |
| |
| if (mUnderlying == null) { |
| logDbg("Underlying network lost"); |
| |
| // Ignored for now; a new network may be coming up. If none does, the delayed |
| // NETWORK_LOST disconnect will be fired, and tear down the session + network. |
| return; |
| } |
| |
| // mUnderlying assumed non-null, given check above. |
| // If network changed, migrate. Otherwise, update any existing networkAgent. |
| if (oldUnderlying == null || !oldUnderlying.network.equals(mUnderlying.network)) { |
| logDbg("Migrating to new network: " + mUnderlying.network); |
| mIkeSession.setNetwork(mUnderlying.network); |
| } else { |
| // oldUnderlying is non-null & underlying network itself has not changed |
| // (only network properties were changed). |
| |
| // Network not yet set up, or child not yet connected. |
| if (mNetworkAgent != null && mChildConfig != null) { |
| // If only network properties changed and agent is active, update properties |
| updateNetworkAgent(mTunnelIface, mNetworkAgent, mChildConfig); |
| } |
| } |
| } |
| |
| protected void setupInterfaceAndNetworkAgent( |
| int token, |
| @NonNull IpSecTunnelInterface tunnelIface, |
| @NonNull VcnChildSessionConfiguration childConfig, |
| @NonNull VcnChildSessionConfiguration oldChildConfig) { |
| setupInterface(token, tunnelIface, childConfig, oldChildConfig); |
| |
| if (mNetworkAgent == null) { |
| mNetworkAgent = buildNetworkAgent(tunnelIface, childConfig); |
| } else { |
| updateNetworkAgent(tunnelIface, mNetworkAgent, childConfig); |
| |
| // mNetworkAgent not null, so the VCN Network has already been established. Clear |
| // the failed attempt counter and safe mode alarm since this transition is complete. |
| clearFailedAttemptCounterAndSafeModeAlarm(); |
| } |
| } |
| |
| @Override |
| protected void exitState() { |
| // Will only set a new alarm if no safe mode alarm is currently scheduled. |
| setSafeModeAlarm(); |
| } |
| } |
| |
| /** |
| * Transitive state representing a VCN that failed to establish a connection, and will retry. |
| * |
| * <p>This state will be exited upon a new underlying network being found, or timeout expiry. |
| */ |
| class RetryTimeoutState extends ActiveBaseState { |
| @Override |
| protected void enterState() throws Exception { |
| // Reset upon entry to ConnectedState |
| mFailedAttempts++; |
| |
| if (mUnderlying == null) { |
| logWtf("Underlying network was null in retry state"); |
| teardownNetwork(); |
| transitionTo(mDisconnectedState); |
| } else { |
| // Safe to blindly set up, as it is cancelled and cleared on exiting this state |
| setRetryTimeoutAlarm(getNextRetryIntervalsMs()); |
| } |
| } |
| |
| @Override |
| protected void processStateMsg(Message msg) { |
| switch (msg.what) { |
| case EVENT_UNDERLYING_NETWORK_CHANGED: |
| final UnderlyingNetworkRecord oldUnderlying = mUnderlying; |
| mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; |
| |
| // If new underlying is null, all networks were lost; go back to disconnected. |
| if (mUnderlying == null) { |
| teardownNetwork(); |
| transitionTo(mDisconnectedState); |
| return; |
| } else if (oldUnderlying != null |
| && mUnderlying.network.equals(oldUnderlying.network)) { |
| // If the network has not changed, do nothing. |
| return; |
| } |
| |
| // Fallthrough |
| case EVENT_RETRY_TIMEOUT_EXPIRED: |
| transitionTo(mConnectingState); |
| break; |
| case EVENT_DISCONNECT_REQUESTED: |
| handleDisconnectRequested((EventDisconnectRequestedInfo) msg.obj); |
| break; |
| case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: |
| handleSafeModeTimeoutExceeded(); |
| break; |
| default: |
| logUnhandledMessage(msg); |
| break; |
| } |
| } |
| |
| @Override |
| public void exitState() { |
| cancelRetryTimeoutAlarm(); |
| } |
| |
| private long getNextRetryIntervalsMs() { |
| final int retryDelayIndex = mFailedAttempts - 1; |
| final long[] retryIntervalsMs = mConnectionConfig.getRetryIntervalsMillis(); |
| |
| // Repeatedly use last item in retry timeout list. |
| if (retryDelayIndex >= retryIntervalsMs.length) { |
| return retryIntervalsMs[retryIntervalsMs.length - 1]; |
| } |
| |
| return retryIntervalsMs[retryDelayIndex]; |
| } |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| static NetworkCapabilities buildNetworkCapabilities( |
| @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig, |
| @Nullable UnderlyingNetworkRecord underlying, |
| boolean isMobileDataEnabled) { |
| final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(); |
| |
| builder.addTransportType(TRANSPORT_CELLULAR); |
| builder.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); |
| builder.addCapability(NET_CAPABILITY_NOT_CONGESTED); |
| builder.addCapability(NET_CAPABILITY_NOT_SUSPENDED); |
| |
| // Add exposed capabilities |
| for (int cap : gatewayConnectionConfig.getAllExposedCapabilities()) { |
| // Skip adding INTERNET or DUN if mobile data is disabled. |
| if (!isMobileDataEnabled |
| && (cap == NET_CAPABILITY_INTERNET || cap == NET_CAPABILITY_DUN)) { |
| continue; |
| } |
| |
| builder.addCapability(cap); |
| } |
| |
| if (underlying != null) { |
| final NetworkCapabilities underlyingCaps = underlying.networkCapabilities; |
| |
| // Mirror merged capabilities. |
| for (int cap : MERGED_CAPABILITIES) { |
| if (underlyingCaps.hasCapability(cap)) { |
| builder.addCapability(cap); |
| } |
| } |
| |
| // Set admin UIDs for ConnectivityDiagnostics use. |
| final int[] underlyingAdminUids = underlyingCaps.getAdministratorUids(); |
| Arrays.sort(underlyingAdminUids); // Sort to allow contains check below. |
| |
| int[] adminUids; |
| if (underlyingCaps.getOwnerUid() > 0 // No owner UID specified |
| && 0 > Arrays.binarySearch(// Owner UID not found in admin UID list. |
| underlyingAdminUids, underlyingCaps.getOwnerUid())) { |
| adminUids = Arrays.copyOf(underlyingAdminUids, underlyingAdminUids.length + 1); |
| adminUids[adminUids.length - 1] = underlyingCaps.getOwnerUid(); |
| Arrays.sort(adminUids); |
| } else { |
| adminUids = underlyingAdminUids; |
| } |
| |
| // Set owner & administrator UID |
| builder.setOwnerUid(Process.myUid()); |
| adminUids = Arrays.copyOf(adminUids, adminUids.length + 1); |
| adminUids[adminUids.length - 1] = Process.myUid(); |
| builder.setAdministratorUids(adminUids); |
| |
| builder.setLinkUpstreamBandwidthKbps(underlyingCaps.getLinkUpstreamBandwidthKbps()); |
| builder.setLinkDownstreamBandwidthKbps(underlyingCaps.getLinkDownstreamBandwidthKbps()); |
| |
| // Set TransportInfo for SysUI use (never parcelled out of SystemServer). |
| if (underlyingCaps.hasTransport(TRANSPORT_WIFI) |
| && underlyingCaps.getTransportInfo() instanceof WifiInfo) { |
| final WifiInfo wifiInfo = (WifiInfo) underlyingCaps.getTransportInfo(); |
| builder.setTransportInfo(new VcnTransportInfo(wifiInfo)); |
| } else if (underlyingCaps.hasTransport(TRANSPORT_CELLULAR) |
| && underlyingCaps.getNetworkSpecifier() instanceof TelephonyNetworkSpecifier) { |
| final TelephonyNetworkSpecifier telNetSpecifier = |
| (TelephonyNetworkSpecifier) underlyingCaps.getNetworkSpecifier(); |
| builder.setTransportInfo(new VcnTransportInfo(telNetSpecifier.getSubscriptionId())); |
| } else { |
| Slog.wtf( |
| TAG, |
| "Unknown transport type or missing TransportInfo/NetworkSpecifier for" |
| + " non-null underlying network"); |
| } |
| } else { |
| Slog.wtf( |
| TAG, |
| "No underlying network while building network capabilities", |
| new IllegalStateException()); |
| } |
| |
| return builder.build(); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| LinkProperties buildConnectedLinkProperties( |
| @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig, |
| @NonNull IpSecTunnelInterface tunnelIface, |
| @NonNull VcnChildSessionConfiguration childConfig, |
| @Nullable UnderlyingNetworkRecord underlying) { |
| final IkeTunnelConnectionParams ikeTunnelParams = |
| gatewayConnectionConfig.getTunnelConnectionParams(); |
| 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 /*gateway*/, |
| null /*iface*/, RouteInfo.RTN_UNICAST)); |
| lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/, |
| null /*iface*/, RouteInfo.RTN_UNICAST)); |
| |
| int underlyingMtu = 0; |
| if (underlying != null) { |
| final LinkProperties underlyingLp = underlying.linkProperties; |
| |
| lp.setTcpBufferSizes(underlyingLp.getTcpBufferSizes()); |
| underlyingMtu = underlyingLp.getMtu(); |
| |
| // WiFi LinkProperties uses DHCP as the sole source of MTU information, and as a result |
| // often lists MTU as 0 (see b/184678973). Use the interface MTU as retrieved by |
| // NetworkInterface APIs. |
| if (underlyingMtu == 0 && underlyingLp.getInterfaceName() != null) { |
| underlyingMtu = mDeps.getUnderlyingIfaceMtu(underlyingLp.getInterfaceName()); |
| } |
| } else { |
| Slog.wtf( |
| TAG, |
| "No underlying network while building link properties", |
| new IllegalStateException()); |
| } |
| lp.setMtu( |
| MtuUtils.getMtu( |
| ikeTunnelParams.getTunnelModeChildSessionParams().getSaProposals(), |
| gatewayConnectionConfig.getMaxMtu(), |
| underlyingMtu)); |
| |
| return lp; |
| } |
| |
| private class IkeSessionCallbackImpl implements IkeSessionCallback { |
| private final int mToken; |
| |
| IkeSessionCallbackImpl(int token) { |
| mToken = token; |
| } |
| |
| @Override |
| public void onOpened(@NonNull IkeSessionConfiguration ikeSessionConfig) { |
| logDbg("IkeOpened for token " + mToken); |
| // Nothing to do here. |
| } |
| |
| @Override |
| public void onClosed() { |
| logDbg("IkeClosed for token " + mToken); |
| sessionClosed(mToken, null); |
| } |
| |
| @Override |
| public void onClosedExceptionally(@NonNull IkeException exception) { |
| logDbg("IkeClosedExceptionally for token " + mToken, exception); |
| sessionClosed(mToken, exception); |
| } |
| |
| @Override |
| public void onError(@NonNull IkeProtocolException exception) { |
| logDbg("IkeError for token " + mToken, exception); |
| // Non-fatal, log and continue. |
| } |
| } |
| |
| /** Implementation of ChildSessionCallback, exposed for testing. */ |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| public class VcnChildSessionCallback implements ChildSessionCallback { |
| private final int mToken; |
| |
| VcnChildSessionCallback(int token) { |
| mToken = token; |
| } |
| |
| /** Internal proxy method for injecting of mocked ChildSessionConfiguration */ |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void onOpened(@NonNull VcnChildSessionConfiguration childConfig) { |
| logDbg("ChildOpened for token " + mToken); |
| childOpened(mToken, childConfig); |
| } |
| |
| @Override |
| public void onOpened(@NonNull ChildSessionConfiguration childConfig) { |
| onOpened(new VcnChildSessionConfiguration(childConfig)); |
| } |
| |
| @Override |
| public void onClosed() { |
| logDbg("ChildClosed for token " + mToken); |
| sessionLost(mToken, null); |
| } |
| |
| @Override |
| public void onClosedExceptionally(@NonNull IkeException exception) { |
| logDbg("ChildClosedExceptionally for token " + mToken, exception); |
| sessionLost(mToken, exception); |
| } |
| |
| @Override |
| public void onIpSecTransformCreated(@NonNull IpSecTransform transform, int direction) { |
| logDbg("ChildTransformCreated; Direction: " + direction + "; token " + mToken); |
| childTransformCreated(mToken, transform, direction); |
| } |
| |
| @Override |
| public void onIpSecTransformsMigrated( |
| @NonNull IpSecTransform inIpSecTransform, |
| @NonNull IpSecTransform outIpSecTransform) { |
| logDbg("ChildTransformsMigrated; token " + mToken); |
| migrationCompleted(mToken, inIpSecTransform, outIpSecTransform); |
| } |
| |
| @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. |
| logDbg("ChildTransformDeleted; Direction: " + direction + "; for token " + mToken); |
| } |
| } |
| |
| private String getLogPrefix() { |
| return "[" |
| + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup) |
| + "-" |
| + mConnectionConfig.getGatewayConnectionName() |
| + "-" |
| + System.identityHashCode(this) |
| + "] "; |
| } |
| |
| private void logVdbg(String msg) { |
| if (VDBG) { |
| Slog.v(TAG, getLogPrefix() + msg); |
| } |
| } |
| |
| private void logDbg(String msg) { |
| Slog.d(TAG, getLogPrefix() + msg); |
| } |
| |
| private void logDbg(String msg, Throwable tr) { |
| Slog.d(TAG, getLogPrefix() + msg, tr); |
| } |
| |
| private void logWarn(String msg) { |
| Slog.w(TAG, getLogPrefix() + msg); |
| LOCAL_LOG.log(getLogPrefix() + "WARN: " + msg); |
| } |
| |
| private void logWarn(String msg, Throwable tr) { |
| Slog.w(TAG, getLogPrefix() + msg, tr); |
| LOCAL_LOG.log(getLogPrefix() + "WARN: " + msg + tr); |
| } |
| |
| private void logErr(String msg) { |
| Slog.e(TAG, getLogPrefix() + msg); |
| LOCAL_LOG.log(getLogPrefix() + "ERR: " + msg); |
| } |
| |
| private void logErr(String msg, Throwable tr) { |
| Slog.e(TAG, getLogPrefix() + msg, tr); |
| LOCAL_LOG.log(getLogPrefix() + "ERR: " + msg + tr); |
| } |
| |
| private void logWtf(String msg) { |
| Slog.wtf(TAG, getLogPrefix() + msg); |
| LOCAL_LOG.log(getLogPrefix() + "WTF: " + msg); |
| } |
| |
| private void logWtf(String msg, Throwable tr) { |
| Slog.wtf(TAG, getLogPrefix() + msg, tr); |
| LOCAL_LOG.log(getLogPrefix() + "WTF: " + msg + tr); |
| } |
| |
| /** |
| * Dumps the state of this VcnGatewayConnection for logging and debugging purposes. |
| * |
| * <p>PII and credentials MUST NEVER be dumped here. |
| */ |
| public void dump(IndentingPrintWriter pw) { |
| pw.println("VcnGatewayConnection (" + mConnectionConfig.getGatewayConnectionName() + "):"); |
| pw.increaseIndent(); |
| |
| pw.println( |
| "Current state: " |
| + (getCurrentState() == null |
| ? null |
| : getCurrentState().getClass().getSimpleName())); |
| pw.println("mIsQuitting: " + mIsQuitting.getValue()); |
| pw.println("mIsInSafeMode: " + mIsInSafeMode); |
| pw.println("mCurrentToken: " + mCurrentToken); |
| pw.println("mFailedAttempts: " + mFailedAttempts); |
| pw.println( |
| "mNetworkAgent.getNetwork(): " |
| + (mNetworkAgent == null ? null : mNetworkAgent.getNetwork())); |
| pw.println(); |
| |
| mUnderlyingNetworkTracker.dump(pw); |
| pw.println(); |
| |
| pw.decreaseIndent(); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void setTunnelInterface(IpSecTunnelInterface tunnelIface) { |
| mTunnelIface = tunnelIface; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| UnderlyingNetworkTrackerCallback getUnderlyingNetworkTrackerCallback() { |
| return mUnderlyingNetworkTrackerCallback; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| UnderlyingNetworkRecord getUnderlyingNetwork() { |
| return mUnderlying; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void setUnderlyingNetwork(@Nullable UnderlyingNetworkRecord record) { |
| mUnderlying = record; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| boolean isQuitting() { |
| return mIsQuitting.getValue(); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void setQuitting() { |
| mIsQuitting.setTrue(); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| VcnIkeSession getIkeSession() { |
| return mIkeSession; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void setIkeSession(@Nullable VcnIkeSession session) { |
| mIkeSession = session; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| VcnNetworkAgent getNetworkAgent() { |
| return mNetworkAgent; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void setNetworkAgent(@Nullable VcnNetworkAgent networkAgent) { |
| mNetworkAgent = networkAgent; |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| void sendDisconnectRequestedAndAcquireWakelock(String reason, boolean shouldQuit) { |
| sendMessageAndAcquireWakeLock( |
| EVENT_DISCONNECT_REQUESTED, |
| TOKEN_ALL, |
| new EventDisconnectRequestedInfo(reason, shouldQuit)); |
| } |
| |
| private IkeSessionParams buildIkeParams(@NonNull Network network) { |
| final IkeTunnelConnectionParams ikeTunnelConnectionParams = |
| mConnectionConfig.getTunnelConnectionParams(); |
| final IkeSessionParams.Builder builder = |
| new IkeSessionParams.Builder(ikeTunnelConnectionParams.getIkeSessionParams()); |
| builder.setNetwork(network); |
| return builder.build(); |
| } |
| |
| private ChildSessionParams buildChildParams() { |
| return mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams(); |
| } |
| |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| VcnIkeSession buildIkeSession(@NonNull Network network) { |
| final int token = ++mCurrentToken; |
| |
| return mDeps.newIkeSession( |
| mVcnContext, |
| buildIkeParams(network), |
| buildChildParams(), |
| new IkeSessionCallbackImpl(token), |
| new VcnChildSessionCallback(token)); |
| } |
| |
| /** 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, |
| TelephonySubscriptionSnapshot snapshot, |
| UnderlyingNetworkTrackerCallback callback) { |
| return new UnderlyingNetworkTracker( |
| vcnContext, |
| subscriptionGroup, |
| snapshot, |
| callback); |
| } |
| |
| /** Builds a new IkeSession. */ |
| public VcnIkeSession newIkeSession( |
| VcnContext vcnContext, |
| IkeSessionParams ikeSessionParams, |
| ChildSessionParams childSessionParams, |
| IkeSessionCallback ikeSessionCallback, |
| ChildSessionCallback childSessionCallback) { |
| return new VcnIkeSession( |
| vcnContext, |
| ikeSessionParams, |
| childSessionParams, |
| ikeSessionCallback, |
| childSessionCallback); |
| } |
| |
| /** Builds a new WakeLock. */ |
| public VcnWakeLock newWakeLock( |
| @NonNull Context context, int wakeLockFlag, @NonNull String wakeLockTag) { |
| return new VcnWakeLock(context, wakeLockFlag, wakeLockTag); |
| } |
| |
| /** Builds a new WakeupMessage. */ |
| public WakeupMessage newWakeupMessage( |
| @NonNull VcnContext vcnContext, |
| @NonNull Handler handler, |
| @NonNull String tag, |
| @NonNull Runnable runnable) { |
| return new WakeupMessage(vcnContext.getContext(), handler, tag, runnable); |
| } |
| |
| /** Builds a new VcnNetworkAgent. */ |
| public VcnNetworkAgent newNetworkAgent( |
| @NonNull VcnContext vcnContext, |
| @NonNull String tag, |
| @NonNull NetworkCapabilities caps, |
| @NonNull LinkProperties lp, |
| @NonNull NetworkScore score, |
| @NonNull NetworkAgentConfig nac, |
| @NonNull NetworkProvider provider, |
| @NonNull Consumer<VcnNetworkAgent> networkUnwantedCallback, |
| @NonNull Consumer<Integer> validationStatusCallback) { |
| return new VcnNetworkAgent( |
| vcnContext, |
| tag, |
| caps, |
| lp, |
| score, |
| nac, |
| provider, |
| networkUnwantedCallback, |
| validationStatusCallback); |
| } |
| |
| /** Checks if airplane mode is enabled. */ |
| public boolean isAirplaneModeOn(@NonNull VcnContext vcnContext) { |
| return Settings.Global.getInt(vcnContext.getContext().getContentResolver(), |
| Settings.Global.AIRPLANE_MODE_ON, 0) != 0; |
| } |
| |
| /** Gets the elapsed real time since boot, in millis. */ |
| public long getElapsedRealTime() { |
| return SystemClock.elapsedRealtime(); |
| } |
| |
| /** Gets the MTU for the given underlying interface. */ |
| public int getUnderlyingIfaceMtu(String ifaceName) { |
| try { |
| final NetworkInterface underlyingIface = NetworkInterface.getByName(ifaceName); |
| return underlyingIface == null ? 0 : underlyingIface.getMTU(); |
| } catch (IOException e) { |
| Slog.d(TAG, "Could not get MTU of underlying network", e); |
| return 0; |
| } |
| } |
| } |
| |
| /** |
| * Proxy implementation of Child Session Configuration, used for testing. |
| * |
| * <p>This wrapper allows mocking of the final, parcelable ChildSessionConfiguration object for |
| * testing purposes. This is the unfortunate result of mockito-inline (for mocking final |
| * classes) not working properly with system services & associated classes. |
| * |
| * <p>This class MUST EXCLUSIVELY be a passthrough, proxying calls directly to the actual |
| * ChildSessionConfiguration. |
| */ |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| public static class VcnChildSessionConfiguration { |
| private final ChildSessionConfiguration mChildConfig; |
| |
| public VcnChildSessionConfiguration(ChildSessionConfiguration childConfig) { |
| mChildConfig = childConfig; |
| } |
| |
| /** Retrieves the addresses to be used inside the tunnel. */ |
| public List<LinkAddress> getInternalAddresses() { |
| return mChildConfig.getInternalAddresses(); |
| } |
| |
| /** Retrieves the DNS servers to be used inside the tunnel. */ |
| public List<InetAddress> getInternalDnsServers() { |
| return mChildConfig.getInternalDnsServers(); |
| } |
| } |
| |
| /** Proxy implementation of IKE session, used for testing. */ |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| public static class VcnIkeSession { |
| private final IkeSession mImpl; |
| |
| public VcnIkeSession( |
| VcnContext vcnContext, |
| IkeSessionParams ikeSessionParams, |
| ChildSessionParams childSessionParams, |
| IkeSessionCallback ikeSessionCallback, |
| ChildSessionCallback childSessionCallback) { |
| mImpl = |
| new IkeSession( |
| vcnContext.getContext(), |
| ikeSessionParams, |
| childSessionParams, |
| new HandlerExecutor(new Handler(vcnContext.getLooper())), |
| ikeSessionCallback, |
| childSessionCallback); |
| } |
| |
| /** Creates a new IKE Child session. */ |
| public void openChildSession( |
| @NonNull ChildSessionParams childSessionParams, |
| @NonNull ChildSessionCallback childSessionCallback) { |
| mImpl.openChildSession(childSessionParams, childSessionCallback); |
| } |
| |
| /** Closes an IKE session as identified by the ChildSessionCallback. */ |
| public void closeChildSession(@NonNull ChildSessionCallback childSessionCallback) { |
| mImpl.closeChildSession(childSessionCallback); |
| } |
| |
| /** Gracefully closes this IKE Session, waiting for remote acknowledgement. */ |
| public void close() { |
| mImpl.close(); |
| } |
| |
| /** Forcibly kills this IKE Session, without waiting for a closure confirmation. */ |
| public void kill() { |
| mImpl.kill(); |
| } |
| |
| /** Sets the underlying network used by the IkeSession. */ |
| public void setNetwork(@NonNull Network network) { |
| mImpl.setNetwork(network); |
| } |
| } |
| |
| /** Proxy Implementation of WakeLock, used for testing. */ |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| public static class VcnWakeLock { |
| private final WakeLock mImpl; |
| |
| public VcnWakeLock(@NonNull Context context, int flags, @NonNull String tag) { |
| final PowerManager powerManager = context.getSystemService(PowerManager.class); |
| mImpl = powerManager.newWakeLock(flags, tag); |
| mImpl.setReferenceCounted(false /* isReferenceCounted */); |
| } |
| |
| /** |
| * Acquire this WakeLock. |
| * |
| * <p>Synchronize this action to minimize locking around WakeLock use. |
| */ |
| public synchronized void acquire() { |
| mImpl.acquire(); |
| } |
| |
| /** |
| * Release this Wakelock. |
| * |
| * <p>Synchronize this action to minimize locking around WakeLock use. |
| */ |
| public synchronized void release() { |
| mImpl.release(); |
| } |
| } |
| |
| /** Proxy Implementation of NetworkAgent, used for testing. */ |
| @VisibleForTesting(visibility = Visibility.PRIVATE) |
| public static class VcnNetworkAgent { |
| private final NetworkAgent mImpl; |
| |
| public VcnNetworkAgent( |
| @NonNull VcnContext vcnContext, |
| @NonNull String tag, |
| @NonNull NetworkCapabilities caps, |
| @NonNull LinkProperties lp, |
| @NonNull NetworkScore score, |
| @NonNull NetworkAgentConfig nac, |
| @NonNull NetworkProvider provider, |
| @NonNull Consumer<VcnNetworkAgent> networkUnwantedCallback, |
| @NonNull Consumer<Integer> validationStatusCallback) { |
| mImpl = |
| new NetworkAgent( |
| vcnContext.getContext(), |
| vcnContext.getLooper(), |
| tag, |
| caps, |
| lp, |
| score, |
| nac, |
| provider) { |
| @Override |
| public void onNetworkUnwanted() { |
| networkUnwantedCallback.accept(VcnNetworkAgent.this); |
| } |
| |
| @Override |
| public void onValidationStatus(int status, @Nullable Uri redirectUri) { |
| validationStatusCallback.accept(status); |
| } |
| }; |
| } |
| |
| /** Registers the underlying NetworkAgent */ |
| public void register() { |
| mImpl.register(); |
| } |
| |
| /** Marks the underlying NetworkAgent as connected */ |
| public void markConnected() { |
| mImpl.markConnected(); |
| } |
| |
| /** Unregisters the underlying NetworkAgent */ |
| public void unregister() { |
| mImpl.unregister(); |
| } |
| |
| /** Sends new NetworkCapabilities for the underlying NetworkAgent */ |
| public void sendNetworkCapabilities(@NonNull NetworkCapabilities caps) { |
| mImpl.sendNetworkCapabilities(caps); |
| } |
| |
| /** Sends new LinkProperties for the underlying NetworkAgent */ |
| public void sendLinkProperties(@NonNull LinkProperties lp) { |
| mImpl.sendLinkProperties(lp); |
| } |
| |
| /** Sends new NetworkCapabilities for the underlying NetworkAgent */ |
| public void setUnderlyingNetworks(@Nullable List<Network> underlyingNetworks) { |
| mImpl.setUnderlyingNetworks(underlyingNetworks); |
| } |
| |
| /** Retrieves the Network for the underlying NetworkAgent */ |
| @Nullable |
| public Network getNetwork() { |
| return mImpl.getNetwork(); |
| } |
| } |
| } |