diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index f7ae58ca0eb41..fc1162e1b55f8 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -16,6 +16,7 @@ package com.android.server; +import static android.Manifest.permission.DUMP; import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; @@ -69,6 +70,7 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.internal.util.IndentingPrintWriter; import com.android.net.module.util.LocationPermissionChecker; import com.android.server.vcn.TelephonySubscriptionTracker; import com.android.server.vcn.Vcn; @@ -76,7 +78,9 @@ import com.android.server.vcn.VcnContext; import com.android.server.vcn.VcnNetworkProvider; import com.android.server.vcn.util.PersistableBundleUtils; +import java.io.FileDescriptor; import java.io.IOException; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -929,6 +933,33 @@ public class VcnManagementService extends IVcnManagementService.Stub { } } + /** + * Dumps the state of the VcnManagementService for logging and debugging purposes. + * + *

PII and credentials MUST NEVER be dumped here. + */ + @Override + protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { + mContext.enforceCallingOrSelfPermission(DUMP, TAG); + + final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); + + pw.println("VcnManagementService dump:"); + pw.increaseIndent(); + + mNetworkProvider.dump(pw); + + synchronized (mLock) { + pw.println("mVcns:"); + for (Vcn vcn : mVcns.values()) { + vcn.dump(pw); + } + pw.println(); + } + + pw.decreaseIndent(); + } + // TODO(b/180452282): Make name more generic and implement directly with VcnManagementService /** Callback for Vcn signals sent up to VcnManagementService. */ public interface VcnCallback { diff --git a/services/core/java/com/android/server/vcn/Vcn.java b/services/core/java/com/android/server/vcn/Vcn.java index ae806aa500a67..7bc6056f91f30 100644 --- a/services/core/java/com/android/server/vcn/Vcn.java +++ b/services/core/java/com/android/server/vcn/Vcn.java @@ -38,6 +38,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.internal.util.IndentingPrintWriter; import com.android.server.VcnManagementService.VcnCallback; import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; @@ -328,6 +329,8 @@ public class Vcn extends Handler { private void handleNetworkRequested( @NonNull NetworkRequest request, int score, int providerId) { + Slog.v(getLogTag(), "Received request " + request); + if (score > getNetworkScore()) { if (VDBG) { Slog.v( @@ -409,6 +412,26 @@ public class Vcn extends Handler { return TAG + " [" + mSubscriptionGroup.hashCode() + "]"; } + /** + * Dumps the state of this Vcn for logging and debugging purposes. + * + *

