diff --git a/Android.bp b/Android.bp index 26e716530426d..4b3c22da5c7cc 100644 --- a/Android.bp +++ b/Android.bp @@ -314,6 +314,7 @@ filegroup { ":framework-telecomm-sources", ":framework-telephony-common-sources", ":framework-telephony-sources", + ":framework-vcn-util-sources", ":framework-wifi-annotations", ":framework-wifi-non-updatable-sources", ":PacProcessor-aidl-sources", diff --git a/core/java/android/net/vcn/VcnConfig.java b/core/java/android/net/vcn/VcnConfig.java index 148acf1308576..d4a3fa7411b18 100644 --- a/core/java/android/net/vcn/VcnConfig.java +++ b/core/java/android/net/vcn/VcnConfig.java @@ -15,30 +15,104 @@ */ package android.net.vcn; +import static com.android.internal.annotations.VisibleForTesting.Visibility; + import android.annotation.NonNull; +import android.annotation.Nullable; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; +import android.util.ArraySet; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.server.vcn.util.PersistableBundleUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; /** * This class represents a configuration for a Virtual Carrier Network. * + *

Each {@link VcnGatewayConnectionConfig} instance added represents a connection that will be + * brought up on demand based on active {@link NetworkRequest}(s). + * + * @see VcnManager for more information on the Virtual Carrier Network feature * @hide */ public final class VcnConfig implements Parcelable { @NonNull private static final String TAG = VcnConfig.class.getSimpleName(); - private VcnConfig() { + private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs"; + @NonNull private final Set mGatewayConnectionConfigs; + + private VcnConfig(@NonNull Set tunnelConfigs) { + mGatewayConnectionConfigs = Collections.unmodifiableSet(tunnelConfigs); + validate(); } - // TODO: Implement getters, validators, etc /** - * Validates this configuration. + * Deserializes a VcnConfig from a PersistableBundle. * * @hide */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public VcnConfig(@NonNull PersistableBundle in) { + final PersistableBundle gatewayConnectionConfigsBundle = + in.getPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY); + mGatewayConnectionConfigs = + new ArraySet<>( + PersistableBundleUtils.toList( + gatewayConnectionConfigsBundle, VcnGatewayConnectionConfig::new)); + + validate(); + } + private void validate() { - // TODO: implement validation logic + Preconditions.checkCollectionNotEmpty( + mGatewayConnectionConfigs, "gatewayConnectionConfigs"); + } + + /** Retrieves the set of configured tunnels. */ + @NonNull + public Set getGatewayConnectionConfigs() { + return Collections.unmodifiableSet(mGatewayConnectionConfigs); + } + + /** + * Serializes this object to a PersistableBundle. + * + * @hide + */ + @NonNull + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + final PersistableBundle gatewayConnectionConfigsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mGatewayConnectionConfigs), + VcnGatewayConnectionConfig::toPersistableBundle); + result.putPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY, gatewayConnectionConfigsBundle); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash(mGatewayConnectionConfigs); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof VcnConfig)) { + return false; + } + + final VcnConfig rhs = (VcnConfig) other; + return mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs); } // Parcelable methods @@ -49,15 +123,16 @@ public final class VcnConfig implements Parcelable { } @Override - public void writeToParcel(Parcel out, int flags) {} + public void writeToParcel(Parcel out, int flags) { + out.writeParcelable(toPersistableBundle(), flags); + } @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @NonNull public VcnConfig createFromParcel(Parcel in) { - // TODO: Ensure all methods are pulled from the parcels - return new VcnConfig(); + return new VcnConfig((PersistableBundle) in.readParcelable(null)); } @NonNull @@ -68,7 +143,23 @@ public final class VcnConfig implements Parcelable { /** This class is used to incrementally build {@link VcnConfig} objects. */ public static class Builder { - // TODO: Implement this builder + @NonNull + private final Set mGatewayConnectionConfigs = new ArraySet<>(); + + /** + * Adds a configuration for an individual gateway connection. + * + * @param gatewayConnectionConfig the configuration for an individual gateway connection + * @return this {@link Builder} instance, for chaining + */ + @NonNull + public Builder addGatewayConnectionConfig( + @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig) { + Objects.requireNonNull(gatewayConnectionConfig, "gatewayConnectionConfig was null"); + + mGatewayConnectionConfigs.add(gatewayConnectionConfig); + return this; + } /** * Builds and validates the VcnConfig. @@ -77,7 +168,7 @@ public final class VcnConfig implements Parcelable { */ @NonNull public VcnConfig build() { - return new VcnConfig(); + return new VcnConfig(mGatewayConnectionConfigs); } } } diff --git a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java index 8160edc87440e..039360a69a3a5 100644 --- a/core/java/android/net/vcn/VcnGatewayConnectionConfig.java +++ b/core/java/android/net/vcn/VcnGatewayConnectionConfig.java @@ -15,7 +15,27 @@ */ package android.net.vcn; +import static android.net.NetworkCapabilities.NetCapability; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.IntRange; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.NetworkCapabilities; +import android.os.PersistableBundle; +import android.util.ArraySet; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; +import com.android.server.vcn.util.PersistableBundleUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; /** * This class represents a configuration for a connection to a Virtual Carrier Network gateway. @@ -49,38 +69,399 @@ import android.annotation.NonNull; *

  • {@link android.net.NetworkCapabilities.NET_CAPABILITY_MCX} * * + *

    The meteredness and roaming of the VCN {@link Network} will be determined by that of the + * underlying Network(s). + * * @hide */ public final class VcnGatewayConnectionConfig { - private VcnGatewayConnectionConfig() { + // TODO: Use MIN_MTU_V6 once it is public, @hide + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int MIN_MTU_V6 = 1280; + + private static final Set ALLOWED_CAPABILITIES; + + static { + Set allowedCaps = new ArraySet<>(); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_MMS); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_SUPL); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_DUN); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_FOTA); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_IMS); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_CBS); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_IA); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_RCS); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_XCAP); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_EIMS); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_INTERNET); + allowedCaps.add(NetworkCapabilities.NET_CAPABILITY_MCX); + + ALLOWED_CAPABILITIES = Collections.unmodifiableSet(allowedCaps); + } + + private static final int DEFAULT_MAX_MTU = 1500; + + /** + * The maximum number of retry intervals that may be specified. + * + *

    Limited to ensure an upper bound on config sizes. + */ + private static final int MAX_RETRY_INTERVAL_COUNT = 10; + + /** + * The minimum allowable repeating retry interval + * + *

    To ensure the device is not constantly being woken up, this retry interval MUST be greater + * than this value. + * + * @see {@link Builder#setRetryInterval()} + */ + private static final long MINIMUM_REPEATING_RETRY_INTERVAL_MS = TimeUnit.MINUTES.toMillis(15); + + private static final long[] DEFAULT_RETRY_INTERVALS_MS = + new long[] { + TimeUnit.SECONDS.toMillis(1), + TimeUnit.SECONDS.toMillis(2), + TimeUnit.SECONDS.toMillis(5), + TimeUnit.SECONDS.toMillis(30), + TimeUnit.MINUTES.toMillis(1), + TimeUnit.MINUTES.toMillis(5), + TimeUnit.MINUTES.toMillis(15) + }; + + private static final String EXPOSED_CAPABILITIES_KEY = "mExposedCapabilities"; + @NonNull private final Set mExposedCapabilities; + + private static final String UNDERLYING_CAPABILITIES_KEY = "mUnderlyingCapabilities"; + @NonNull private final Set mUnderlyingCapabilities; + + // TODO: Add Ike/ChildSessionParams as a subclass - maybe VcnIkeGatewayConnectionConfig + + private static final String MAX_MTU_KEY = "mMaxMtu"; + private final int mMaxMtu; + + private static final String RETRY_INTERVAL_MS_KEY = "mRetryIntervalsMs"; + @NonNull private final long[] mRetryIntervalsMs; + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public VcnGatewayConnectionConfig( + @NonNull Set exposedCapabilities, + @NonNull Set underlyingCapabilities, + @NonNull long[] retryIntervalsMs, + @IntRange(from = MIN_MTU_V6) int maxMtu) { + mExposedCapabilities = exposedCapabilities; + mUnderlyingCapabilities = underlyingCapabilities; + mRetryIntervalsMs = retryIntervalsMs; + mMaxMtu = maxMtu; + validate(); } - // TODO: Implement getters, validators, etc + /** @hide */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public VcnGatewayConnectionConfig(@NonNull PersistableBundle in) { + final PersistableBundle exposedCapsBundle = + in.getPersistableBundle(EXPOSED_CAPABILITIES_KEY); + final PersistableBundle underlyingCapsBundle = + in.getPersistableBundle(UNDERLYING_CAPABILITIES_KEY); + + mExposedCapabilities = new ArraySet<>(PersistableBundleUtils.toList( + exposedCapsBundle, PersistableBundleUtils.INTEGER_DESERIALIZER)); + mUnderlyingCapabilities = new ArraySet<>(PersistableBundleUtils.toList( + underlyingCapsBundle, PersistableBundleUtils.INTEGER_DESERIALIZER)); + mRetryIntervalsMs = in.getLongArray(RETRY_INTERVAL_MS_KEY); + mMaxMtu = in.getInt(MAX_MTU_KEY); + + validate(); + } + + private void validate() { + Preconditions.checkArgument( + mExposedCapabilities != null && !mExposedCapabilities.isEmpty(), + "exposedCapsBundle was null or empty"); + for (Integer cap : getAllExposedCapabilities()) { + checkValidCapability(cap); + } + + Preconditions.checkArgument( + mUnderlyingCapabilities != null && !mUnderlyingCapabilities.isEmpty(), + "underlyingCapabilities was null or empty"); + for (Integer cap : getAllUnderlyingCapabilities()) { + checkValidCapability(cap); + } + + Objects.requireNonNull(mRetryIntervalsMs, "retryIntervalsMs was null"); + validateRetryInterval(mRetryIntervalsMs); + + Preconditions.checkArgument( + mMaxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)"); + } + + private static void checkValidCapability(int capability) { + Preconditions.checkArgument( + ALLOWED_CAPABILITIES.contains(capability), + "NetworkCapability " + capability + "out of range"); + } + + private static void validateRetryInterval(@Nullable long[] retryIntervalsMs) { + Preconditions.checkArgument( + retryIntervalsMs != null + && retryIntervalsMs.length > 0 + && retryIntervalsMs.length <= MAX_RETRY_INTERVAL_COUNT, + "retryIntervalsMs was null, empty or exceed max interval count"); + + final long repeatingInterval = retryIntervalsMs[retryIntervalsMs.length - 1]; + if (repeatingInterval < MINIMUM_REPEATING_RETRY_INTERVAL_MS) { + throw new IllegalArgumentException( + "Repeating retry interval was too short, must be a minimum of 15 minutes: " + + repeatingInterval); + } + } /** - * Validates this configuration + * Returns all exposed capabilities. * * @hide */ - private void validate() { - // TODO: implement validation logic + @NonNull + public Set getAllExposedCapabilities() { + return Collections.unmodifiableSet(mExposedCapabilities); } - // Parcelable methods + /** + * Checks if this config is configured to support/expose a specific capability. + * + * @param capability the capability to check for + */ + public boolean hasExposedCapability(@NetCapability int capability) { + checkValidCapability(capability); - /** This class is used to incrementally build {@link VcnGatewayConnectionConfig} objects */ + return mExposedCapabilities.contains(capability); + } + + /** + * Returns all capabilities required of underlying networks. + * + * @hide + */ + @NonNull + public Set getAllUnderlyingCapabilities() { + return Collections.unmodifiableSet(mUnderlyingCapabilities); + } + + /** + * Checks if this config requires an underlying network to have the specified capability. + * + * @param capability the capability to check for + */ + public boolean requiresUnderlyingCapability(@NetCapability int capability) { + checkValidCapability(capability); + + return mUnderlyingCapabilities.contains(capability); + } + + /** Retrieves the configured retry intervals. */ + @NonNull + public long[] getRetryIntervalsMs() { + return Arrays.copyOf(mRetryIntervalsMs, mRetryIntervalsMs.length); + } + + /** Retrieves the maximum MTU allowed for this Gateway Connection. */ + @IntRange(from = MIN_MTU_V6) + public int getMaxMtu() { + return mMaxMtu; + } + + /** + * Converts this config to a PersistableBundle. + * + * @hide + */ + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + final PersistableBundle exposedCapsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mExposedCapabilities), + PersistableBundleUtils.INTEGER_SERIALIZER); + final PersistableBundle underlyingCapsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mUnderlyingCapabilities), + PersistableBundleUtils.INTEGER_SERIALIZER); + + result.putPersistableBundle(EXPOSED_CAPABILITIES_KEY, exposedCapsBundle); + result.putPersistableBundle(UNDERLYING_CAPABILITIES_KEY, underlyingCapsBundle); + result.putLongArray(RETRY_INTERVAL_MS_KEY, mRetryIntervalsMs); + result.putInt(MAX_MTU_KEY, mMaxMtu); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash( + mExposedCapabilities, + mUnderlyingCapabilities, + Arrays.hashCode(mRetryIntervalsMs), + mMaxMtu); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof VcnGatewayConnectionConfig)) { + return false; + } + + final VcnGatewayConnectionConfig rhs = (VcnGatewayConnectionConfig) other; + return mExposedCapabilities.equals(rhs.mExposedCapabilities) + && mUnderlyingCapabilities.equals(rhs.mUnderlyingCapabilities) + && Arrays.equals(mRetryIntervalsMs, rhs.mRetryIntervalsMs) + && mMaxMtu == rhs.mMaxMtu; + } + + /** This class is used to incrementally build {@link VcnGatewayConnectionConfig} objects. */ public static class Builder { - // TODO: Implement this builder + @NonNull private final Set mExposedCapabilities = new ArraySet(); + @NonNull private final Set mUnderlyingCapabilities = new ArraySet(); + @NonNull private long[] mRetryIntervalsMs = DEFAULT_RETRY_INTERVALS_MS; + private int mMaxMtu = DEFAULT_MAX_MTU; + + // TODO: (b/175829816) Consider VCN-exposed capabilities that may be transport dependent. + // Consider the case where the VCN might only expose MMS on WiFi, but defer to MMS + // when on Cell. /** - * Builds and validates the VcnGatewayConnectionConfig + * Add a capability that this VCN Gateway Connection will support. + * + * @param exposedCapability the app-facing capability to be exposed by this VCN Gateway + * Connection (i.e., the capabilities that this VCN Gateway Connection will support). + * @return this {@link Builder} instance, for chaining + * @see VcnGatewayConnectionConfig for a list of capabilities may be exposed by a Gateway + * Connection + */ + public Builder addExposedCapability(@NetCapability int exposedCapability) { + checkValidCapability(exposedCapability); + + mExposedCapabilities.add(exposedCapability); + return this; + } + + /** + * Remove a capability that this VCN Gateway Connection will support. + * + * @param exposedCapability the app-facing capability to not be exposed by this VCN Gateway + * Connection (i.e., the capabilities that this VCN Gateway Connection will support) + * @return this {@link Builder} instance, for chaining + * @see VcnGatewayConnectionConfig for a list of capabilities may be exposed by a Gateway + * Connection + */ + public Builder removeExposedCapability(@NetCapability int exposedCapability) { + checkValidCapability(exposedCapability); + + mExposedCapabilities.remove(exposedCapability); + return this; + } + + /** + * Require a capability for Networks underlying this VCN Gateway Connection. + * + * @param underlyingCapability the capability that a network MUST have in order to be an + * underlying network for this VCN Gateway Connection. + * @return this {@link Builder} instance, for chaining + * @see VcnGatewayConnectionConfig for a list of capabilities may be required of underlying + * networks + */ + public Builder addRequiredUnderlyingCapability(@NetCapability int underlyingCapability) { + checkValidCapability(underlyingCapability); + + mUnderlyingCapabilities.add(underlyingCapability); + return this; + } + + /** + * Remove a requirement of a capability for Networks underlying this VCN Gateway Connection. + * + *

    Calling this method will allow Networks that do NOT have this capability to be + * selected as an underlying network for this VCN Gateway Connection. However, underlying + * networks MAY still have the removed capability. + * + * @param underlyingCapability the capability that a network DOES NOT need to have in order + * to be an underlying network for this VCN Gateway Connection. + * @return this {@link Builder} instance, for chaining + * @see VcnGatewayConnectionConfig for a list of capabilities may be required of underlying + * networks + */ + public Builder removeRequiredUnderlyingCapability(@NetCapability int underlyingCapability) { + checkValidCapability(underlyingCapability); + + mUnderlyingCapabilities.remove(underlyingCapability); + return this; + } + + /** + * Set the retry interval between VCN establishment attempts upon successive failures. + * + *

    The last retry interval will be repeated until safe mode is entered, or a connection + * is successfully established, at which point the retry timers will be reset. For power + * reasons, the last (repeated) retry interval MUST be at least 15 minutes. + * + *

    Retry intervals MAY be subject to system power saving modes. That is to say that if + * the system enters a power saving mode, the retry may not occur until the device leaves + * the specified power saving mode. Intervals are sequential, and intervals will NOT be + * skipped if system power saving results in delaying retries (even if it exceed multiple + * retry intervals). + * + *

    Each Gateway Connection will retry according to the retry intervals configured, but if + * safe mode is enabled, all Gateway Connection(s) will be disabled. + * + * @param retryIntervalsMs an array of between 1 and 10 millisecond intervals after which + * the VCN will attempt to retry a session initiation. The last (repeating) retry + * interval must be at least 15 minutes. Defaults to: {@code [1s, 2s, 5s, 30s, 1m, 5m, + * 15m]} + * @return this {@link Builder} instance, for chaining + * @see VcnManager for additional discussion on fail-safe mode + */ + @NonNull + public Builder setRetryInterval(@NonNull long[] retryIntervalsMs) { + validateRetryInterval(retryIntervalsMs); + + mRetryIntervalsMs = retryIntervalsMs; + return this; + } + + /** + * Sets the maximum MTU allowed for this VCN Gateway Connection. + * + *

    This MTU is applied to the VCN Gateway Connection exposed Networks, and represents the + * MTU of the virtualized network. + * + *

    The system may reduce the MTU below the maximum specified based on signals such as the + * MTU of the underlying networks (and adjusted for Gateway Connection overhead). + * + * @param maxMtu the maximum MTU allowed for this Gateway Connection. Must be greater than + * the IPv6 minimum MTU of 1280. Defaults to 1500. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + public Builder setMaxMtu(@IntRange(from = MIN_MTU_V6) int maxMtu) { + Preconditions.checkArgument( + maxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)"); + + mMaxMtu = maxMtu; + return this; + } + + /** + * Builds and validates the VcnGatewayConnectionConfig. * * @return an immutable VcnGatewayConnectionConfig instance */ @NonNull public VcnGatewayConnectionConfig build() { - return new VcnGatewayConnectionConfig(); + return new VcnGatewayConnectionConfig( + mExposedCapabilities, mUnderlyingCapabilities, mRetryIntervalsMs, mMaxMtu); } } } diff --git a/core/java/android/net/vcn/VcnManager.java b/core/java/android/net/vcn/VcnManager.java index 6769b9e46e4c9..46d1c1c7a23a0 100644 --- a/core/java/android/net/vcn/VcnManager.java +++ b/core/java/android/net/vcn/VcnManager.java @@ -23,6 +23,9 @@ import android.annotation.SystemService; import android.content.Context; import android.os.ParcelUuid; import android.os.RemoteException; +import android.os.ServiceSpecificException; + +import java.io.IOException; /** * VcnManager publishes APIs for applications to configure and manage Virtual Carrier Networks. @@ -63,15 +66,20 @@ public final class VcnManager { * @param config the configuration parameters for the VCN * @throws SecurityException if the caller does not have carrier privileges, or is not running * as the primary user + * @throws IOException if the configuration failed to be persisted. A caller encountering this + * exception should attempt to retry (possibly after a delay). * @hide */ @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant - public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + public void setVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) + throws IOException { requireNonNull(subscriptionGroup, "subscriptionGroup was null"); requireNonNull(config, "config was null"); try { mService.setVcnConfig(subscriptionGroup, config); + } catch (ServiceSpecificException e) { + throw new IOException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -88,14 +96,18 @@ public final class VcnManager { * @param subscriptionGroup the subscription group that the configuration should be applied to * @throws SecurityException if the caller does not have carrier privileges, or is not running * as the primary user + * @throws IOException if the configuration failed to be cleared. A caller encountering this + * exception should attempt to retry (possibly after a delay). * @hide */ @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant - public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) { + public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) throws IOException { requireNonNull(subscriptionGroup, "subscriptionGroup was null"); try { mService.clearVcnConfig(subscriptionGroup); + } catch (ServiceSpecificException e) { + throw new IOException(e); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index e9f17fff5a616..5e85409b0a422 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -26,20 +26,31 @@ import android.net.NetworkRequest; import android.net.vcn.IVcnManagementService; import android.net.vcn.VcnConfig; import android.os.Binder; +import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.ParcelUuid; +import android.os.PersistableBundle; import android.os.Process; +import android.os.ServiceSpecificException; import android.os.UserHandle; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.util.ArrayMap; +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.server.vcn.util.PersistableBundleUtils; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; /** * VcnManagementService manages Virtual Carrier Network profiles and lifecycles. @@ -101,20 +112,72 @@ public class VcnManagementService extends IVcnManagementService.Stub { public static final boolean VDBG = false; // STOPSHIP: if true + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final String VCN_CONFIG_FILE = "/data/system/vcn/configs.xml"; + /* Binder context for this service */ @NonNull private final Context mContext; @NonNull private final Dependencies mDeps; @NonNull private final Looper mLooper; + @NonNull private final Handler mHandler; @NonNull private final VcnNetworkProvider mNetworkProvider; + @GuardedBy("mLock") + @NonNull + private final Map mConfigs = new ArrayMap<>(); + + @NonNull private final Object mLock = new Object(); + + @NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper; + @VisibleForTesting(visibility = Visibility.PRIVATE) VcnManagementService(@NonNull Context context, @NonNull Dependencies deps) { mContext = requireNonNull(context, "Missing context"); mDeps = requireNonNull(deps, "Missing dependencies"); mLooper = mDeps.getLooper(); + mHandler = new Handler(mLooper); mNetworkProvider = new VcnNetworkProvider(mContext, mLooper); + + mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE); + + // Run on handler to ensure I/O does not block system server startup + mHandler.post(() -> { + PersistableBundle configBundle = null; + try { + configBundle = mConfigDiskRwHelper.readFromDisk(); + } catch (IOException e1) { + Slog.e(TAG, "Failed to read configs from disk; retrying", e1); + + // Retry immediately. The IOException may have been transient. + try { + configBundle = mConfigDiskRwHelper.readFromDisk(); + } catch (IOException e2) { + Slog.wtf(TAG, "Failed to read configs from disk", e2); + return; + } + } + + if (configBundle != null) { + final Map configs = + PersistableBundleUtils.toMap( + configBundle, + PersistableBundleUtils::toParcelUuid, + VcnConfig::new); + + synchronized (mLock) { + for (Entry entry : configs.entrySet()) { + // Ensure no new configs are overwritten; a carrier app may have added a new + // config. + if (!mConfigs.containsKey(entry.getKey())) { + mConfigs.put(entry.getKey(), entry.getValue()); + } + } + // TODO: Trigger re-evaluation of active VCNs; start/stop VCNs as needed. + } + } + }); } // Package-visibility for SystemServer to create instances. @@ -151,12 +214,21 @@ public class VcnManagementService extends IVcnManagementService.Stub { public int getBinderCallingUid() { return Binder.getCallingUid(); } + + /** + * Creates and returns a new {@link PersistableBundle.LockingReadWriteHelper} + * + * @param path the file path to read/write from/to. + * @return the {@link PersistableBundleUtils.LockingReadWriteHelper} instance + */ + public PersistableBundleUtils.LockingReadWriteHelper + newPersistableBundleLockingReadWriteHelper(@NonNull String path) { + return new PersistableBundleUtils.LockingReadWriteHelper(path); + } } /** Notifies the VcnManagementService that external dependencies can be set up. */ public void systemReady() { - // TODO: Retrieve existing profiles from KeyStore - mContext.getSystemService(ConnectivityManager.class) .registerNetworkProvider(mNetworkProvider); } @@ -217,9 +289,15 @@ public class VcnManagementService extends IVcnManagementService.Stub { enforceCallingUserAndCarrierPrivilege(subscriptionGroup); - // TODO: Clear Binder calling identity + synchronized (mLock) { + mConfigs.put(subscriptionGroup, config); - // TODO: Store VCN configuration, trigger startup as necessary + // Must be done synchronously to ensure that writes do not happen out-of-order. + writeConfigsToDiskLocked(); + } + + // TODO: Clear Binder calling identity + // TODO: Trigger startup as necessary } /** @@ -233,9 +311,38 @@ public class VcnManagementService extends IVcnManagementService.Stub { enforceCallingUserAndCarrierPrivilege(subscriptionGroup); - // TODO: Clear Binder calling identity + synchronized (mLock) { + mConfigs.remove(subscriptionGroup); - // TODO: Clear VCN configuration, trigger teardown as necessary + // Must be done synchronously to ensure that writes do not happen out-of-order. + writeConfigsToDiskLocked(); + } + + // TODO: Clear Binder calling identity + // TODO: Trigger teardown as necessary + } + + @GuardedBy("mLock") + private void writeConfigsToDiskLocked() { + try { + PersistableBundle bundle = + PersistableBundleUtils.fromMap( + mConfigs, + PersistableBundleUtils::fromParcelUuid, + VcnConfig::toPersistableBundle); + mConfigDiskRwHelper.writeToDisk(bundle); + } catch (IOException e) { + Slog.e(TAG, "Failed to save configs to disk", e); + throw new ServiceSpecificException(0, "Failed to save configs"); + } + } + + /** Get current configuration list for testing purposes */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + Map getConfigs() { + synchronized (mLock) { + return Collections.unmodifiableMap(mConfigs); + } } /** diff --git a/services/core/java/com/android/server/vcn/Android.bp b/services/core/java/com/android/server/vcn/Android.bp new file mode 100644 index 0000000000000..5ed204fd76409 --- /dev/null +++ b/services/core/java/com/android/server/vcn/Android.bp @@ -0,0 +1,4 @@ +filegroup { + name: "framework-vcn-util-sources", + srcs: ["util/**/*.java"], +} \ No newline at end of file diff --git a/tests/vcn/java/android/net/vcn/VcnConfigTest.java b/tests/vcn/java/android/net/vcn/VcnConfigTest.java new file mode 100644 index 0000000000000..77944deb26f1c --- /dev/null +++ b/tests/vcn/java/android/net/vcn/VcnConfigTest.java @@ -0,0 +1,83 @@ +/* + * 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 android.net.vcn; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.os.Parcel; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Collections; +import java.util.Set; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VcnConfigTest { + private static final Set GATEWAY_CONNECTION_CONFIGS = + Collections.singleton(VcnGatewayConnectionConfigTest.buildTestConfig()); + + // Public visibility for VcnManagementServiceTest + public static VcnConfig buildTestConfig() { + VcnConfig.Builder builder = new VcnConfig.Builder(); + + for (VcnGatewayConnectionConfig gatewayConnectionConfig : GATEWAY_CONNECTION_CONFIGS) { + builder.addGatewayConnectionConfig(gatewayConnectionConfig); + } + + return builder.build(); + } + + @Test + public void testBuilderRequiresGatewayConnectionConfig() { + try { + new VcnConfig.Builder().build(); + fail("Expected exception due to no VcnGatewayConnectionConfigs provided"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testBuilderAndGetters() { + final VcnConfig config = buildTestConfig(); + + assertEquals(GATEWAY_CONNECTION_CONFIGS, config.getGatewayConnectionConfigs()); + } + + @Test + public void testPersistableBundle() { + final VcnConfig config = buildTestConfig(); + + assertEquals(config, new VcnConfig(config.toPersistableBundle())); + } + + @Test + public void testParceling() { + final VcnConfig config = buildTestConfig(); + + Parcel parcel = Parcel.obtain(); + config.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + + assertEquals(config, VcnConfig.CREATOR.createFromParcel(parcel)); + } +} diff --git a/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java new file mode 100644 index 0000000000000..e98b6ef2b3a68 --- /dev/null +++ b/tests/vcn/java/android/net/vcn/VcnGatewayConnectionConfigTest.java @@ -0,0 +1,143 @@ +/* + * 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 android.net.vcn; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import android.net.NetworkCapabilities; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class VcnGatewayConnectionConfigTest { + private static final int[] EXPOSED_CAPS = + new int[] { + NetworkCapabilities.NET_CAPABILITY_INTERNET, NetworkCapabilities.NET_CAPABILITY_MMS + }; + private static final int[] UNDERLYING_CAPS = new int[] {NetworkCapabilities.NET_CAPABILITY_DUN}; + private static final long[] RETRY_INTERVALS_MS = + new long[] { + TimeUnit.SECONDS.toMillis(5), + TimeUnit.SECONDS.toMillis(30), + TimeUnit.MINUTES.toMillis(1), + TimeUnit.MINUTES.toMillis(5), + TimeUnit.MINUTES.toMillis(15), + TimeUnit.MINUTES.toMillis(30) + }; + private static final int MAX_MTU = 1360; + + // Package protected for use in VcnConfigTest + static VcnGatewayConnectionConfig buildTestConfig() { + final VcnGatewayConnectionConfig.Builder builder = + new VcnGatewayConnectionConfig.Builder() + .setRetryInterval(RETRY_INTERVALS_MS) + .setMaxMtu(MAX_MTU); + + for (int caps : EXPOSED_CAPS) { + builder.addExposedCapability(caps); + } + + for (int caps : UNDERLYING_CAPS) { + builder.addRequiredUnderlyingCapability(caps); + } + + return builder.build(); + } + + @Test + public void testBuilderRequiresNonEmptyExposedCaps() { + try { + new VcnGatewayConnectionConfig.Builder() + .addRequiredUnderlyingCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + + fail("Expected exception due to invalid exposed capabilities"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testBuilderRequiresNonEmptyUnderlyingCaps() { + try { + new VcnGatewayConnectionConfig.Builder() + .addExposedCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build(); + + fail("Expected exception due to invalid required underlying capabilities"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testBuilderRequiresNonNullRetryInterval() { + try { + new VcnGatewayConnectionConfig.Builder().setRetryInterval(null); + fail("Expected exception due to invalid retryIntervalMs"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testBuilderRequiresNonEmptyRetryInterval() { + try { + new VcnGatewayConnectionConfig.Builder().setRetryInterval(new long[0]); + fail("Expected exception due to invalid retryIntervalMs"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testBuilderRequiresValidMtu() { + try { + new VcnGatewayConnectionConfig.Builder() + .setMaxMtu(VcnGatewayConnectionConfig.MIN_MTU_V6 - 1); + fail("Expected exception due to invalid mtu"); + } catch (IllegalArgumentException e) { + } + } + + @Test + public void testBuilderAndGetters() { + final VcnGatewayConnectionConfig config = buildTestConfig(); + + for (int cap : EXPOSED_CAPS) { + config.hasExposedCapability(cap); + } + for (int cap : UNDERLYING_CAPS) { + config.requiresUnderlyingCapability(cap); + } + + assertArrayEquals(RETRY_INTERVALS_MS, config.getRetryIntervalsMs()); + assertEquals(MAX_MTU, config.getMaxMtu()); + } + + @Test + public void testPersistableBundle() { + final VcnGatewayConnectionConfig config = buildTestConfig(); + + assertEquals(config, new VcnGatewayConnectionConfig(config.toPersistableBundle())); + } +} diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 633cf64bc2749..1cc953239fed3 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -16,6 +16,9 @@ package com.android.server; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; @@ -26,7 +29,9 @@ import static org.mockito.Mockito.verify; import android.content.Context; import android.net.ConnectivityManager; import android.net.vcn.VcnConfig; +import android.net.vcn.VcnConfigTest; import android.os.ParcelUuid; +import android.os.PersistableBundle; import android.os.Process; import android.os.UserHandle; import android.os.test.TestLooper; @@ -37,10 +42,14 @@ import android.telephony.TelephonyManager; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.server.vcn.util.PersistableBundleUtils; + import org.junit.Test; import org.junit.runner.RunWith; +import java.io.FileNotFoundException; import java.util.Collections; +import java.util.Map; import java.util.UUID; /** Tests for {@link VcnManagementService}. */ @@ -48,6 +57,11 @@ import java.util.UUID; @SmallTest public class VcnManagementServiceTest { private static final ParcelUuid TEST_UUID_1 = new ParcelUuid(new UUID(0, 0)); + private static final ParcelUuid TEST_UUID_2 = new ParcelUuid(new UUID(1, 1)); + private static final VcnConfig TEST_VCN_CONFIG = VcnConfigTest.buildTestConfig(); + private static final Map TEST_VCN_CONFIG_MAP = + Collections.unmodifiableMap(Collections.singletonMap(TEST_UUID_1, TEST_VCN_CONFIG)); + private static final SubscriptionInfo TEST_SUBSCRIPTION_INFO = new SubscriptionInfo( 1 /* id */, @@ -79,6 +93,8 @@ public class VcnManagementServiceTest { private final TelephonyManager mTelMgr = mock(TelephonyManager.class); private final SubscriptionManager mSubMgr = mock(SubscriptionManager.class); private final VcnManagementService mVcnMgmtSvc; + private final PersistableBundleUtils.LockingReadWriteHelper mConfigReadWriteHelper = + mock(PersistableBundleUtils.LockingReadWriteHelper.class); public VcnManagementServiceTest() throws Exception { setupSystemService(mConnMgr, Context.CONNECTIVITY_SERVICE, ConnectivityManager.class); @@ -88,6 +104,16 @@ public class VcnManagementServiceTest { doReturn(mTestLooper.getLooper()).when(mMockDeps).getLooper(); doReturn(Process.FIRST_APPLICATION_UID).when(mMockDeps).getBinderCallingUid(); + doReturn(mConfigReadWriteHelper) + .when(mMockDeps) + .newPersistableBundleLockingReadWriteHelper(any()); + + final PersistableBundle bundle = + PersistableBundleUtils.fromMap( + TEST_VCN_CONFIG_MAP, + PersistableBundleUtils::fromParcelUuid, + VcnConfig::toPersistableBundle); + doReturn(bundle).when(mConfigReadWriteHelper).readFromDisk(); setupMockedCarrierPrivilege(true); mVcnMgmtSvc = new VcnManagementService(mMockContext, mMockDeps); @@ -115,12 +141,42 @@ public class VcnManagementServiceTest { .registerNetworkProvider(any(VcnManagementService.VcnNetworkProvider.class)); } + @Test + public void testNonSystemServerRealConfigFileAccessPermission() throws Exception { + // Attempt to build a real instance of the dependencies, and verify we cannot write to the + // file. + VcnManagementService.Dependencies deps = new VcnManagementService.Dependencies(); + PersistableBundleUtils.LockingReadWriteHelper configReadWriteHelper = + deps.newPersistableBundleLockingReadWriteHelper( + VcnManagementService.VCN_CONFIG_FILE); + + // Even tests should not be able to read/write configs from disk; SELinux policies restrict + // it to only the system server. + // Reading config should always return null since the file "does not exist", and writing + // should throw an IOException. + assertNull(configReadWriteHelper.readFromDisk()); + + try { + configReadWriteHelper.writeToDisk(new PersistableBundle()); + fail("Expected IOException due to SELinux policy"); + } catch (FileNotFoundException expected) { + } + } + + @Test + public void testLoadVcnConfigsOnStartup() throws Exception { + mTestLooper.dispatchAll(); + + assertEquals(TEST_VCN_CONFIG_MAP, mVcnMgmtSvc.getConfigs()); + verify(mConfigReadWriteHelper).readFromDisk(); + } + @Test public void testSetVcnConfigRequiresNonSystemServer() throws Exception { doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid(); try { - mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build()); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig()); fail("Expected IllegalStateException exception for system server"); } catch (IllegalStateException expected) { } @@ -133,7 +189,7 @@ public class VcnManagementServiceTest { .getBinderCallingUid(); try { - mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build()); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig()); fail("Expected security exception for non system user"); } catch (SecurityException expected) { } @@ -144,12 +200,20 @@ public class VcnManagementServiceTest { setupMockedCarrierPrivilege(false); try { - mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, new VcnConfig.Builder().build()); + mVcnMgmtSvc.setVcnConfig(TEST_UUID_1, VcnConfigTest.buildTestConfig()); fail("Expected security exception for missing carrier privileges"); } catch (SecurityException expected) { } } + @Test + public void testSetVcnConfig() throws Exception { + // Use a different UUID to simulate a new VCN config. + mVcnMgmtSvc.setVcnConfig(TEST_UUID_2, TEST_VCN_CONFIG); + assertEquals(TEST_VCN_CONFIG, mVcnMgmtSvc.getConfigs().get(TEST_UUID_2)); + verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class)); + } + @Test public void testClearVcnConfigRequiresNonSystemServer() throws Exception { doReturn(Process.SYSTEM_UID).when(mMockDeps).getBinderCallingUid(); @@ -184,4 +248,11 @@ public class VcnManagementServiceTest { } catch (SecurityException expected) { } } + + @Test + public void testClearVcnConfig() throws Exception { + mVcnMgmtSvc.clearVcnConfig(TEST_UUID_1); + assertTrue(mVcnMgmtSvc.getConfigs().isEmpty()); + verify(mConfigReadWriteHelper).writeToDisk(any(PersistableBundle.class)); + } }