PII and credentials MUST NEVER be dumped here. + */ + public void dump(IndentingPrintWriter pw) { + pw.println("Vcn (" + mSubscriptionGroup + "):"); + pw.increaseIndent(); + + pw.println("mCurrentStatus: " + mCurrentStatus); + + pw.println("mVcnGatewayConnections:"); + for (VcnGatewayConnection gw : mVcnGatewayConnections.values()) { + gw.dump(pw); + } + pw.println(); + + pw.decreaseIndent(); + } + /** Retrieves the network score for a VCN Network */ // Package visibility for use in VcnGatewayConnection static int getNetworkScore() { diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 20c08eb2ce928..83ac36f9e0f65 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -77,6 +77,7 @@ 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; @@ -84,6 +85,7 @@ import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscription 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.MtuUtils; import java.io.IOException; import java.net.Inet4Address; @@ -448,6 +450,44 @@ public class VcnGatewayConnection extends StateMachine { */ private static final int EVENT_SAFE_MODE_TIMEOUT_EXCEEDED = 10; + /** + * Sent when an IKE has completed migration, and created updated transforms for application. + * + *

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(); @@ -574,7 +614,7 @@ public class VcnGatewayConnection extends StateMachine { *

Set in Connected state, always @NonNull in Connected, Migrating states, @Nullable * otherwise. */ - private NetworkAgent mNetworkAgent; + private VcnNetworkAgent mNetworkAgent; @Nullable private WakeupMessage mTeardownTimeoutAlarm; @Nullable private WakeupMessage mDisconnectRequestAlarm; @@ -1053,6 +1093,14 @@ public class VcnGatewayConnection extends StateMachine { 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( @@ -1148,7 +1196,9 @@ public class VcnGatewayConnection extends StateMachine { case EVENT_SETUP_COMPLETED: // Fallthrough case EVENT_DISCONNECT_REQUESTED: // Fallthrough case EVENT_TEARDOWN_TIMEOUT_EXPIRED: // Fallthrough - case EVENT_SUBSCRIPTIONS_CHANGED: + case EVENT_SUBSCRIPTIONS_CHANGED: // Fallthrough + case EVENT_SAFE_MODE_TIMEOUT_EXCEEDED: // Fallthrough + case EVENT_MIGRATION_COMPLETED: logUnexpectedEvent(msg.what); break; default: @@ -1440,30 +1490,32 @@ public class VcnGatewayConnection extends StateMachine { private abstract class ConnectedStateBase extends ActiveBaseState { protected void updateNetworkAgent( @NonNull IpSecTunnelInterface tunnelIface, - @NonNull NetworkAgent agent, + @NonNull VcnNetworkAgent agent, @NonNull VcnChildSessionConfiguration childConfig) { final NetworkCapabilities caps = buildNetworkCapabilities(mConnectionConfig, mUnderlying); final LinkProperties lp = - buildConnectedLinkProperties(mConnectionConfig, tunnelIface, childConfig); + buildConnectedLinkProperties( + mConnectionConfig, tunnelIface, childConfig, mUnderlying); agent.sendNetworkCapabilities(caps); agent.sendLinkProperties(lp); } - protected NetworkAgent buildNetworkAgent( + protected VcnNetworkAgent buildNetworkAgent( @NonNull IpSecTunnelInterface tunnelIface, @NonNull VcnChildSessionConfiguration childConfig) { final NetworkCapabilities caps = buildNetworkCapabilities(mConnectionConfig, mUnderlying); final LinkProperties lp = - buildConnectedLinkProperties(mConnectionConfig, tunnelIface, childConfig); + buildConnectedLinkProperties( + mConnectionConfig, tunnelIface, childConfig, mUnderlying); final NetworkAgentConfig nac = new NetworkAgentConfig.Builder() .setLegacyType(ConnectivityManager.TYPE_MOBILE) .build(); - final NetworkAgent agent = + final VcnNetworkAgent agent = mDeps.newNetworkAgent( mVcnContext, TAG, @@ -1472,15 +1524,21 @@ public class VcnGatewayConnection extends StateMachine { Vcn.getNetworkScore(), nac, mVcnContext.getVcnNetworkProvider(), - () -> { - Slog.d(TAG, "NetworkAgent was unwanted"); - // If network agent has already been torn down, skip sending the - // disconnect. Unwanted() is always called, even when networkAgents - // are unregistered in teardownNetwork(), so prevent duplicate - // notifications. - if (mNetworkAgent != null) { - teardownAsynchronously(); + (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) { + Slog.d(TAG, "unwanted() called on stale NetworkAgent"); + return; } + + Slog.d(TAG, "NetworkAgent was unwanted"); + teardownAsynchronously(); } /* networkUnwantedCallback */, (status) -> { if (status == NetworkAgent.VALIDATION_STATUS_VALID) { @@ -1620,12 +1678,36 @@ public class VcnGatewayConnection extends StateMachine { 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) { + 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; @@ -1815,7 +1897,10 @@ public class VcnGatewayConnection extends StateMachine { private static LinkProperties buildConnectedLinkProperties( @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig, @NonNull IpSecTunnelInterface tunnelIface, - @NonNull VcnChildSessionConfiguration childConfig) { + @NonNull VcnChildSessionConfiguration childConfig, + @Nullable UnderlyingNetworkRecord underlying) { + final VcnControlPlaneIkeConfig controlPlaneConfig = + (VcnControlPlaneIkeConfig) gatewayConnectionConfig.getControlPlaneConfig(); final LinkProperties lp = new LinkProperties(); lp.setInterfaceName(tunnelIface.getInterfaceName()); @@ -1831,7 +1916,12 @@ public class VcnGatewayConnection extends StateMachine { lp.addRoute(new RouteInfo(new IpPrefix(Inet6Address.ANY, 0), null /*gateway*/, null /*iface*/, RouteInfo.RTN_UNICAST)); - lp.setMtu(gatewayConnectionConfig.getMaxMtu()); + final int underlyingMtu = (underlying == null) ? 0 : underlying.linkProperties.getMtu(); + lp.setMtu( + MtuUtils.getMtu( + controlPlaneConfig.getChildSessionParams().getSaProposals(), + gatewayConnectionConfig.getMaxMtu(), + underlyingMtu)); return lp; } @@ -1912,8 +2002,7 @@ public class VcnGatewayConnection extends StateMachine { @NonNull IpSecTransform inIpSecTransform, @NonNull IpSecTransform outIpSecTransform) { Slog.v(TAG, "ChildTransformsMigrated; token " + mToken); - onIpSecTransformCreated(inIpSecTransform, IpSecManager.DIRECTION_IN); - onIpSecTransformCreated(outIpSecTransform, IpSecManager.DIRECTION_OUT); + migrationCompleted(mToken, inIpSecTransform, outIpSecTransform); } @Override @@ -1924,6 +2013,27 @@ public class VcnGatewayConnection extends StateMachine { } } + /** + * Dumps the state of this VcnGatewayConnection for logging and debugging purposes. + * + *

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().getClass().getSimpleName()); + pw.println("mIsQuitting: " + mIsQuitting); + pw.println("mIsInSafeMode: " + mIsInSafeMode); + pw.println("mCurrentToken: " + mCurrentToken); + pw.println("mFailedAttempts: " + mFailedAttempts); + pw.println( + "mNetworkAgent.getNetwork(): " + + (mNetworkAgent == null ? null : mNetworkAgent.getNetwork())); + + pw.decreaseIndent(); + } + @VisibleForTesting(visibility = Visibility.PRIVATE) void setTunnelInterface(IpSecTunnelInterface tunnelIface) { mTunnelIface = tunnelIface; @@ -1965,12 +2075,12 @@ public class VcnGatewayConnection extends StateMachine { } @VisibleForTesting(visibility = Visibility.PRIVATE) - NetworkAgent getNetworkAgent() { + VcnNetworkAgent getNetworkAgent() { return mNetworkAgent; } @VisibleForTesting(visibility = Visibility.PRIVATE) - void setNetworkAgent(@Nullable NetworkAgent networkAgent) { + void setNetworkAgent(@Nullable VcnNetworkAgent networkAgent) { mNetworkAgent = networkAgent; } @@ -2058,8 +2168,8 @@ public class VcnGatewayConnection extends StateMachine { return new WakeupMessage(vcnContext.getContext(), handler, tag, runnable); } - /** Builds a new NetworkAgent. */ - public NetworkAgent newNetworkAgent( + /** Builds a new VcnNetworkAgent. */ + public VcnNetworkAgent newNetworkAgent( @NonNull VcnContext vcnContext, @NonNull String tag, @NonNull NetworkCapabilities caps, @@ -2067,27 +2177,18 @@ public class VcnGatewayConnection extends StateMachine { @NonNull int score, @NonNull NetworkAgentConfig nac, @NonNull NetworkProvider provider, - @NonNull Runnable networkUnwantedCallback, + @NonNull Consumer networkUnwantedCallback, @NonNull Consumer validationStatusCallback) { - return new NetworkAgent( - vcnContext.getContext(), - vcnContext.getLooper(), + return new VcnNetworkAgent( + vcnContext, tag, caps, lp, score, nac, - provider) { - @Override - public void onNetworkUnwanted() { - networkUnwantedCallback.run(); - } - - @Override - public void onValidationStatus(int status, @Nullable Uri redirectUri) { - validationStatusCallback.accept(status); - } - }; + provider, + networkUnwantedCallback, + validationStatusCallback); } /** Gets the elapsed real time since boot, in millis. */ @@ -2203,4 +2304,73 @@ public class VcnGatewayConnection extends StateMachine { 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 int score, + @NonNull NetworkAgentConfig nac, + @NonNull NetworkProvider provider, + @NonNull Consumer networkUnwantedCallback, + @NonNull Consumer 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); + } + + /** Retrieves the Network for the underlying NetworkAgent */ + @Nullable + public Network getNetwork() { + return mImpl.getNetwork(); + } + } } diff --git a/services/core/java/com/android/server/vcn/VcnNetworkProvider.java b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java index a909695991593..be0deb57ee769 100644 --- a/services/core/java/com/android/server/vcn/VcnNetworkProvider.java +++ b/services/core/java/com/android/server/vcn/VcnNetworkProvider.java @@ -29,6 +29,7 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.internal.util.IndentingPrintWriter; import java.util.Objects; import java.util.Set; @@ -129,10 +130,50 @@ public class VcnNetworkProvider extends NetworkProvider { mScore = score; mProviderId = providerId; } + + /** + * Dumps the state of this NetworkRequestEntry for logging and debugging purposes. + * + *

PII and credentials MUST NEVER be dumped here. + */ + public void dump(IndentingPrintWriter pw) { + pw.println("NetworkRequestEntry:"); + pw.increaseIndent(); + + pw.println("mRequest: " + mRequest); + pw.println("mScore: " + mScore); + pw.println("mProviderId: " + mProviderId); + + pw.decreaseIndent(); + } } // package-private interface NetworkRequestListener { void onNetworkRequested(@NonNull NetworkRequest request, int score, int providerId); } + + /** + * Dumps the state of this VcnNetworkProvider for logging and debugging purposes. + * + *

PII and credentials MUST NEVER be dumped here. + */ + public void dump(IndentingPrintWriter pw) { + pw.println("VcnNetworkProvider:"); + pw.increaseIndent(); + + pw.println("mListeners:"); + for (NetworkRequestListener listener : mListeners) { + pw.println(listener); + } + pw.println(); + + pw.println("mRequests.values:"); + for (NetworkRequestEntry entry : mRequests.values()) { + entry.dump(pw); + } + pw.println(); + + pw.decreaseIndent(); + } } diff --git a/services/core/java/com/android/server/vcn/util/MtuUtils.java b/services/core/java/com/android/server/vcn/util/MtuUtils.java new file mode 100644 index 0000000000000..49c1a02215e30 --- /dev/null +++ b/services/core/java/com/android/server/vcn/util/MtuUtils.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2021 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.util; + +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_3DES; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CTR; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_CHACHA20_POLY1305; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_AES_CMAC_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_AES_XCBC_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA1_96; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_384_192; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_512_256; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_NONE; + +import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; + +import static java.lang.Math.max; +import static java.util.Collections.unmodifiableMap; + +import android.annotation.NonNull; +import android.net.ipsec.ike.ChildSaProposal; +import android.util.ArrayMap; +import android.util.Pair; +import android.util.Slog; + +import java.util.List; +import java.util.Map; + +/** @hide */ +public class MtuUtils { + private static final String TAG = MtuUtils.class.getSimpleName(); + /** + * Max ESP overhead possible + * + *

60 (Outer IPv4 + options) + 8 (UDP encap) + 4 (SPI) + 4 (Seq) + 2 (Pad + NextHeader) + */ + private static final int GENERIC_ESP_OVERHEAD_MAX = 78; + + /** Maximum overheads of authentication algorithms, keyed on IANA-defined constants */ + private static final Map AUTH_ALGORITHM_OVERHEAD; + + static { + final Map map = new ArrayMap<>(); + map.put(INTEGRITY_ALGORITHM_NONE, 0); + map.put(INTEGRITY_ALGORITHM_HMAC_SHA1_96, 12); + map.put(INTEGRITY_ALGORITHM_AES_XCBC_96, 12); + map.put(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128, 32); + map.put(INTEGRITY_ALGORITHM_HMAC_SHA2_384_192, 48); + map.put(INTEGRITY_ALGORITHM_HMAC_SHA2_512_256, 64); + map.put(INTEGRITY_ALGORITHM_AES_CMAC_96, 12); + + AUTH_ALGORITHM_OVERHEAD = unmodifiableMap(map); + } + + /** Maximum overheads of encryption algorithms, keyed on IANA-defined constants */ + private static final Map CRYPT_ALGORITHM_OVERHEAD; + + static { + final Map map = new ArrayMap<>(); + map.put(ENCRYPTION_ALGORITHM_3DES, 15); // 8 (IV) + 7 (Max pad) + map.put(ENCRYPTION_ALGORITHM_AES_CBC, 31); // 16 (IV) + 15 (Max pad) + map.put(ENCRYPTION_ALGORITHM_AES_CTR, 11); // 8 (IV) + 3 (Max pad) + + CRYPT_ALGORITHM_OVERHEAD = unmodifiableMap(map); + } + + /** Maximum overheads of combined mode algorithms, keyed on IANA-defined constants */ + private static final Map AUTHCRYPT_ALGORITHM_OVERHEAD; + + static { + final Map map = new ArrayMap<>(); + map.put(ENCRYPTION_ALGORITHM_AES_GCM_8, 19); // 8 (IV) + 3 (Max pad) + 8 (ICV) + map.put(ENCRYPTION_ALGORITHM_AES_GCM_12, 23); // 8 (IV) + 3 (Max pad) + 12 (ICV) + map.put(ENCRYPTION_ALGORITHM_AES_GCM_16, 27); // 8 (IV) + 3 (Max pad) + 16 (ICV) + map.put(ENCRYPTION_ALGORITHM_CHACHA20_POLY1305, 27); // 8 (IV) + 3 (Max pad) + 16 (ICV) + + AUTHCRYPT_ALGORITHM_OVERHEAD = unmodifiableMap(map); + } + + /** + * Calculates the MTU of the inner interface based on the parameters provided + * + *

The MTU of the inner interface will be the minimum of the following: + * + *

+ */ + public static int getMtu( + @NonNull List childProposals, int maxMtu, int underlyingMtu) { + if (underlyingMtu <= 0) { + return IPV6_MIN_MTU; + } + + boolean hasUnknownAlgorithm = false; + int maxAuthOverhead = 0; + int maxCryptOverhead = 0; + int maxAuthCryptOverhead = 0; + + for (ChildSaProposal proposal : childProposals) { + for (Pair encryptionAlgoPair : proposal.getEncryptionAlgorithms()) { + final int algo = encryptionAlgoPair.first; + + if (AUTHCRYPT_ALGORITHM_OVERHEAD.containsKey(algo)) { + maxAuthCryptOverhead = + max(maxAuthCryptOverhead, AUTHCRYPT_ALGORITHM_OVERHEAD.get(algo)); + continue; + } else if (CRYPT_ALGORITHM_OVERHEAD.containsKey(algo)) { + maxCryptOverhead = max(maxCryptOverhead, CRYPT_ALGORITHM_OVERHEAD.get(algo)); + continue; + } + + Slog.wtf(TAG, "Unknown encryption algorithm requested: " + algo); + return IPV6_MIN_MTU; + } + + for (int algo : proposal.getIntegrityAlgorithms()) { + if (AUTH_ALGORITHM_OVERHEAD.containsKey(algo)) { + maxAuthOverhead = max(maxAuthOverhead, AUTH_ALGORITHM_OVERHEAD.get(algo)); + continue; + } + + Slog.wtf(TAG, "Unknown integrity algorithm requested: " + algo); + return IPV6_MIN_MTU; + } + } + + // Return minimum of maxMtu, and the adjusted MTUs based on algorithms. + final int combinedModeMtu = underlyingMtu - maxAuthCryptOverhead - GENERIC_ESP_OVERHEAD_MAX; + final int normalModeMtu = + underlyingMtu - maxCryptOverhead - maxAuthOverhead - GENERIC_ESP_OVERHEAD_MAX; + return Math.min(Math.min(maxMtu, combinedModeMtu), normalModeMtu); + } +} diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java index 34c00182f855a..bb67593d84ba1 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionConnectedStateTest.java @@ -26,6 +26,7 @@ import static android.net.vcn.VcnManager.VCN_ERROR_CODE_NETWORK_ERROR; import static com.android.server.vcn.VcnGatewayConnection.VcnChildSessionConfiguration; import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; +import static com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -46,15 +47,19 @@ import android.net.LinkAddress; import android.net.LinkProperties; import android.net.NetworkAgent; import android.net.NetworkCapabilities; +import android.net.ipsec.ike.ChildSaProposal; import android.net.ipsec.ike.exceptions.AuthenticationFailedException; import android.net.ipsec.ike.exceptions.IkeException; import android.net.ipsec.ike.exceptions.IkeInternalException; import android.net.ipsec.ike.exceptions.TemporaryFailureException; +import android.net.vcn.VcnControlPlaneIkeConfig; import android.net.vcn.VcnManager.VcnErrorCode; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.vcn.util.MtuUtils; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -73,13 +78,13 @@ import java.util.function.Consumer; @SmallTest public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnectionTestBase { private VcnIkeSession mIkeSession; - private NetworkAgent mNetworkAgent; + private VcnNetworkAgent mNetworkAgent; @Before public void setUp() throws Exception { super.setUp(); - mNetworkAgent = mock(NetworkAgent.class); + mNetworkAgent = mock(VcnNetworkAgent.class); doReturn(mNetworkAgent) .when(mDeps) .newNetworkAgent(any(), any(), any(), any(), anyInt(), any(), any(), any(), any()); @@ -152,7 +157,9 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection } @Test - public void testMigratedTransformsAreApplied() throws Exception { + public void testMigration() throws Exception { + triggerChildOpened(); + getChildSessionCallback() .onIpSecTransformsMigrated(makeDummyIpSecTransform(), makeDummyIpSecTransform()); mTestLooper.dispatchAll(); @@ -170,6 +177,17 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection } assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); + + final List saProposals = + ((VcnControlPlaneIkeConfig) mConfig.getControlPlaneConfig()) + .getChildSessionParams() + .getSaProposals(); + final int expectedMtu = + MtuUtils.getMtu( + saProposals, + mConfig.getMaxMtu(), + TEST_UNDERLYING_NETWORK_RECORD_1.linkProperties.getMtu()); + verify(mNetworkAgent).sendLinkProperties(argThat(lp -> expectedMtu == lp.getMtu())); } private void triggerChildOpened() { @@ -299,8 +317,9 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection .removeAddressFromTunnelInterface( eq(TEST_IPSEC_TUNNEL_RESOURCE_ID), eq(TEST_INTERNAL_ADDR), any()); - // TODO(b/184579891): Also verify link properties updated and sent when sendLinkProperties - // is mockable + verify(mNetworkAgent).sendLinkProperties(argThat( + lp -> newInternalAddrs.equals(lp.getLinkAddresses()) + && Collections.singletonList(TEST_DNS_ADDR_2).equals(lp.getDnsServers()))); // Verify that IpSecTunnelInterface only created once verify(mIpSecSvc).createTunnelInterface(any(), any(), any(), any(), any()); @@ -323,6 +342,66 @@ public class VcnGatewayConnectionConnectedStateTest extends VcnGatewayConnection assertFalse(mGatewayConnection.isInSafeMode()); } + private Consumer setupNetworkAndGetUnwantedCallback() { + triggerChildOpened(); + mTestLooper.dispatchAll(); + + final ArgumentCaptor> unwantedCallbackCaptor = + ArgumentCaptor.forClass(Consumer.class); + verify(mDeps) + .newNetworkAgent( + any(), + any(), + any(), + any(), + anyInt(), + any(), + any(), + unwantedCallbackCaptor.capture(), + any()); + + return unwantedCallbackCaptor.getValue(); + } + + @Test + public void testUnwantedNetworkAgentTriggersTeardown() throws Exception { + final Consumer unwantedCallback = setupNetworkAndGetUnwantedCallback(); + + unwantedCallback.accept(mNetworkAgent); + mTestLooper.dispatchAll(); + + assertTrue(mGatewayConnection.isQuitting()); + assertEquals(mGatewayConnection.mDisconnectingState, mGatewayConnection.getCurrentState()); + } + + @Test + public void testUnwantedNetworkAgentWithDisconnectedNetworkAgent() throws Exception { + final Consumer unwantedCallback = setupNetworkAndGetUnwantedCallback(); + + mGatewayConnection.setNetworkAgent(null); + unwantedCallback.accept(mNetworkAgent); + mTestLooper.dispatchAll(); + + // Verify that the call was ignored; the state machine is still running, and the state has + // not changed. + assertFalse(mGatewayConnection.isQuitting()); + assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); + } + + @Test + public void testUnwantedNetworkAgentWithNewNetworkAgent() throws Exception { + final Consumer unwantedCallback = setupNetworkAndGetUnwantedCallback(); + final VcnNetworkAgent testAgent = mock(VcnNetworkAgent.class); + + mGatewayConnection.setNetworkAgent(testAgent); + unwantedCallback.accept(mNetworkAgent); + mTestLooper.dispatchAll(); + + assertFalse(mGatewayConnection.isQuitting()); + assertEquals(mGatewayConnection.mConnectedState, mGatewayConnection.getCurrentState()); + assertEquals(testAgent, mGatewayConnection.getNetworkAgent()); + } + @Test public void testChildSessionClosedTriggersDisconnect() throws Exception { // Verify scheduled but not canceled when entering ConnectedState diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index c5ed8f6ddcc7c..dc73be25ffa3c 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -18,6 +18,7 @@ package com.android.server.vcn; import static com.android.server.vcn.UnderlyingNetworkTracker.UnderlyingNetworkRecord; import static com.android.server.vcn.VcnGatewayConnection.VcnIkeSession; +import static com.android.server.vcn.VcnGatewayConnection.VcnNetworkAgent; import static com.android.server.vcn.VcnTestUtils.setupIpSecManager; import static org.junit.Assert.assertEquals; @@ -44,7 +45,6 @@ import android.net.IpSecTunnelInterfaceResponse; import android.net.LinkAddress; import android.net.LinkProperties; import android.net.Network; -import android.net.NetworkAgent; import android.net.NetworkCapabilities; import android.net.ipsec.ike.ChildSessionCallback; import android.net.ipsec.ike.IkeSessionCallback; @@ -90,12 +90,18 @@ public class VcnGatewayConnectionTestBase { protected static final int TEST_SUB_ID = 5; protected static final long ELAPSED_REAL_TIME = 123456789L; protected static final String TEST_IPSEC_TUNNEL_IFACE = "IPSEC_IFACE"; + protected static final UnderlyingNetworkRecord TEST_UNDERLYING_NETWORK_RECORD_1 = new UnderlyingNetworkRecord( new Network(0), new NetworkCapabilities(), new LinkProperties(), false /* blocked */); + + static { + TEST_UNDERLYING_NETWORK_RECORD_1.linkProperties.setMtu(1500); + } + protected static final UnderlyingNetworkRecord TEST_UNDERLYING_NETWORK_RECORD_2 = new UnderlyingNetworkRecord( new Network(1), @@ -103,6 +109,10 @@ public class VcnGatewayConnectionTestBase { new LinkProperties(), false /* blocked */); + static { + TEST_UNDERLYING_NETWORK_RECORD_2.linkProperties.setMtu(1460); + } + protected static final TelephonySubscriptionSnapshot TEST_SUBSCRIPTION_SNAPSHOT = new TelephonySubscriptionSnapshot( Collections.singletonMap(TEST_SUB_ID, TEST_SUB_GRP), Collections.EMPTY_MAP); @@ -278,8 +288,8 @@ public class VcnGatewayConnectionTestBase { protected void verifySafeModeTimeoutNotifiesCallbackAndUnregistersNetworkAgent( @NonNull State expectedState) { - // Set a NetworkAgent, and expect it to be unregistered and cleared - final NetworkAgent mockNetworkAgent = mock(NetworkAgent.class); + // Set a VcnNetworkAgent, and expect it to be unregistered and cleared + final VcnNetworkAgent mockNetworkAgent = mock(VcnNetworkAgent.class); mGatewayConnection.setNetworkAgent(mockNetworkAgent); // SafeMode timer starts when VcnGatewayConnection exits DisconnectedState (the initial diff --git a/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java b/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java new file mode 100644 index 0000000000000..29511f780bf61 --- /dev/null +++ b/tests/vcn/java/com/android/server/vcn/util/MtuUtilsTest.java @@ -0,0 +1,92 @@ +/* + * 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.util; + +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_CBC; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_12; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_16; +import static android.net.ipsec.ike.SaProposal.ENCRYPTION_ALGORITHM_AES_GCM_8; +import static android.net.ipsec.ike.SaProposal.INTEGRITY_ALGORITHM_HMAC_SHA2_256_128; +import static android.net.ipsec.ike.SaProposal.KEY_LEN_AES_256; + +import static com.android.net.module.util.NetworkStackConstants.ETHER_MTU; +import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; +import static com.android.server.vcn.util.MtuUtils.getMtu; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import static java.util.Collections.emptyList; + +import android.net.ipsec.ike.ChildSaProposal; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MtuUtilsTest { + @Test + public void testUnderlyingMtuZero() { + assertEquals( + IPV6_MIN_MTU, getMtu(emptyList(), ETHER_MTU /* maxMtu */, 0 /* underlyingMtu */)); + } + + @Test + public void testClampsToMaxMtu() { + assertEquals(0, getMtu(emptyList(), 0 /* maxMtu */, IPV6_MIN_MTU /* underlyingMtu */)); + } + + @Test + public void testNormalModeAlgorithmLessThanUnderlyingMtu() { + final List saProposals = + Arrays.asList( + new ChildSaProposal.Builder() + .addEncryptionAlgorithm( + ENCRYPTION_ALGORITHM_AES_CBC, KEY_LEN_AES_256) + .addIntegrityAlgorithm(INTEGRITY_ALGORITHM_HMAC_SHA2_256_128) + .build()); + + final int actualMtu = + getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */); + assertTrue(ETHER_MTU > actualMtu); + } + + @Test + public void testCombinedModeAlgorithmLessThanUnderlyingMtu() { + final List saProposals = + Arrays.asList( + new ChildSaProposal.Builder() + .addEncryptionAlgorithm( + ENCRYPTION_ALGORITHM_AES_GCM_16, KEY_LEN_AES_256) + .addEncryptionAlgorithm( + ENCRYPTION_ALGORITHM_AES_GCM_12, KEY_LEN_AES_256) + .addEncryptionAlgorithm( + ENCRYPTION_ALGORITHM_AES_GCM_8, KEY_LEN_AES_256) + .build()); + + final int actualMtu = + getMtu(saProposals, ETHER_MTU /* maxMtu */, ETHER_MTU /* underlyingMtu */); + assertTrue(ETHER_MTU > actualMtu); + } +}