diff --git a/OWNERS_core_networking b/OWNERS_core_networking index 4b7363993d..492ebd6ecd 100644 --- a/OWNERS_core_networking +++ b/OWNERS_core_networking @@ -11,4 +11,4 @@ xiaom@google.com yuyanghuang@google.com martinwu@google.com #{LAST_RESORT_SUGGESTION} -satk@google.com #{LAST_RESORT_SUGGESTION} \ No newline at end of file +larsberg@google.com #{LAST_RESORT_SUGGESTION} \ No newline at end of file diff --git a/OWNERS_core_networking_xts b/OWNERS_core_networking_xts index 60ca885cec..294b6e617b 100644 --- a/OWNERS_core_networking_xts +++ b/OWNERS_core_networking_xts @@ -1,5 +1,5 @@ lorenzo@google.com -satk@google.com #{LAST_RESORT_SUGGESTION} +larsberg@google.com #{LAST_RESORT_SUGGESTION} # For cherry-picks of CLs that are already merged in aosp/master, flaky test # fixes, or no-op refactors. diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 68cbdc7d02..97a78cf4be 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -15,6 +15,3 @@ ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py --disabled-rules comment-w hidden_api_txt_checksorted_hook = ${REPO_ROOT}/tools/platform-compat/hiddenapi/checksorted_sha.sh ${PREUPLOAD_COMMIT} ${REPO_ROOT} python_hook = ${REPO_ROOT}/packages/modules/Connectivity/preupload_hook_script.py ${PREUPLOAD_FILES} - -# go/alint for details -alint_hook = ${REPO_ROOT}/vendor/google/tools/alint diff --git a/TEST_MAPPING b/TEST_MAPPING index 9f8868c3ef..9ec5c02fd7 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -83,6 +83,14 @@ "exclude-annotation": "com.android.testutils.NetworkStackModuleTest" } ] + }, + { + "name": "CtsHostsideNetworkTests", + "options": [ + { + "exclude-annotation": "androidx.test.filters.RequiresDevice" + } + ] } ], "captiveportal-networkstack-mainline-presubmit": [ @@ -319,14 +327,6 @@ } ] }, - { - "name": "CtsHostsideNetworkTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]", - "options": [ - { - "exclude-annotation": "androidx.test.filters.RequiresDevice" - } - ] - }, // Test with APK modules only, in cases where APEX is not supported, or the other modules // were simply not updated { @@ -435,12 +435,23 @@ "name": "CtsNetTestCasesUpdateStatsPermission" } ], - "tv-presubmit": [ + "hsum-presubmit": [ { "name": "CtsNetTestCases" }, { - "name": "CtsNativeNetTestCases" + "name": "CtsNativeNetDnsTestCases" + }, + { + "name": "CtsNetTestCasesInternetPermission" + }, + { + "name": "CtsNetTestCasesUpdateStatsPermission" + } + ], + "tv-presubmit": [ + { + "name": "CtsNetTestCases" }, { "name": "CtsNativeNetDnsTestCases" @@ -471,5 +482,18 @@ { "path": "vendor/xts/gts-tests/hostsidetests/networkstack" } + ], + "wear-cts-presubmit": [ + { + "name": "CtsNetTestCases", + "options": [ + { + "exclude-filter": "android.net.cts.NetworkValidationTest#testCapportApiCallbacks" + } + ] + }, + { + "name": "CtsNetTestCasesUpdateStatsPermission" + } ] } diff --git a/Tethering/Android.bp b/Tethering/Android.bp index 091849ba26..2ffae71763 100644 --- a/Tethering/Android.bp +++ b/Tethering/Android.bp @@ -22,7 +22,10 @@ package { java_defaults { name: "TetheringApiLevel", sdk_version: "module_current", - min_sdk_version: "30", + min_sdk_version: select(release_flag("RELEASE_DEPRECATE_MAINLINE_R_SUPPORT"), { + true: "31", + default: "30", + }), } java_defaults { @@ -65,6 +68,8 @@ java_defaults { "androidx.annotation_annotation", "connectivity-net-module-utils-bpf", "com.android.net.flags-aconfig-java", + "com.android.tethering.beta.test.flags-aconfig-java", + "com.android.tethering.mainline.beta.flags-aconfig-java", "modules-utils-build", "modules-utils-statemachine", "networkstack-client", diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp index 19e2c6f078..7329396163 100644 --- a/Tethering/apex/Android.bp +++ b/Tethering/apex/Android.bp @@ -67,6 +67,7 @@ apex { "libservice-connectivity", "libservice-thread-jni", "libandroid_net_connectivity_com_android_net_module_util_jni", + "libandroid_net_connectivity_com_android_net_module_util_jni_CommonConnectivityJni", ], native_shared_libs: [ "libcom.android.tethering.dns_helper", diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java index 9b6097d094..d53fc95725 100644 --- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java +++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java @@ -33,20 +33,17 @@ import androidx.annotation.Nullable; import com.android.net.module.util.IBpfMap; import com.android.net.module.util.IBpfMap.ThrowingBiConsumer; import com.android.net.module.util.SharedLog; +import com.android.net.module.util.Struct.S32; +import com.android.net.module.util.Struct.S64; import com.android.net.module.util.bpf.Tether4Key; import com.android.net.module.util.bpf.Tether4Value; -import com.android.net.module.util.bpf.TetherStatsKey; import com.android.net.module.util.bpf.TetherStatsValue; import com.android.networkstack.tethering.BpfCoordinator.Dependencies; import com.android.networkstack.tethering.BpfCoordinator.Ipv6DownstreamRule; import com.android.networkstack.tethering.BpfCoordinator.Ipv6UpstreamRule; import com.android.networkstack.tethering.BpfUtils; import com.android.networkstack.tethering.Tether6Value; -import com.android.networkstack.tethering.TetherDevKey; -import com.android.networkstack.tethering.TetherDevValue; import com.android.networkstack.tethering.TetherDownstream6Key; -import com.android.networkstack.tethering.TetherLimitKey; -import com.android.networkstack.tethering.TetherLimitValue; import com.android.networkstack.tethering.TetherUpstream6Key; import java.io.FileDescriptor; @@ -85,15 +82,15 @@ public class BpfCoordinatorShimImpl // BPF map of tethering statistics of the upstream interface since tethering startup. @Nullable - private final IBpfMap mBpfStatsMap; + private final IBpfMap mBpfStatsMap; // BPF map of per-interface quota for tethering offload. @Nullable - private final IBpfMap mBpfLimitMap; + private final IBpfMap mBpfLimitMap; // BPF map of interface index mapping for XDP. @Nullable - private final IBpfMap mBpfDevMap; + private final IBpfMap mBpfDevMap; // Tracking IPv4 rule count while any rule is using the given upstream interfaces. Used for // reducing the BPF map iteration query. The count is increased or decreased when the rule is @@ -252,7 +249,7 @@ public class BpfCoordinatorShimImpl try { // The reported tether stats are total data usage for all currently-active upstream // interfaces since tethering start. - mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.ifindex, value)); + mBpfStatsMap.forEach((key, value) -> tetherStatsList.put((int) key.val, value)); } catch (ErrnoException e) { mLog.e("Fail to fetch tethering stats from BPF map: ", e); return null; @@ -269,7 +266,7 @@ public class BpfCoordinatorShimImpl TetherStatsValue statsValue = null; try { - statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex)); + statsValue = mBpfStatsMap.getValue(new S32(ifIndex)); } catch (ErrnoException e) { // The BpfMap#getValue doesn't throw an errno ENOENT exception. Catch other error // while trying to get stats entry. @@ -287,7 +284,7 @@ public class BpfCoordinatorShimImpl // This function is the *only* thing that can create entries. // BpfMap#insertEntry use BPF_NOEXIST to create the entry. The entry is created // if and only if it doesn't exist. - mBpfStatsMap.insertEntry(new TetherStatsKey(ifIndex), new TetherStatsValue( + mBpfStatsMap.insertEntry(new S32(ifIndex), new TetherStatsValue( 0 /* rxPackets */, 0 /* rxBytes */, 0 /* rxErrors */, 0 /* txPackets */, 0 /* txBytes */, 0 /* txErrors */)); } catch (ErrnoException | IllegalArgumentException e) { @@ -305,7 +302,7 @@ public class BpfCoordinatorShimImpl if (newLimit < rxBytes + txBytes) newLimit = QUOTA_UNLIMITED; try { - mBpfLimitMap.updateEntry(new TetherLimitKey(ifIndex), new TetherLimitValue(newLimit)); + mBpfLimitMap.updateEntry(new S32(ifIndex), new S64(newLimit)); } catch (ErrnoException e) { mLog.e("Fail to set quota " + quotaBytes + " for interface index " + ifIndex + ": ", e); return false; @@ -331,7 +328,7 @@ public class BpfCoordinatorShimImpl TetherStatsValue statsValue = null; try { - statsValue = mBpfStatsMap.getValue(new TetherStatsKey(ifIndex)); + statsValue = mBpfStatsMap.getValue(new S32(ifIndex)); } catch (ErrnoException e) { mLog.e("Could not get stats entry for interface index " + ifIndex + ": ", e); return null; @@ -343,14 +340,14 @@ public class BpfCoordinatorShimImpl } try { - mBpfStatsMap.deleteEntry(new TetherStatsKey(ifIndex)); + mBpfStatsMap.deleteEntry(new S32(ifIndex)); } catch (ErrnoException e) { mLog.e("Could not delete stats entry for interface index " + ifIndex + ": ", e); return null; } try { - mBpfLimitMap.deleteEntry(new TetherLimitKey(ifIndex)); + mBpfLimitMap.deleteEntry(new S32(ifIndex)); } catch (ErrnoException e) { mLog.e("Could not delete limit for interface index " + ifIndex + ": ", e); return null; @@ -470,7 +467,7 @@ public class BpfCoordinatorShimImpl @Override public boolean addDevMap(int ifIndex) { try { - mBpfDevMap.updateEntry(new TetherDevKey(ifIndex), new TetherDevValue(ifIndex)); + mBpfDevMap.updateEntry(new S32(ifIndex), new S32(ifIndex)); } catch (ErrnoException e) { mLog.e("Could not add interface " + ifIndex + ": " + e); return false; @@ -481,7 +478,7 @@ public class BpfCoordinatorShimImpl @Override public boolean removeDevMap(int ifIndex) { try { - mBpfDevMap.deleteEntry(new TetherDevKey(ifIndex)); + mBpfDevMap.deleteEntry(new S32(ifIndex)); } catch (ErrnoException e) { mLog.e("Could not delete interface " + ifIndex + ": " + e); return false; diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp index d2a8c13dfe..be220ffaca 100644 --- a/Tethering/common/TetheringLib/Android.bp +++ b/Tethering/common/TetheringLib/Android.bp @@ -34,8 +34,9 @@ java_sdk_library { // Using for test only "//cts/tests/netlegacy22.api", - // TODO: b/374174952 Remove it when VCN CTS is moved to Connectivity/ + // TODO: b/374174952 Clean up VCN paths when the code location is finalized "//cts/tests/tests/vcn", + "//packages/modules/Connectivity/Vcn/tests:__subpackages__", "//external/sl4a:__subpackages__", "//frameworks/base/core/tests/bandwidthtests", diff --git a/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl b/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl index c3d66ee145..45245de760 100644 --- a/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl +++ b/Tethering/common/TetheringLib/src/android/net/IIntResultListener.aidl @@ -18,7 +18,7 @@ package android.net; /** * Listener interface allowing objects to listen to various module event. - * {@hide} + * @hide */ oneway interface IIntResultListener { void onResult(int resultCode); diff --git a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl index 43262fb9d1..1c273157d8 100644 --- a/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl +++ b/Tethering/common/TetheringLib/src/android/net/TetherStatesParcel.aidl @@ -20,7 +20,7 @@ import android.net.TetheringInterface; /** * Status details for tethering downstream interfaces. - * {@hide} + * @hide */ parcelable TetherStatesParcel { TetheringInterface[] availableList; diff --git a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java index a8a471dbc1..c37ee6c5f4 100644 --- a/Tethering/common/TetheringLib/src/android/net/TetheringManager.java +++ b/Tethering/common/TetheringLib/src/android/net/TetheringManager.java @@ -381,7 +381,7 @@ public class TetheringManager { * @param context Context for the manager. * @param connectorSupplier Supplier for the manager connector; may return null while the * service is not connected. - * {@hide} + * @hide */ @SystemApi(client = MODULE_LIBRARIES) public TetheringManager(@NonNull final Context context, @@ -574,7 +574,7 @@ public class TetheringManager { * already released. * 2. This is not synchronous, so it's not useful to use try-with-resources. * - * {@hide} + * @hide */ @SystemApi(client = MODULE_LIBRARIES) @SuppressLint("NotCloseable") @@ -588,7 +588,7 @@ public class TetheringManager { /** * Callback for requestTetheredInterface. * - * {@hide} + * @hide */ @SystemApi(client = MODULE_LIBRARIES) public interface TetheredInterfaceCallback { @@ -694,7 +694,7 @@ public class TetheringManager { * @param iface the interface name to tether. * @return error a {@code TETHER_ERROR} value indicating success or failure type * - * {@hide} + * @hide */ @Deprecated @SystemApi(client = MODULE_LIBRARIES) @@ -729,7 +729,7 @@ public class TetheringManager { * On SDK versions after {@link Build.VERSION_CODES.VANILLA_ICE_CREAM}, this will * throw an UnsupportedOperationException. * - * {@hide} + * @hide */ @Deprecated @SystemApi(client = MODULE_LIBRARIES) @@ -758,7 +758,7 @@ public class TetheringManager { * used and an entitlement check is needed, downstream USB tethering will be enabled but will * not have any upstream. * - * {@hide} + * @hide */ @Deprecated @SystemApi(client = MODULE_LIBRARIES) @@ -1553,7 +1553,7 @@ public class TetheringManager { * Helper function of #requestLatestTetheringEntitlementResult to remain backwards compatible * with ConnectivityManager#getLatestTetheringEntitlementResult * - * {@hide} + * @hide */ // TODO: improve the usage of ResultReceiver, b/145096122 @SystemApi(client = MODULE_LIBRARIES) diff --git a/Tethering/jni/commonconnectivity/onload.cpp b/Tethering/jni/commonconnectivity/onload.cpp new file mode 100644 index 0000000000..b1fb6d7d91 --- /dev/null +++ b/Tethering/jni/commonconnectivity/onload.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 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. + */ +#define LOG_TAG "com_android_networkstack_tethering_util_CommonConnectivityJni" + +#include +#include + +namespace android { +int register_CommonConnectivityJni(JNIEnv *env, char const *class_name); + +extern "C" jint JNI_OnLoad(JavaVM *vm, void *) { + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != + JNI_OK) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "ERROR: GetEnv failed"); + return JNI_ERR; + } + + if (register_CommonConnectivityJni(env, + "com/android/networkstack/tethering/util/CommonConnectivityJni") < 0) return JNI_ERR; + + return JNI_VERSION_1_6; +} +}; // namespace android \ No newline at end of file diff --git a/Tethering/jni/onload.cpp b/Tethering/jni/onload.cpp index fd40d411ab..606cc44392 100644 --- a/Tethering/jni/onload.cpp +++ b/Tethering/jni/onload.cpp @@ -23,6 +23,7 @@ namespace android { int register_com_android_net_module_util_BpfMap(JNIEnv* env, char const* class_name); +int register_com_android_net_module_util_BpfBitmap(JNIEnv* env, char const* class_name); int register_com_android_net_module_util_TcUtils(JNIEnv* env, char const* class_name); int register_com_android_networkstack_tethering_BpfCoordinator(JNIEnv* env); int register_com_android_networkstack_tethering_util_TetheringUtils(JNIEnv* env); @@ -39,6 +40,9 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void*) { if (register_com_android_net_module_util_BpfMap(env, "com/android/networkstack/tethering/util/BpfMap") < 0) return JNI_ERR; + if (register_com_android_net_module_util_BpfBitmap(env, + "com/android/networkstack/tethering/util/BpfBitmap") < 0) return JNI_ERR; + if (register_com_android_net_module_util_TcUtils(env, "com/android/networkstack/tethering/util/TcUtils") < 0) return JNI_ERR; diff --git a/Tethering/src/android/net/ip/IpServer.java b/Tethering/src/android/net/ip/IpServer.java index e8796b9647..783bff3622 100644 --- a/Tethering/src/android/net/ip/IpServer.java +++ b/Tethering/src/android/net/ip/IpServer.java @@ -39,11 +39,11 @@ import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS; import static android.net.util.NetworkConstants.asByte; import static android.system.OsConstants.RT_SCOPE_UNIVERSE; +import static com.android.net.module.util.ConnectivityCommonFlags.USE_ROUTE_PARCEL_IPCS; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH; import static com.android.networkstack.tethering.TetheringConfiguration.USE_SYNC_SM; -import static com.android.networkstack.tethering.TetheringFeatureFlags.TETHERING_LOCAL_NETWORK_AGENT; -import static com.android.networkstack.tethering.TetheringFeatureFlags.WIFIP2PGO_LOCAL_NETWORK_AGENT; +import static com.android.networkstack.tethering.TetheringFeatureFlags.TETHERING_AND_P2P_GO_LOCAL_AGENT; import static com.android.networkstack.tethering.util.PrefixUtils.asIpPrefix; import static com.android.networkstack.tethering.util.TetheringMessageBase.BASE_IPSERVER; import static com.android.networkstack.tethering.util.TetheringUtils.getTransportTypeForTetherableType; @@ -91,6 +91,7 @@ import com.android.net.module.util.IIpv4PrefixRequest; import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.NetdUtils; import com.android.net.module.util.RoutingCoordinatorManager; +import com.android.net.module.util.SdkUtil; import com.android.net.module.util.SharedLog; import com.android.net.module.util.SyncStateMachine.StateInfo; import com.android.net.module.util.ip.InterfaceController; @@ -100,6 +101,7 @@ import com.android.networkstack.tethering.metrics.TetheringMetrics; import com.android.networkstack.tethering.util.InterfaceSet; import com.android.networkstack.tethering.util.PrefixUtils; import com.android.networkstack.tethering.util.StateMachineShim; +import com.android.tethering.mainline.beta.Flags; import java.net.Inet4Address; import java.net.Inet6Address; @@ -218,10 +220,24 @@ public class IpServer extends StateMachineShim { DhcpServerCallbacks cb); /** - * @see DeviceConfigUtils#isTetheringFeatureEnabled + * Get whether a tethering feature flag is enabled via chickened out. + * @param name the name of the feature. */ - public boolean isFeatureEnabled(Context context, String name) { - return DeviceConfigUtils.isTetheringFeatureEnabled(context, name); + public boolean isTetheringFeatureNotChickenedOut(@NonNull Context context, + @NonNull String name) { + return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name); + } + + /** Get whether tethering and P2P GO local agent flag is enabled via beta flags. */ + public boolean isTetheringAndP2pGoLocalAgentBetaFlagEnabled() { + return Flags.tetheringAndP2pGoLocalAgent(); + } + + /** + * @see DeviceConfigUtils#isTetheringFeatureNotChickenedOut + */ + public boolean isFeatureNotChickenedOut(Context context, String name) { + return DeviceConfigUtils.isTetheringFeatureNotChickenedOut(context, name); } /** Create a NetworkAgent instance to be used by IpServer. */ @@ -288,6 +304,7 @@ public class IpServer extends StateMachineShim { private final boolean mUsingLegacyDhcp; private final int mP2pLeasesSubnetPrefixLength; private final boolean mIsWifiP2pDedicatedIpEnabled; + private final boolean mUseRouteParcel; private final Dependencies mDeps; @@ -335,8 +352,8 @@ public class IpServer extends StateMachineShim { private final Handler mHandler; private final Context mContext; - private final boolean mSupportTetheringLocalAgent; - private final boolean mSupportWifiP2pGroupOwnerLocalAgent; + // Whether to enable tethering and Wi-Fi P2p Group Owner mode local network agent. + private final boolean mSupportTetheringAndP2pGoLocalAgent; // This will be null if the TetheredState is not entered or feature not supported. // This will be only accessed from the IpServer handler thread. @@ -381,11 +398,21 @@ public class IpServer extends StateMachineShim { mLastError = TETHER_ERROR_NO_ERROR; mServingMode = STATE_AVAILABLE; - // Tethering network agent is supported on V+, and will be rolled out gradually. - mSupportTetheringLocalAgent = SdkLevel.isAtLeastV() - && mDeps.isFeatureEnabled(mContext, TETHERING_LOCAL_NETWORK_AGENT); - mSupportWifiP2pGroupOwnerLocalAgent = mSupportTetheringLocalAgent - && mDeps.isFeatureEnabled(mContext, WIFIP2PGO_LOCAL_NETWORK_AGENT); + // Determines support for the Tethering/P2P GO local network agent. This feature is + // gated on Android V+ because it requires NET_CAPABILITY_LOCAL_NETWORK. + // For 25Q4+, it is enabled by default with a kill switch to prevent impacting + // existing devices if issues arise. For older V+ devices, rollout is controlled + // by a mainline beta flag. + mSupportTetheringAndP2pGoLocalAgent = SdkLevel.isAtLeastV() + && (SdkUtil.isAtLeast25Q4() + ? mDeps.isTetheringFeatureNotChickenedOut(mContext, + TETHERING_AND_P2P_GO_LOCAL_AGENT) + : mDeps.isTetheringAndP2pGoLocalAgentBetaFlagEnabled()); + + // For post 25Q2 releases always use *RouteParcel methods. For 25Q2 or lower, decide based + // on kill-switch + mUseRouteParcel = SdkUtil.isAtLeast25Q4() + || mDeps.isFeatureNotChickenedOut(mContext, USE_ROUTE_PARCEL_IPCS); mInitialState = new InitialState(); mLocalHotspotState = new LocalHotspotState(); @@ -848,6 +875,7 @@ public class IpServer extends StateMachineShim { String upstreamIface = null; InterfaceParams upstreamIfaceParams = null; int upstreamIfIndex = NO_UPSTREAM; + int pmtu6 = 1400; // default 'safe-ish' value if (v6only != null) { upstreamIface = v6only.getInterfaceName(); @@ -856,7 +884,14 @@ public class IpServer extends StateMachineShim { upstreamIfIndex = upstreamIfaceParams.index; } params = new RaParams(); - params.mtu = v6only.getMtu(); + pmtu6 = v6only.getMtu(); + if (pmtu6 < 1280) pmtu6 = 1400; // we simply don't know what it is, 1400 is safe-ish + for (RouteInfo route : v6only.getRoutes()) { + if (route.getMtu() >= 1280) pmtu6 = Math.min(pmtu6, route.getMtu()); + } + // Clamp v6 MTU to 1280-1500 range. + if (pmtu6 > 1500) pmtu6 = 1500; + params.mtu = pmtu6; params.hasDefaultRoute = v6only.hasIpv6DefaultRoute(); if (params.hasDefaultRoute) params.hopLimit = getHopLimit(upstreamIface, ttlAdjustment); @@ -874,6 +909,8 @@ public class IpServer extends StateMachineShim { // CMD_TETHER_CONNECTION_CHANGED. Adding the mapping update here to the avoid potential // timing issue. It prevents that the IPv6 capability is updated later than // CMD_TETHER_CONNECTION_CHANGED. + // + // TODO: I believe we need to push v6mtu into this call. mBpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfIndex, upstreamIface); // If v6only is null, we pass in null to setRaParams(), which handles @@ -886,7 +923,7 @@ public class IpServer extends StateMachineShim { // mLastIPv6UpstreamIfindex and mLastIPv6UpstreamPrefixes because BpfCoordinator will call // IpServer#getIpv6UpstreamIfindex and IpServer#getIpv6UpstreamPrefixes to retrieve current // upstream interface index and prefixes when handling upstream changes. - mBpfCoordinator.updateIpv6UpstreamInterface(this, upstreamIfIndex, upstreamPrefixes); + mBpfCoordinator.updateIpv6UpstreamInterface(this, upstreamIfIndex, upstreamPrefixes, pmtu6); mLastIPv6LinkProperties = v6only; mLastIPv6UpstreamIfindex = upstreamIfIndex; mLastIPv6UpstreamPrefixes = upstreamPrefixes; @@ -896,7 +933,8 @@ public class IpServer extends StateMachineShim { } private void removeRoutesFromNetwork(int netId, @NonNull final List toBeRemoved) { - final int removalFailures = NetdUtils.removeRoutesFromNetwork(mNetd, netId, toBeRemoved); + final int removalFailures = NetdUtils.removeRoutesFromNetwork(mNetd, netId, toBeRemoved, + mUseRouteParcel); if (removalFailures > 0) { mLog.e("Failed to remove " + removalFailures + " IPv6 routes from network " + netId + "."); @@ -927,15 +965,14 @@ public class IpServer extends StateMachineShim { } } - private void addRoutesToNetwork(int netId, - @NonNull final List toBeAdded) { + private void addRoutesToNetwork(int netId, @NonNull final List toBeAdded) { // It's safe to call addInterfaceToNetwork() even if // the interface is already in the network. addInterfaceToNetwork(netId, mIfaceName); try { // Add routes from local network. Note that adding routes that // already exist does not cause an error (EEXIST is silently ignored). - NetdUtils.addRoutesToNetwork(mNetd, netId, mIfaceName, toBeAdded); + NetdUtils.addRoutesToNetwork(mNetd, netId, mIfaceName, toBeAdded, mUseRouteParcel); } catch (IllegalStateException e) { mLog.e("Failed to add IPv4/v6 routes to local table: " + e); return; @@ -949,6 +986,7 @@ public class IpServer extends StateMachineShim { final List routesToBeRemoved = getLocalRoutesFor(mIfaceName, deprecatedPrefixes); if (mTetheringAgent == null) { + // Routes for local network are not considered local routes. removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved); } for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route); @@ -1166,8 +1204,7 @@ public class IpServer extends StateMachineShim { @SuppressLint("NewApi") private void startServingInterface() { - if (mSupportTetheringLocalAgent && (mSupportWifiP2pGroupOwnerLocalAgent - || getScope() != CONNECTIVITY_SCOPE_LOCAL)) { + if (mSupportTetheringAndP2pGoLocalAgent) { try { mTetheringAgent = mDeps.makeNetworkAgent(mContext, Looper.myLooper(), TAG, mInterfaceType, mLinkProperties); @@ -1197,10 +1234,11 @@ public class IpServer extends StateMachineShim { 20 /* maxAttempts */, 50 /* pollingIntervalMs */); // Activate a route to dest and IPv6 link local. NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID, - new RouteInfo(asIpPrefix(mIpv4Address), null, mIfaceName, RTN_UNICAST)); + new RouteInfo(asIpPrefix(mIpv4Address), null, mIfaceName, + RTN_UNICAST), mUseRouteParcel); NetdUtils.modifyRoute(mNetd, NetdUtils.ModifyOperation.ADD, LOCAL_NET_ID, new RouteInfo(new IpPrefix("fe80::/64"), null, mIfaceName, - RTN_UNICAST)); + RTN_UNICAST), mUseRouteParcel); } } catch (RemoteException | ServiceSpecificException | IllegalStateException e) { mLog.e("Error Tethering", e); @@ -1330,6 +1368,7 @@ public class IpServer extends StateMachineShim { final List routesToBeRemoved = List.of(getDirectConnectedRoute(deprecatedLinkAddress)); if (mTetheringAgent == null) { + // Routes for local network are not considered local routes. removeRoutesFromNetwork(LOCAL_NET_ID, routesToBeRemoved); } for (RouteInfo route : routesToBeRemoved) mLinkProperties.removeRoute(route); @@ -1440,7 +1479,7 @@ public class IpServer extends StateMachineShim { for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname); mUpstreamIfaceSet = null; mBpfCoordinator.updateIpv6UpstreamInterface(IpServer.this, NO_UPSTREAM, - Collections.emptySet()); + Collections.emptySet(), 1400); } private void cleanupUpstreamInterface(String upstreamIface) { diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java index 180791e813..1a3328a292 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java @@ -74,9 +74,9 @@ import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.NetworkStackConstants; import com.android.net.module.util.SharedLog; import com.android.net.module.util.Struct.S32; +import com.android.net.module.util.Struct.S64; import com.android.net.module.util.bpf.Tether4Key; import com.android.net.module.util.bpf.Tether4Value; -import com.android.net.module.util.bpf.TetherStatsKey; import com.android.net.module.util.bpf.TetherStatsValue; import com.android.net.module.util.ip.ConntrackMonitor; import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer; @@ -448,11 +448,11 @@ public class BpfCoordinator { } /** Get stats BPF map. */ - @Nullable public IBpfMap getBpfStatsMap() { + @Nullable public IBpfMap getBpfStatsMap() { if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_STATS_MAP_PATH, - TetherStatsKey.class, TetherStatsValue.class); + S32.class, TetherStatsValue.class); } catch (ErrnoException e) { Log.e(TAG, "Cannot create stats map: " + e); return null; @@ -460,11 +460,11 @@ public class BpfCoordinator { } /** Get limit BPF map. */ - @Nullable public IBpfMap getBpfLimitMap() { + @Nullable public IBpfMap getBpfLimitMap() { if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_LIMIT_MAP_PATH, - TetherLimitKey.class, TetherLimitValue.class); + S32.class, S64.class); } catch (ErrnoException e) { Log.e(TAG, "Cannot create limit map: " + e); return null; @@ -472,11 +472,11 @@ public class BpfCoordinator { } /** Get dev BPF map. */ - @Nullable public IBpfMap getBpfDevMap() { + @Nullable public IBpfMap getBpfDevMap() { if (!isAtLeastS()) return null; try { return new BpfMap<>(TETHER_DEV_MAP_PATH, - TetherDevKey.class, TetherDevValue.class); + S32.class, S32.class); } catch (ErrnoException e) { Log.e(TAG, "Cannot create dev map: " + e); return null; @@ -501,6 +501,12 @@ public class BpfCoordinator { lastMaxSessionCount); } + /** Send a BpfCoordinatorShimInitError event. */ + public void sendBpfCoordinatorShimInitError() { + ConnectivityStatsLog.write(ConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED, + ConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_BPF_COORDINATOR_SHIM_INIT_ERROR); + } + /** * @see DeviceConfigUtils#isTetheringFeatureEnabled */ @@ -542,6 +548,7 @@ public class BpfCoordinator { mBpfCoordinatorShim = BpfCoordinatorShim.getBpfCoordinatorShim(deps); if (!mBpfCoordinatorShim.isInitialized()) { mLog.e("Bpf shim not initialized"); + mDeps.sendBpfCoordinatorShimInitError(); } // BPF IPv4 forwarding only supports on S+. @@ -790,7 +797,7 @@ public class BpfCoordinator { */ private void updateAllIpv6Rules(@NonNull final IpServer ipServer, final InterfaceParams interfaceParams, int newUpstreamIfindex, - @NonNull final Set newUpstreamPrefixes) { + @NonNull final Set newUpstreamPrefixes, int pmtu) { if (!isUsingBpf()) return; // Remove IPv6 downstream rules. Remove the old ones before adding the new rules, otherwise @@ -816,7 +823,7 @@ public class BpfCoordinator { for (final IpPrefix ipPrefix : newUpstreamPrefixes) { addIpv6UpstreamRule(ipServer, new Ipv6UpstreamRule( newUpstreamIfindex, interfaceParams.index, ipPrefix, - interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS)); + interfaceParams.macAddr, NULL_MAC_ADDRESS, NULL_MAC_ADDRESS, pmtu)); } } @@ -958,7 +965,7 @@ public class BpfCoordinator { * Note that this can be only called on handler thread. */ public void updateIpv6UpstreamInterface(@NonNull final IpServer ipServer, int upstreamIfindex, - @NonNull Set upstreamPrefixes) { + @NonNull Set upstreamPrefixes, int pmtu) { if (!isUsingBpf()) return; // If the upstream interface has changed, remove all rules and re-add them with the new @@ -971,7 +978,7 @@ public class BpfCoordinator { final boolean upstreamSupportsBpf = checkUpstreamSupportsBpf(upstreamIfindex); updateAllIpv6Rules(ipServer, interfaceParams, getInterfaceIndexForRule(upstreamIfindex, upstreamSupportsBpf), - upstreamPrefixes); + upstreamPrefixes, pmtu); } } @@ -1059,12 +1066,12 @@ public class BpfCoordinator { // Find the rules which are related with the given client. mBpfCoordinatorShim.tetherOffloadRuleForEach(UPSTREAM, (k, v) -> { - if (Arrays.equals(k.src4, clientAddr.getAddress())) { + if (Objects.equals(k.src4, clientAddr)) { deleteUpstreamRuleKeys.add(k); } }); mBpfCoordinatorShim.tetherOffloadRuleForEach(DOWNSTREAM, (k, v) -> { - if (Arrays.equals(v.dst46, toIpv4MappedAddressBytes(clientAddr))) { + if (Objects.equals(v.dst46, clientAddr)) { deleteDownstreamRuleKeys.add(k); upstreamIndiceSet.add((int) k.iif); } @@ -1370,7 +1377,7 @@ public class BpfCoordinator { } } private void dumpBpfStats(@NonNull IndentingPrintWriter pw) { - try (IBpfMap map = mDeps.getBpfStatsMap()) { + try (IBpfMap map = mDeps.getBpfStatsMap()) { if (map == null) { pw.println("No BPF stats map"); return; @@ -1439,9 +1446,10 @@ public class BpfCoordinator { } private String ipv6UpstreamRuleToString(TetherUpstream6Key key, Tether6Value value) { - return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s]", + return String.format("%d(%s) [%s] [%s] -> %d(%s) %04x [%s] [%s] %d", key.iif, getIfName(key.iif), key.dstMac, bytesToPrefix(key.src64), value.oif, - getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac); + getIfName(value.oif), value.ethProto, value.ethSrcMac, value.ethDstMac, + value.pmtu); } private void dumpIpv6UpstreamRules(IndentingPrintWriter pw) { @@ -1467,9 +1475,9 @@ public class BpfCoordinator { } catch (UnknownHostException impossible) { throw new AssertionError("IP address array not valid IPv6 address!"); } - return String.format("%d(%s) [%s] %s -> %d(%s) %04x [%s] [%s]", + return String.format("%d(%s) [%s] %s -> %d(%s) %04x [%s] [%s] %d", key.iif, getIfName(key.iif), key.dstMac, neigh6, value.oif, getIfName(value.oif), - value.ethProto, value.ethSrcMac, value.ethDstMac); + value.ethProto, value.ethSrcMac, value.ethDstMac, value.pmtu); } private void dumpIpv6DownstreamRules(IndentingPrintWriter pw) { @@ -1492,13 +1500,13 @@ public class BpfCoordinator { // duplicate bpf map dump code. private void dumpBpfForwardingRulesIpv6(IndentingPrintWriter pw) { pw.println("IPv6 Upstream: iif(iface) [inDstMac] [sourcePrefix] -> oif(iface) etherType " - + "[outSrcMac] [outDstMac]"); + + "[outSrcMac] [outDstMac] pmtu"); pw.increaseIndent(); dumpIpv6UpstreamRules(pw); pw.decreaseIndent(); pw.println("IPv6 Downstream: iif(iface) [inDstMac] neigh6 -> oif(iface) etherType " - + "[outSrcMac] [outDstMac]"); + + "[outSrcMac] [outDstMac] pmtu"); pw.increaseIndent(); dumpIpv6DownstreamRules(pw); pw.decreaseIndent(); @@ -1522,7 +1530,7 @@ public class BpfCoordinator { // expected argument order. // TODO: dump downstream4 map. if (CollectionUtils.contains(args, DUMPSYS_RAWMAP_ARG_STATS)) { - try (IBpfMap statsMap = mDeps.getBpfStatsMap()) { + try (IBpfMap statsMap = mDeps.getBpfStatsMap()) { BpfDump.dumpRawMap(statsMap, pw); } catch (IOException e) { pw.println("Error dumping stats map: " + e); @@ -1552,20 +1560,15 @@ public class BpfCoordinator { Tether4Key key, Tether4Value value) { final String src4, public4, dst4; final int publicPort; - try { - src4 = InetAddress.getByAddress(key.src4).getHostAddress(); - if (downstream) { - public4 = InetAddress.getByAddress(key.dst4).getHostAddress(); - publicPort = key.dstPort; - } else { - public4 = InetAddress.getByAddress(value.src46).getHostAddress(); - publicPort = value.srcPort; - } - dst4 = InetAddress.getByAddress(value.dst46).getHostAddress(); - } catch (UnknownHostException impossible) { - throw new AssertionError("IP address array not valid IPv4 address!"); + src4 = key.src4.getHostAddress(); + if (downstream) { + public4 = key.dst4.getHostAddress(); + publicPort = key.dstPort; + } else { + public4 = value.src46.getHostAddress(); + publicPort = value.srcPort; } - + dst4 = value.dst46.getHostAddress(); final String ageStr = (value.lastUsed == 0) ? "-" : String.format("%dms", (now - value.lastUsed) / 1_000_000); return String.format("%s [%s] %d(%s) %s:%d -> %d(%s) %s:%d -> %s:%d [%s] %d %s", @@ -1636,7 +1639,7 @@ public class BpfCoordinator { } private void dumpDevmap(@NonNull IndentingPrintWriter pw) { - try (IBpfMap map = mDeps.getBpfDevMap()) { + try (IBpfMap map = mDeps.getBpfDevMap()) { if (map == null) { pw.println("No devmap support"); return; @@ -1651,8 +1654,8 @@ public class BpfCoordinator { // Only get upstream interface name. Just do the best to make the index readable. // TODO: get downstream interface name because the index is either upstream or // downstream interface in dev map. - pw.println(String.format("%d (%s) -> %d (%s)", k.ifIndex, getIfName(k.ifIndex), - v.ifIndex, getIfName(v.ifIndex))); + pw.println(String.format("%d (%s) -> %d (%s)", k.val, getIfName(k.val), + v.val, getIfName(v.val))); }); } catch (ErrnoException | IOException e) { pw.println("Error dumping dev map: " + e); @@ -1692,7 +1695,7 @@ public class BpfCoordinator { // |field |oif |ethDst|ethSrc|ethPro|pmtu | // | | |mac |mac |to | | // +------+------+------+------+------+------+ - // |value |upstre|-- |-- |ETH_P_|1500 | + // |value |upstre|-- |-- |ETH_P_|pmtu | // | |am | | |IP | | // +------+------+------+------+------+------+ // @@ -1706,16 +1709,19 @@ public class BpfCoordinator { public final MacAddress outSrcMac; @NonNull public final MacAddress outDstMac; + public final int pmtu; public Ipv6UpstreamRule(int upstreamIfindex, int downstreamIfindex, @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac, - @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac) { + @NonNull MacAddress outSrcMac, @NonNull MacAddress outDstMac, + int pmtu) { this.upstreamIfindex = upstreamIfindex; this.downstreamIfindex = downstreamIfindex; this.sourcePrefix = sourcePrefix; this.inDstMac = inDstMac; this.outSrcMac = outSrcMac; this.outDstMac = outDstMac; + this.pmtu = pmtu; } /** @@ -1732,8 +1738,7 @@ public class BpfCoordinator { */ @NonNull public Tether6Value makeTether6Value() { - return new Tether6Value(upstreamIfindex, outDstMac, outSrcMac, ETH_P_IPV6, - NetworkStackConstants.ETHER_MTU); + return new Tether6Value(upstreamIfindex, outDstMac, outSrcMac, ETH_P_IPV6, pmtu); } @Override @@ -2032,20 +2037,6 @@ public class BpfCoordinator { return null; } - @NonNull - @VisibleForTesting - static byte[] toIpv4MappedAddressBytes(Inet4Address ia4) { - final byte[] addr4 = ia4.getAddress(); - final byte[] addr6 = new byte[16]; - addr6[10] = (byte) 0xff; - addr6[11] = (byte) 0xff; - addr6[12] = addr4[0]; - addr6[13] = addr4[1]; - addr6[14] = addr4[2]; - addr6[15] = addr4[3]; - return addr6; - } - // TODO: parse CTA_PROTOINFO of conntrack event in ConntrackMonitor. For TCP, only add rules // while TCP status is established. @VisibleForTesting @@ -2111,16 +2102,16 @@ public class BpfCoordinator { private Tether4Key makeTetherUpstream4Key( @NonNull ConntrackEvent e, @NonNull ClientInfo c) { return new Tether4Key(c.downstreamIfindex, c.downstreamMac, - e.tupleOrig.protoNum, e.tupleOrig.srcIp.getAddress(), - e.tupleOrig.dstIp.getAddress(), e.tupleOrig.srcPort, e.tupleOrig.dstPort); + e.tupleOrig.protoNum, e.tupleOrig.srcIp, e.tupleOrig.dstIp, + e.tupleOrig.srcPort, e.tupleOrig.dstPort); } @NonNull private Tether4Key makeTetherDownstream4Key( @NonNull ConntrackEvent e, @NonNull ClientInfo c, int upstreamIndex) { return new Tether4Key(upstreamIndex, NULL_MAC_ADDRESS /* dstMac (rawip) */, - e.tupleReply.protoNum, e.tupleReply.srcIp.getAddress(), - e.tupleReply.dstIp.getAddress(), e.tupleReply.srcPort, e.tupleReply.dstPort); + e.tupleReply.protoNum, e.tupleReply.srcIp, e.tupleReply.dstIp, + e.tupleReply.srcPort, e.tupleReply.dstPort); } @NonNull @@ -2129,8 +2120,7 @@ public class BpfCoordinator { return new Tether4Value(upstreamInfo.ifIndex, NULL_MAC_ADDRESS /* ethDstMac (rawip) */, NULL_MAC_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP, - upstreamInfo.mtu, toIpv4MappedAddressBytes(e.tupleReply.dstIp), - toIpv4MappedAddressBytes(e.tupleReply.srcIp), e.tupleReply.dstPort, + upstreamInfo.mtu, e.tupleReply.dstIp, e.tupleReply.srcIp, e.tupleReply.dstPort, e.tupleReply.srcPort, 0 /* lastUsed, filled by bpf prog only */); } @@ -2139,8 +2129,7 @@ public class BpfCoordinator { @NonNull ClientInfo c, @NonNull UpstreamInfo upstreamInfo) { return new Tether4Value(c.downstreamIfindex, c.clientMac, c.downstreamMac, ETH_P_IP, upstreamInfo.mtu, - toIpv4MappedAddressBytes(e.tupleOrig.dstIp), - toIpv4MappedAddressBytes(e.tupleOrig.srcIp), + e.tupleOrig.dstIp, e.tupleOrig.srcIp, e.tupleOrig.dstPort, e.tupleOrig.srcPort, 0 /* lastUsed, filled by bpf prog only */); } @@ -2465,17 +2454,6 @@ public class BpfCoordinator { return Math.max(DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS, configInterval); } - @Nullable - private Inet4Address parseIPv4Address(byte[] addrBytes) { - try { - final InetAddress ia = Inet4Address.getByAddress(addrBytes); - if (ia instanceof Inet4Address) return (Inet4Address) ia; - } catch (UnknownHostException e) { - mLog.e("Failed to parse IPv4 address: " + e); - } - return null; - } - // Update CTA_TUPLE_ORIG timeout for a given conntrack entry. Note that there will also be // coming a conntrack event to notify updated timeout. private void updateConntrackTimeout(byte proto, Inet4Address src4, short srcPort, @@ -2532,9 +2510,8 @@ public class BpfCoordinator { // both directions for TCP. mBpfCoordinatorShim.tetherOffloadRuleForEach(UPSTREAM, (k, v) -> { if ((now - v.lastUsed) / 1_000_000 < CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS) { - updateConntrackTimeout((byte) k.l4proto, - parseIPv4Address(k.src4), (short) k.srcPort, - parseIPv4Address(k.dst4), (short) k.dstPort); + updateConntrackTimeout((byte) k.l4proto, k.src4, (short) k.srcPort, k.dst4, + (short) k.dstPort); } }); @@ -2543,9 +2520,8 @@ public class BpfCoordinator { // which is opposite direction for downstream map value. mBpfCoordinatorShim.tetherOffloadRuleForEach(DOWNSTREAM, (k, v) -> { if ((now - v.lastUsed) / 1_000_000 < CONNTRACK_TIMEOUT_UPDATE_INTERVAL_MS) { - updateConntrackTimeout((byte) k.l4proto, - parseIPv4Address(v.dst46), (short) v.dstPort, - parseIPv4Address(v.src46), (short) v.srcPort); + updateConntrackTimeout((byte) k.l4proto, (Inet4Address) v.dst46, (short) v.dstPort, + (Inet4Address) v.src46, (short) v.srcPort); } }); } diff --git a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java index 12a0c96f0b..8fa3d640b3 100644 --- a/Tethering/src/com/android/networkstack/tethering/BpfUtils.java +++ b/Tethering/src/com/android/networkstack/tethering/BpfUtils.java @@ -30,7 +30,7 @@ import java.io.IOException; /** * The classes and the methods for BPF utilization. * - * {@hide} + * @hide */ public class BpfUtils { static { diff --git a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java index 900b505f0f..005b40571f 100644 --- a/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java +++ b/Tethering/src/com/android/networkstack/tethering/EntitlementManager.java @@ -32,6 +32,7 @@ import static android.net.TetheringManager.TETHERING_WIFI; import static android.net.TetheringManager.TETHER_ERROR_ENTITLEMENT_UNKNOWN; import static android.net.TetheringManager.TETHER_ERROR_NO_ERROR; import static android.net.TetheringManager.TETHER_ERROR_PROVISIONING_FAILED; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import static com.android.networkstack.apishim.ConstantsShim.ACTION_TETHER_UNSUPPORTED_CARRIER_UI; @@ -448,7 +449,8 @@ public class EntitlementManager { return TETHERING_PROVISIONING_CARRIER_UNSUPPORT; } - if (!config.isCarrierConfigAffirmsEntitlementCheckRequired) { + if (!config.isCarrierConfigAffirmsEntitlementCheckRequired + || INVALID_SUBSCRIPTION_ID == config.activeDataSubId) { return TETHERING_PROVISIONING_NOT_REQUIRED; } return (config.provisioningApp.length == 2) diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java b/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java deleted file mode 100644 index 00dfcc6718..0000000000 --- a/Tethering/src/com/android/networkstack/tethering/TetherLimitValue.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.networkstack.tethering; - -import com.android.net.module.util.Struct; -import com.android.net.module.util.Struct.Field; -import com.android.net.module.util.Struct.Type; - -/** The value of BpfMap which is used for tethering per-interface limit. */ -public class TetherLimitValue extends Struct { - // Use the signed long variable to store the int64 limit on limit BPF map. - // S64 is enough for each interface limit even at 5Gbps for ~468 years. - // 2^63 / (5 * 1000 * 1000 * 1000) * 8 / 86400 / 365 = 468. - // Note that QUOTA_UNLIMITED (-1) indicates there is no limit. - @Field(order = 0, type = Type.S64) - public final long limit; - - public TetherLimitValue(final long limit) { - this.limit = limit; - } -} diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java index 00a7f09079..26f3a6b588 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java +++ b/Tethering/src/com/android/networkstack/tethering/TetheringDependencies.java @@ -138,9 +138,8 @@ public abstract class TetheringDependencies { IBinder binder; if (!SdkLevel.isAtLeastS()) { final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); - binder = - new RoutingCoordinatorService( - getINetd(context, log), cm::getAllNetworks, context); + binder = new RoutingCoordinatorService(getINetd(context, log), cm::getAllNetworks, + context, false /* bluetoothTetheringUseRandomAddress */); } else { binder = ConnectivityInternalApiUtil.getRoutingCoordinator(context); } diff --git a/Tethering/src/com/android/networkstack/tethering/TetheringFeatureFlags.java b/Tethering/src/com/android/networkstack/tethering/TetheringFeatureFlags.java index fd4aa510ea..62a384246c 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetheringFeatureFlags.java +++ b/Tethering/src/com/android/networkstack/tethering/TetheringFeatureFlags.java @@ -61,15 +61,8 @@ public class TetheringFeatureFlags { public static final String TETHER_ACTIVE_SESSIONS_METRICS = "tether_active_sessions_metrics"; /** - * A feature flag to control whether the tethering local network agent should be enabled. - * Disabled by default. + * A kill switch to control whether the TETHERING_AND_P2P_GO_LOCAL_AGENT is enabled. */ - public static final String TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent"; - - /** - * A feature flag to control whether the Wifi P2P Group Owner local network agent - * should be enabled. Disabled by default. This depends on TETHERING_LOCAL_AGENT - * and will not be enabled if that flag is off. - */ - public static final String WIFIP2PGO_LOCAL_NETWORK_AGENT = "wifip2pgo_local_network_agent"; + public static final String TETHERING_AND_P2P_GO_LOCAL_AGENT = + "tethering_and_p2p_go_local_agent"; } diff --git a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java index 9392ae8d53..39e22cd0c4 100644 --- a/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java +++ b/Tethering/src/com/android/networkstack/tethering/util/TetheringUtils.java @@ -48,7 +48,7 @@ import java.util.Arrays; /** * The classes and the methods for tethering utilization. * - * {@hide} + * @hide */ public class TetheringUtils { static { diff --git a/Tethering/tests/integration/Android.bp b/Tethering/tests/integration/Android.bp index 221154638b..51377bb792 100644 --- a/Tethering/tests/integration/Android.bp +++ b/Tethering/tests/integration/Android.bp @@ -28,6 +28,7 @@ java_defaults { static_libs: [ "DhcpPacketLib", "androidx.test.rules", + "com.android.tethering.mainline.beta.flags-aconfig-java", "cts-net-utils", "mockito-target-minus-junit4", "net-tests-utils", diff --git a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java index 14795b7320..def44c2017 100644 --- a/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java +++ b/Tethering/tests/integration/src/android/net/EthernetTetheringTest.java @@ -32,7 +32,6 @@ import static android.net.TetheringTester.buildUdpPacket; import static android.net.TetheringTester.isExpectedIcmpPacket; import static android.net.TetheringTester.isExpectedUdpDnsPacket; import static android.net.connectivity.ConnectivityCompatChanges.ENABLE_MATCH_NON_THREAD_LOCAL_NETWORKS; -import static android.provider.DeviceConfig.NAMESPACE_TETHERING; import static android.system.OsConstants.ICMP_ECHO; import static android.system.OsConstants.ICMP_ECHOREPLY; import static android.system.OsConstants.IPPROTO_UDP; @@ -63,6 +62,9 @@ import android.os.Build; import android.os.SystemClock; import android.os.SystemProperties; import android.os.VintfRuntimeInfo; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Log; import android.util.Pair; @@ -75,26 +77,26 @@ import com.android.modules.utils.build.SdkLevel; import com.android.net.module.util.BpfDump; import com.android.net.module.util.Ipv6Utils; import com.android.net.module.util.Struct; +import com.android.net.module.util.Struct.S32; import com.android.net.module.util.bpf.ClatEgress4Key; import com.android.net.module.util.bpf.ClatEgress4Value; import com.android.net.module.util.bpf.ClatIngress6Key; import com.android.net.module.util.bpf.ClatIngress6Value; import com.android.net.module.util.bpf.Tether4Key; import com.android.net.module.util.bpf.Tether4Value; -import com.android.net.module.util.bpf.TetherStatsKey; import com.android.net.module.util.bpf.TetherStatsValue; import com.android.net.module.util.structs.Ipv4Header; import com.android.net.module.util.structs.UdpHeader; import com.android.testutils.AutoReleaseNetworkCallbackRule; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; -import com.android.testutils.DeviceConfigRule; import com.android.testutils.DeviceInfoUtils; import com.android.testutils.DumpTestUtils; import com.android.testutils.NetworkStackModuleTest; import com.android.testutils.PollPacketReader; import com.android.testutils.TestableNetworkCallback; import com.android.testutils.TestableNetworkCallback.Event; +import com.android.tethering.mainline.beta.Flags; import org.junit.After; import org.junit.Rule; @@ -122,9 +124,8 @@ import java.util.concurrent.TimeoutException; public class EthernetTetheringTest extends EthernetTetheringTestBase { @Rule public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule(); - // For manipulating feature flag before and after testing. @Rule - public final DeviceConfigRule mDeviceConfigRule = new DeviceConfigRule(); + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Rule public final AutoReleaseNetworkCallbackRule mNetworkCallbackRule = new AutoReleaseNetworkCallbackRule(); @@ -217,10 +218,6 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { (byte) 0x01, (byte) 0x02, (byte) 0x03, (byte) 0x04 /* Address: 1.2.3.4 */ }; - // Shamelessly copied from TetheringConfiguration. - private static final String TETHERING_LOCAL_NETWORK_AGENT = "tethering_local_network_agent"; - private static final String WIFIP2PGO_LOCAL_NETWORK_AGENT = "wifip2pgo_local_network_agent"; - @After public void tearDown() throws Exception { super.tearDown(); @@ -1023,17 +1020,15 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { final Tether4Key upstream4Key = rule.getKey(); assertEquals(IPPROTO_UDP, upstream4Key.l4proto); - assertTrue(Arrays.equals(tethered.ipv4Addr.getAddress(), upstream4Key.src4)); + assertEquals(tethered.ipv4Addr, upstream4Key.src4); assertEquals(LOCAL_PORT, upstream4Key.srcPort); - assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), upstream4Key.dst4)); + assertEquals(REMOTE_IP4_ADDR, upstream4Key.dst4); assertEquals(REMOTE_PORT, upstream4Key.dstPort); final Tether4Value upstream4Value = rule.getValue(); - assertTrue(Arrays.equals(tetheringUpstreamIp.getAddress(), - InetAddress.getByAddress(upstream4Value.src46).getAddress())); + assertEquals(tetheringUpstreamIp, upstream4Value.src46); assertEquals(LOCAL_PORT, upstream4Value.srcPort); - assertTrue(Arrays.equals(REMOTE_IP4_ADDR.getAddress(), - InetAddress.getByAddress(upstream4Value.dst46).getAddress())); + assertEquals(REMOTE_IP4_ADDR, upstream4Value.dst46); assertEquals(REMOTE_PORT, upstream4Value.dstPort); // [2] Verify stats map. @@ -1055,15 +1050,15 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { // Dump stats map to verify. final String[] statsArgs = new String[] {DUMPSYS_TETHERING_RAWMAP_ARG, DUMPSYS_RAWMAP_ARG_STATS}; - final HashMap statsMap = pollRawMapFromDump( - TetherStatsKey.class, TetherStatsValue.class, Context.TETHERING_SERVICE, statsArgs); + final HashMap statsMap = pollRawMapFromDump( + S32.class, TetherStatsValue.class, Context.TETHERING_SERVICE, statsArgs); assertNotNull(statsMap); assertEquals(1, statsMap.size()); - final Map.Entry stats = + final Map.Entry stats = statsMap.entrySet().iterator().next(); - // TODO: verify the upstream index in TetherStatsKey. + // TODO: verify the upstream index in S32. final TetherStatsValue statsValue = stats.getValue(); assertEquals(RX_UDP_PACKET_COUNT, statsValue.rxPackets); @@ -1252,19 +1247,18 @@ public class EthernetTetheringTest extends EthernetTetheringTestBase { } @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @RequiresFlagsEnabled(Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT) @Test public void testTetheringLocalAgent_networkCallbacks() throws Exception { assumeMatchNonThreadLocalNetworksEnabled(); - mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, TETHERING_LOCAL_NETWORK_AGENT, "1"); doTestLocalAgent_networkCallbacks(CONNECTIVITY_SCOPE_GLOBAL); } @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @RequiresFlagsEnabled(Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT) @Test public void testWifiP2pGroupOwnerLocalAgent_networkCallbacks() throws Exception { assumeMatchNonThreadLocalNetworksEnabled(); - mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, TETHERING_LOCAL_NETWORK_AGENT, "1"); - mDeviceConfigRule.setConfig(NAMESPACE_TETHERING, WIFIP2PGO_LOCAL_NETWORK_AGENT, "1"); doTestLocalAgent_networkCallbacks(CONNECTIVITY_SCOPE_LOCAL); } diff --git a/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java b/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java index 9494aa4aff..0e15dc855e 100644 --- a/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java +++ b/Tethering/tests/mts/src/android/tethering/mts/SkDestroyListenerTest.java @@ -33,8 +33,8 @@ import android.system.Os; import android.util.Pair; import com.android.net.module.util.BpfDump; -import com.android.net.module.util.bpf.CookieTagMapKey; import com.android.net.module.util.bpf.CookieTagMapValue; +import com.android.net.module.util.Struct.S64; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; @@ -56,11 +56,11 @@ public class SkDestroyListenerTest { private static final String LINE_DELIMITER = "\\n"; private static final String DUMP_COMMAND = "dumpsys netstats --bpfRawMap --cookieTagMap"; - private Map parseBpfRawMap(final String dump) { - final Map map = new HashMap<>(); + private Map parseBpfRawMap(final String dump) { + final Map map = new HashMap<>(); for (final String line: dump.split(LINE_DELIMITER)) { - final Pair keyValue = - BpfDump.fromBase64EncodedString(CookieTagMapKey.class, + final Pair keyValue = + BpfDump.fromBase64EncodedString(S64.class, CookieTagMapValue.class, line.trim()); map.put(keyValue.first, keyValue.second); } @@ -69,7 +69,7 @@ public class SkDestroyListenerTest { private int countTaggedSocket() { final String dump = runShellCommandOrThrow(DUMP_COMMAND); - final Map cookieTagMap = parseBpfRawMap(dump); + final Map cookieTagMap = parseBpfRawMap(dump); int count = 0; for (final CookieTagMapValue value: cookieTagMap.values()) { if (value.tag == COOKIE_TAG && value.uid == Process.myUid()) { diff --git a/Tethering/tests/privileged/Android.bp b/Tethering/tests/privileged/Android.bp index 3597a91546..76b1521bf8 100644 --- a/Tethering/tests/privileged/Android.bp +++ b/Tethering/tests/privileged/Android.bp @@ -53,5 +53,8 @@ android_test { "TetheringApiCurrentLib", ], compile_multilib: "both", - min_sdk_version: "30", + min_sdk_version: select(release_flag("RELEASE_DEPRECATE_MAINLINE_R_SUPPORT"), { + true: "31", + default: "30", + }), } diff --git a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java index 7cc8c7454f..c1aa389a9f 100644 --- a/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java +++ b/Tethering/tests/privileged/src/android/net/ip/RouterAdvertisementDaemonTest.java @@ -326,18 +326,8 @@ public final class RouterAdvertisementDaemonTest { mRaDaemon.buildNewRa(null, params1); assertMulticastRaPacket(new TestRaPacket(null, params1)); - // Add a default route "fe80::/64 -> ::" to local network, otherwise, device will fail to - // send the unicast RA out due to the ENETUNREACH error(No route to the peer's link-local - // address is present). - try { - sNetd.networkAddRoute(INetd.LOCAL_NET_ID, mTetheredParams.name, - "fe80::/64", INetd.NEXTHOP_NONE); - } catch (RemoteException | ServiceSpecificException e) { - throw new IllegalStateException(e); - } - final ByteBuffer rs = createRsPacket("fe80::1122:3344:5566:7788"); mTetheredPacketReader.sendResponse(rs); - assertUnicastRaPacket(new TestRaPacket(null, params1)); + // TODO: make this pass: assertUnicastRaPacket(new TestRaPacket(null, params1)); } } diff --git a/Tethering/tests/unit/Android.bp b/Tethering/tests/unit/Android.bp index c2826186e3..de4c176576 100644 --- a/Tethering/tests/unit/Android.bp +++ b/Tethering/tests/unit/Android.bp @@ -45,7 +45,10 @@ java_library { java_defaults { name: "TetheringTestsDefaults", - min_sdk_version: "30", + min_sdk_version: select(release_flag("RELEASE_DEPRECATE_MAINLINE_R_SUPPORT"), { + true: "31", + default: "30", + }), srcs: [ "src/**/*.java", "src/**/*.kt", diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java index 1d0deff3d8..8b01c47949 100644 --- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java +++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java @@ -39,9 +39,9 @@ import static android.net.ip.IpServer.getTetherableIpv6Prefixes; import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.modules.utils.build.SdkLevel.isAtLeastV; +import static com.android.net.module.util.ConnectivityCommonFlags.USE_ROUTE_PARCEL_IPCS; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; -import static com.android.networkstack.tethering.TetheringFeatureFlags.TETHERING_LOCAL_NETWORK_AGENT; -import static com.android.networkstack.tethering.TetheringFeatureFlags.WIFIP2PGO_LOCAL_NETWORK_AGENT; +import static com.android.networkstack.tethering.TetheringFeatureFlags.TETHERING_AND_P2P_GO_LOCAL_AGENT; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -110,6 +110,8 @@ import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.com.android.testutils.SetFeatureFlagsRule; +import com.android.testutils.com.android.testutils.SetFeatureFlagsRule.FeatureFlag; +import com.android.tethering.mainline.beta.Flags; import org.junit.Before; import org.junit.Rule; @@ -227,9 +229,15 @@ public class IpServerTest { when(mDependencies.getInterfaceParams(UPSTREAM_IFACE)).thenReturn(UPSTREAM_IFACE_PARAMS); when(mDependencies.getInterfaceParams(UPSTREAM_IFACE2)).thenReturn(UPSTREAM_IFACE_PARAMS2); when(mDependencies.getInterfaceParams(IPSEC_IFACE)).thenReturn(IPSEC_IFACE_PARAMS); + final boolean agentFeatureEnabled = mFeatureFlags.getOrDefault( + Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT, false); + doReturn(agentFeatureEnabled).when(mDependencies).isTetheringFeatureNotChickenedOut(any(), + eq(TETHERING_AND_P2P_GO_LOCAL_AGENT)); + doReturn(agentFeatureEnabled).when(mDependencies) + .isTetheringAndP2pGoLocalAgentBetaFlagEnabled(); doAnswer( - invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), false) - ).when(mDependencies).isFeatureEnabled(any(), anyString()); + invocation -> mFeatureFlags.getOrDefault((String) invocation.getArgument(1), true) + ).when(mDependencies).isFeatureNotChickenedOut(any(), anyString()); if (isAtLeastV()) { when(mDependencies.makeNetworkAgent(any(), any(), anyString(), anyInt(), any())) .thenReturn(mNetworkAgent); @@ -290,7 +298,7 @@ public class IpServerTest { verify(mBpfCoordinator, times(2)).maybeAddUpstreamToLookupTable( interfaceParams.index, upstreamIface); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, interfaceParams.index, upstreamPrefixes); + mIpServer, interfaceParams.index, upstreamPrefixes, 1400); } reset(mNetd, mBpfCoordinator, mCallback, mRoutingCoordinatorManager); when(mRoutingCoordinatorManager.requestStickyDownstreamAddress(anyInt(), anyInt(), @@ -376,10 +384,12 @@ public class IpServerTest { } private boolean isTetheringNetworkAgentFeatureEnabled() { - return isAtLeastV() && mFeatureFlags.getOrDefault(TETHERING_LOCAL_NETWORK_AGENT, false); + return isAtLeastV() && mFeatureFlags.getOrDefault( + Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT, false); } @Test + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) public void canBeTetheredAsBluetooth() throws Exception { initStateMachine(TETHERING_BLUETOOTH); @@ -399,13 +409,11 @@ public class IpServerTest { inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); if (isTetheringNetworkAgentFeatureEnabled()) { inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString()); - inOrder.verify(mNetd, never()) - .networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verifyNeverNetworkAddRoute(inOrder); } else { inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); // One for ipv4 route, one for ipv6 link local route. - inOrder.verify(mNetd, times(2)) - .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), any(), any()); + verifyNetworkAddRoute(inOrder, true, 2); } inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR); @@ -446,6 +454,7 @@ public class IpServerTest { } @Test + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) public void canBeTetheredAsUsb() throws Exception { initStateMachine(TETHERING_USB); @@ -460,12 +469,10 @@ public class IpServerTest { inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); if (isTetheringNetworkAgentFeatureEnabled()) { inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString()); - inOrder.verify(mNetd, never()) - .networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verifyNeverNetworkAddRoute(inOrder); } else { inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd, times(2)) - .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), any(), any()); + verifyNetworkAddRoute(inOrder, true, 2); } inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_TETHERED, TETHER_ERROR_NO_ERROR); @@ -476,6 +483,7 @@ public class IpServerTest { } @Test + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) public void canBeTetheredAsWifiP2p_NotUsingDedicatedIp() throws Exception { initStateMachine(TETHERING_WIFI_P2P); @@ -489,8 +497,7 @@ public class IpServerTest { IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP))); inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), - any(), any()); + verifyNetworkAddRoute(inOrder, true, 2); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR); inOrder.verify(mCallback).updateLinkProperties( @@ -500,6 +507,7 @@ public class IpServerTest { } @Test + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) public void canBeTetheredAsWifiP2p_UsingDedicatedIp() throws Exception { initStateMachine(TETHERING_WIFI_P2P, false /* usingLegacyDhcp */, DEFAULT_USING_BPF_OFFLOAD, true /* shouldEnableWifiP2pDedicatedIp */); @@ -516,8 +524,7 @@ public class IpServerTest { IFACE_NAME.equals(cfg.ifName) && assertNotContainsFlag(cfg.flags, IF_STATE_UP))); inOrder.verify(mNetd).tetherInterfaceAdd(IFACE_NAME); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); - inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), - any(), any()); + verifyNetworkAddRoute(inOrder, true, 2); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR); inOrder.verify(mCallback).updateLinkProperties( @@ -638,7 +645,7 @@ public class IpServerTest { inOrder.verify(mRoutingCoordinatorManager) .removeInterfaceForward(IFACE_NAME, UPSTREAM_IFACE); inOrder.verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, NO_UPSTREAM, NO_PREFIXES); + mIpServer, NO_UPSTREAM, NO_PREFIXES, 1400); // When tethering stops, upstream interface is set to zero and thus clearing all upstream // rules. Downstream rules are needed to be cleared explicitly by calling // BpfCoordinator#clearAllIpv6Rules in TetheredState#exit. @@ -784,6 +791,7 @@ public class IpServerTest { } @Test + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) public void testOnNewPrefixRequest() throws Exception { initStateMachine(TETHERING_NCM); dispatchCommand(IpServer.CMD_TETHER_REQUESTED, 0, 0, @@ -805,8 +813,7 @@ public class IpServerTest { inOrder.verify(mRoutingCoordinatorManager, never()).requestDownstreamAddress(any()); inOrder.verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, IFACE_NAME); // One for ipv4 route, one for ipv6 link local route. - inOrder.verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(IFACE_NAME), - any(), any()); + verifyNetworkAddRoute(inOrder, true, 2); inOrder.verify(mCallback).updateInterfaceState( mIpServer, STATE_LOCAL_ONLY, TETHER_ERROR_NO_ERROR); inOrder.verify(mCallback).updateLinkProperties(eq(mIpServer), lpCaptor.capture()); @@ -858,7 +865,7 @@ public class IpServerTest { lp.setLinkAddresses(UPSTREAM_ADDRESSES); dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp, -1); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES); + mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES, 1400); reset(mBpfCoordinator); // Upstream link addresses change result in updating the rules. @@ -867,7 +874,7 @@ public class IpServerTest { lp2.setLinkAddresses(UPSTREAM_ADDRESSES2); dispatchTetherConnectionChanged(UPSTREAM_IFACE2, lp2, -1); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2); + mIpServer, UPSTREAM_IFINDEX2, UPSTREAM_PREFIXES2, 1400); reset(mBpfCoordinator); // When the upstream is lost, rules are removed. @@ -877,33 +884,33 @@ public class IpServerTest { // - processMessage CMD_IPV6_TETHER_UPDATE for the IPv6 upstream is lost. // See dispatchTetherConnectionChanged. verify(mBpfCoordinator, times(2)).updateIpv6UpstreamInterface( - mIpServer, NO_UPSTREAM, NO_PREFIXES); + mIpServer, NO_UPSTREAM, NO_PREFIXES, 1400); reset(mBpfCoordinator); // If the upstream is IPv4-only, no rules are added. dispatchTetherConnectionChanged(UPSTREAM_IFACE); verify(mBpfCoordinator, never()).updateIpv6UpstreamInterface( - mIpServer, NO_UPSTREAM, NO_PREFIXES); + mIpServer, NO_UPSTREAM, NO_PREFIXES, 1400); reset(mBpfCoordinator); // Rules are added again once upstream IPv6 connectivity is available. lp.setInterfaceName(UPSTREAM_IFACE); dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES); + mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES, 1400); reset(mBpfCoordinator); // If upstream IPv6 connectivity is lost, rules are removed. dispatchTetherConnectionChanged(UPSTREAM_IFACE, null, 0); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, NO_UPSTREAM, NO_PREFIXES); + mIpServer, NO_UPSTREAM, NO_PREFIXES, 1400); reset(mBpfCoordinator); // When upstream IPv6 connectivity comes back, rules are added. lp.setInterfaceName(UPSTREAM_IFACE); dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES); + mIpServer, UPSTREAM_IFINDEX, UPSTREAM_PREFIXES, 1400); reset(mBpfCoordinator); // When the downstream interface goes down, rules are removed. @@ -912,7 +919,7 @@ public class IpServerTest { verify(mBpfCoordinator).clearAllIpv6Rules(mIpServer); verify(mBpfCoordinator).removeIpServer(mIpServer); verify(mBpfCoordinator).updateIpv6UpstreamInterface( - mIpServer, NO_UPSTREAM, NO_PREFIXES); + mIpServer, NO_UPSTREAM, NO_PREFIXES, 1400); reset(mBpfCoordinator); } @@ -1115,44 +1122,67 @@ public class IpServerTest { return true; } + private void verifyNeverNetworkAddRoute(InOrder inOrder) throws Exception { + inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), + anyString()); + } + + private void verifyNetworkAddRoute(InOrder inOrder, boolean useParcelMethods, int times) + throws Exception { + if (useParcelMethods) { + inOrder.verify(mNetd, times(times)).networkAddRouteParcel(eq(INetd.LOCAL_NET_ID), + argThat(routeInfo -> routeInfo != null && IFACE_NAME.equals(routeInfo.ifName))); + inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), + anyString()); + } else { + inOrder.verify(mNetd, times(times)).networkAddRoute(eq(INetd.LOCAL_NET_ID), + eq(IFACE_NAME), any(), any()); + inOrder.verify(mNetd, never()).networkAddRouteParcel(anyInt(), any()); + } + } + @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) + @FeatureFlag(name = Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT) + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) @Test public void testTetheringNetworkAgent_tetheringAgentEnabled() throws Exception { - doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, true); + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, true, true); + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, true, true); } - @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT, enabled = false) - @SetFeatureFlagsRule.FeatureFlag(name = WIFIP2PGO_LOCAL_NETWORK_AGENT) + + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS) @Test public void testTetheringNetworkAgent_tetheringAgentDisabled() throws Exception { - doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false); - // Even if the flag is enabled, no wifip2p network agent if the tethering network - // agent feature is not supported. - doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false); + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false, true); + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false, true); } - // Verify Tethering Network Agent feature doesn't affect Wi-fi P2P Group Owner although - // the code is mostly shared. + // todo(b/441359615) : remove the test while cleaning up flag USE_ROUTE_PARCEL_IPCS. + @IgnoreAfter(Build.VERSION_CODES.BAKLAVA) @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) - @SetFeatureFlagsRule.FeatureFlag(name = WIFIP2PGO_LOCAL_NETWORK_AGENT, enabled = false) + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS, enabled = false) @Test - public void testTetheringNetworkAgent_p2pGroupOwnerAgentDisabled() throws Exception { - doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false); - } - - @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) - @SetFeatureFlagsRule.FeatureFlag(name = WIFIP2PGO_LOCAL_NETWORK_AGENT) - @Test - public void testTetheringNetworkAgent_p2pGroupOwnerAgentEnabled() throws Exception { - doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, true); - } - - private void doTestTetheringNetworkAgent(int scope, boolean expectAgentEnabled) + public void testTetheringNetworkAgent_UseRouteParcelDisabled_RouteMethodsCalled() throws Exception { + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false, false); + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false, false); + } + + @IgnoreUpTo(Build.VERSION_CODES.BAKLAVA) + @FeatureFlag(name = USE_ROUTE_PARCEL_IPCS, enabled = false) + @Test + public void testTetheringNetworkAgent_postBaklava_RouteParcelMethodsCalled() + throws Exception { + // On Android versions post-Baklava, the implementation exclusively uses route parcel + // IPCs. As a result, the USE_ROUTE_PARCEL_IPCS feature flag is no longer checked and + // has no effect. + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_GLOBAL, false, true); + doTestTetheringNetworkAgent(CONNECTIVITY_SCOPE_LOCAL, false, true); + } + + private void doTestTetheringNetworkAgent(int scope, boolean expectAgentEnabled, + boolean useRouteParcelIpcs) throws Exception { initStateMachine(TETHERING_USB); final InOrder inOrder = inOrder(mNetworkAgent, mNetd); @@ -1163,12 +1193,12 @@ public class IpServerTest { inOrder.verify(mNetd, times(1)).tetherInterfaceAdd(anyString()); if (expectAgentEnabled) { inOrder.verify(mNetd, never()).networkAddInterface(anyInt(), anyString()); - inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any()); + verifyNeverNetworkAddRoute(inOrder); inOrder.verify(mNetworkAgent, times(1)).sendLinkProperties(any()); inOrder.verify(mNetworkAgent, times(1)).markConnected(); } else { inOrder.verify(mNetd, times(1)).networkAddInterface(anyInt(), anyString()); - inOrder.verify(mNetd, times(2)).networkAddRoute(anyInt(), anyString(), any(), any()); + verifyNetworkAddRoute(inOrder, useRouteParcelIpcs, 2); inOrder.verify(mNetworkAgent, never()).sendLinkProperties(any()); inOrder.verify(mNetworkAgent, never()).markConnected(); } @@ -1184,8 +1214,8 @@ public class IpServerTest { } // Verify if the registration failed, tethering can be gracefully shutdown. + @FeatureFlag(name = Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT) @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) @Test public void testTetheringNetworkAgent_registerThrows() throws Exception { initStateMachine(TETHERING_USB); @@ -1207,8 +1237,8 @@ public class IpServerTest { } // Verify if the network creation failed, tethering can be gracefully shutdown. + @FeatureFlag(name = Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT) @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) @Test public void testTetheringNetworkAgent_netdThrows() throws Exception { initStateMachine(TETHERING_USB); @@ -1229,8 +1259,8 @@ public class IpServerTest { } // Verify when IPv6 address update, set routes accordingly. + @FeatureFlag(name = Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT) @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - @SetFeatureFlagsRule.FeatureFlag(name = TETHERING_LOCAL_NETWORK_AGENT) @Test public void testTetheringNetworkAgent_ipv6AddressUpdate() throws Exception { initStateMachine(TETHERING_USB); @@ -1241,7 +1271,7 @@ public class IpServerTest { inOrder.verify(mNetworkAgent).register(); inOrder.verify(mNetd, never()).networkCreate(any()); - inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any()); + verifyNeverNetworkAddRoute(inOrder); // Ipv6 link local route won't show up in the LinkProperties, so just // verify ipv4 route. @@ -1264,7 +1294,7 @@ public class IpServerTest { lp.setInterfaceName(UPSTREAM_IFACE); lp.setLinkAddresses(UPSTREAM_ADDRESSES); dispatchTetherConnectionChanged(UPSTREAM_IFACE, lp, -1); - inOrder.verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), any(), any()); + verifyNeverNetworkAddRoute(inOrder); inOrder.verify(mNetworkAgent).sendLinkProperties(lpCaptor.capture()); // Expect one Ipv4 route, plus one Ipv6 route. diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java index 92ed6fc5b4..6c2967e499 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java @@ -57,7 +57,6 @@ import static com.android.networkstack.tethering.BpfCoordinator.NON_OFFLOADED_UP import static com.android.networkstack.tethering.BpfCoordinator.StatsType; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_IFACE; import static com.android.networkstack.tethering.BpfCoordinator.StatsType.STATS_PER_UID; -import static com.android.networkstack.tethering.BpfCoordinator.toIpv4MappedAddressBytes; import static com.android.networkstack.tethering.BpfUtils.DOWNSTREAM; import static com.android.networkstack.tethering.BpfUtils.UPSTREAM; import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS; @@ -122,9 +121,9 @@ import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.NetworkStackConstants; import com.android.net.module.util.SharedLog; import com.android.net.module.util.Struct.S32; +import com.android.net.module.util.Struct.S64; import com.android.net.module.util.bpf.Tether4Key; import com.android.net.module.util.bpf.Tether4Value; -import com.android.net.module.util.bpf.TetherStatsKey; import com.android.net.module.util.bpf.TetherStatsValue; import com.android.net.module.util.ip.ConntrackMonitor; import com.android.net.module.util.ip.ConntrackMonitor.ConntrackEventConsumer; @@ -301,8 +300,8 @@ public class BpfCoordinatorTest { private int mIif = DOWNSTREAM_IFINDEX; private MacAddress mDstMac = DOWNSTREAM_MAC; private short mL4proto = (short) IPPROTO_TCP; - private byte[] mSrc4 = PRIVATE_ADDR.getAddress(); - private byte[] mDst4 = REMOTE_ADDR.getAddress(); + private Inet4Address mSrc4 = PRIVATE_ADDR; + private Inet4Address mDst4 = REMOTE_ADDR; private int mSrcPort = PRIVATE_PORT; private int mDstPort = REMOTE_PORT; @@ -325,8 +324,8 @@ public class BpfCoordinatorTest { private int mIif = UPSTREAM_IFINDEX; private MacAddress mDstMac = MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */; private short mL4proto = (short) IPPROTO_TCP; - private byte[] mSrc4 = REMOTE_ADDR.getAddress(); - private byte[] mDst4 = PUBLIC_ADDR.getAddress(); + private Inet4Address mSrc4 = REMOTE_ADDR; + private Inet4Address mDst4 = PUBLIC_ADDR; private int mSrcPort = REMOTE_PORT; private int mDstPort = PUBLIC_PORT; @@ -351,8 +350,8 @@ public class BpfCoordinatorTest { private MacAddress mEthSrcMac = MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */; private int mEthProto = ETH_P_IP; private short mPmtu = NetworkStackConstants.ETHER_MTU; - private byte[] mSrc46 = toIpv4MappedAddressBytes(PUBLIC_ADDR); - private byte[] mDst46 = toIpv4MappedAddressBytes(REMOTE_ADDR); + private InetAddress mSrc46 = PUBLIC_ADDR; + private InetAddress mDst46 = REMOTE_ADDR; private int mSrcPort = PUBLIC_PORT; private int mDstPort = REMOTE_PORT; private long mLastUsed = 0; @@ -376,8 +375,8 @@ public class BpfCoordinatorTest { private MacAddress mEthSrcMac = DOWNSTREAM_MAC; private int mEthProto = ETH_P_IP; private short mPmtu = NetworkStackConstants.ETHER_MTU; - private byte[] mSrc46 = toIpv4MappedAddressBytes(REMOTE_ADDR); - private byte[] mDst46 = toIpv4MappedAddressBytes(PRIVATE_ADDR); + private InetAddress mSrc46 = REMOTE_ADDR; + private InetAddress mDst46 = PRIVATE_ADDR; private int mSrcPort = REMOTE_PORT; private int mDstPort = PRIVATE_PORT; private long mLastUsed = 0; @@ -480,12 +479,12 @@ public class BpfCoordinatorTest { spy(new TestBpfMap<>(TetherDownstream6Key.class, Tether6Value.class)); private final IBpfMap mBpfUpstream6Map = spy(new TestBpfMap<>(TetherUpstream6Key.class, Tether6Value.class)); - private final IBpfMap mBpfStatsMap = - spy(new TestBpfMap<>(TetherStatsKey.class, TetherStatsValue.class)); - private final IBpfMap mBpfLimitMap = - spy(new TestBpfMap<>(TetherLimitKey.class, TetherLimitValue.class)); - private final IBpfMap mBpfDevMap = - spy(new TestBpfMap<>(TetherDevKey.class, TetherDevValue.class)); + private final IBpfMap mBpfStatsMap = + spy(new TestBpfMap<>(S32.class, TetherStatsValue.class)); + private final IBpfMap mBpfLimitMap = + spy(new TestBpfMap<>(S32.class, S64.class)); + private final IBpfMap mBpfDevMap = + spy(new TestBpfMap<>(S32.class, S32.class)); private final IBpfMap mBpfErrorMap = spy(new TestBpfMap<>(S32.class, S32.class)); private BpfCoordinator.Dependencies mDeps = @@ -554,17 +553,17 @@ public class BpfCoordinatorTest { } @Nullable - public IBpfMap getBpfStatsMap() { + public IBpfMap getBpfStatsMap() { return mBpfStatsMap; } @Nullable - public IBpfMap getBpfLimitMap() { + public IBpfMap getBpfLimitMap() { return mBpfLimitMap; } @Nullable - public IBpfMap getBpfDevMap() { + public IBpfMap getBpfDevMap() { return mBpfDevMap; } @@ -604,7 +603,7 @@ public class BpfCoordinatorTest { private void dispatchIpv6UpstreamChanged(BpfCoordinator bpfCoordinator, IpServer ipServer, int upstreamIfindex, String upstreamIface, Set upstreamPrefixes) { bpfCoordinator.maybeAddUpstreamToLookupTable(upstreamIfindex, upstreamIface); - bpfCoordinator.updateIpv6UpstreamInterface(ipServer, upstreamIfindex, upstreamPrefixes); + bpfCoordinator.updateIpv6UpstreamInterface(ipServer, upstreamIfindex, upstreamPrefixes, 1400); when(ipServer.getIpv6UpstreamIfindex()).thenReturn(upstreamIfindex); when(ipServer.getIpv6UpstreamPrefixes()).thenReturn(upstreamPrefixes); } @@ -677,7 +676,7 @@ public class BpfCoordinatorTest { // Update a stats entry or create if not exists. private void updateStatsEntryToStatsMap(@NonNull TetherStatsParcel stats) throws Exception { - final TetherStatsKey key = new TetherStatsKey(stats.ifIndex); + final S32 key = new S32(stats.ifIndex); final TetherStatsValue value = new TetherStatsValue(stats.rxPackets, stats.rxBytes, 0L /* rxErrors */, stats.txPackets, stats.txBytes, 0L /* txErrors */); mBpfStatsMap.updateEntry(key, value); @@ -769,8 +768,7 @@ public class BpfCoordinatorTest { final TetherUpstream6Key key = new TetherUpstream6Key(DOWNSTREAM_IFACE_PARAMS.index, DOWNSTREAM_IFACE_PARAMS.macAddr, prefix64); final Tether6Value value = new Tether6Value(upstreamIfindex, - MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, ETH_P_IPV6, - NetworkStackConstants.ETHER_MTU); + MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, ETH_P_IPV6, 1400); expected.put(key, value); } ArgumentCaptor keyCaptor = @@ -926,15 +924,15 @@ public class BpfCoordinatorTest { private void verifyTetherOffloadSetInterfaceQuota(@Nullable InOrder inOrder, int ifIndex, long quotaBytes, boolean isInit) throws Exception { if (mDeps.isAtLeastS()) { - final TetherStatsKey key = new TetherStatsKey(ifIndex); + final S32 key = new S32(ifIndex); verifyWithOrder(inOrder, mBpfStatsMap).getValue(key); if (isInit) { verifyWithOrder(inOrder, mBpfStatsMap).insertEntry(key, new TetherStatsValue( 0L /* rxPackets */, 0L /* rxBytes */, 0L /* rxErrors */, 0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */)); } - verifyWithOrder(inOrder, mBpfLimitMap).updateEntry(new TetherLimitKey(ifIndex), - new TetherLimitValue(quotaBytes)); + verifyWithOrder(inOrder, mBpfLimitMap).updateEntry(new S32(ifIndex), + new S64(quotaBytes)); } else { verifyWithOrder(inOrder, mNetd).tetherOffloadSetInterfaceQuota(ifIndex, quotaBytes); } @@ -954,9 +952,9 @@ public class BpfCoordinatorTest { private void verifyTetherOffloadGetAndClearStats(@NonNull InOrder inOrder, int ifIndex) throws Exception { if (mDeps.isAtLeastS()) { - inOrder.verify(mBpfStatsMap).getValue(new TetherStatsKey(ifIndex)); - inOrder.verify(mBpfStatsMap).deleteEntry(new TetherStatsKey(ifIndex)); - inOrder.verify(mBpfLimitMap).deleteEntry(new TetherLimitKey(ifIndex)); + inOrder.verify(mBpfStatsMap).getValue(new S32(ifIndex)); + inOrder.verify(mBpfStatsMap).deleteEntry(new S32(ifIndex)); + inOrder.verify(mBpfLimitMap).deleteEntry(new S32(ifIndex)); } else { inOrder.verify(mNetd).tetherOffloadGetAndClearStats(ifIndex); } @@ -1230,7 +1228,7 @@ public class BpfCoordinatorTest { private static Ipv6UpstreamRule buildTestUpstreamRule(int upstreamIfindex, int downstreamIfindex, @NonNull IpPrefix sourcePrefix, @NonNull MacAddress inDstMac) { return new Ipv6UpstreamRule(upstreamIfindex, downstreamIfindex, sourcePrefix, inDstMac, - MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS); + MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS, 1400); } @NonNull @@ -1295,7 +1293,7 @@ public class BpfCoordinatorTest { assertEquals(MAC_NULL, value.ethDstMac); assertEquals(MAC_NULL, value.ethSrcMac); assertEquals(ETH_P_IPV6, value.ethProto); - assertEquals(NetworkStackConstants.ETHER_MTU, value.pmtu); + assertEquals(1400, value.pmtu); // oif (4) + ethDstMac (6) + ethSrcMac (6) + ethProto (2) + pmtu (2) = 20 assertEquals(20, value.writeToBytes().length); } @@ -1536,7 +1534,7 @@ public class BpfCoordinatorTest { // The rule can't be updated. coordinator.updateIpv6UpstreamInterface(mIpServer, rule.upstreamIfindex + 1 /* new */, - UPSTREAM_PREFIXES); + UPSTREAM_PREFIXES, 1400); verifyNeverRemoveDownstreamRule(); verifyNeverAddDownstreamRule(); rules = coordinator.getIpv6DownstreamRulesForTesting().get(mIpServer); @@ -1973,10 +1971,10 @@ public class BpfCoordinatorTest { dispatchIpv6UpstreamChanged( coordinator, mIpServer, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES); - verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), - eq(new TetherDevValue(UPSTREAM_IFINDEX))); - verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)), - eq(new TetherDevValue(DOWNSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new S32(UPSTREAM_IFINDEX)), + eq(new S32(UPSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new S32(DOWNSTREAM_IFINDEX)), + eq(new S32(DOWNSTREAM_IFINDEX))); clearInvocations(mBpfDevMap); // Adding the second downstream, only the second downstream ifindex is added to DevMap, @@ -1984,10 +1982,10 @@ public class BpfCoordinatorTest { coordinator.addIpServer(mIpServer2); dispatchIpv6UpstreamChanged( coordinator, mIpServer2, UPSTREAM_IFINDEX, UPSTREAM_IFACE, UPSTREAM_PREFIXES); - verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX2)), - eq(new TetherDevValue(DOWNSTREAM_IFINDEX2))); - verify(mBpfDevMap, never()).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), - eq(new TetherDevValue(UPSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new S32(DOWNSTREAM_IFINDEX2)), + eq(new S32(DOWNSTREAM_IFINDEX2))); + verify(mBpfDevMap, never()).updateEntry(eq(new S32(UPSTREAM_IFINDEX)), + eq(new S32(UPSTREAM_IFINDEX))); } @Test @@ -2000,10 +1998,10 @@ public class BpfCoordinatorTest { .setMsgType(IPCTNL_MSG_CT_NEW) .setProto(IPPROTO_TCP) .build()); - verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(UPSTREAM_IFINDEX)), - eq(new TetherDevValue(UPSTREAM_IFINDEX))); - verify(mBpfDevMap).updateEntry(eq(new TetherDevKey(DOWNSTREAM_IFINDEX)), - eq(new TetherDevValue(DOWNSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new S32(UPSTREAM_IFINDEX)), + eq(new S32(UPSTREAM_IFINDEX))); + verify(mBpfDevMap).updateEntry(eq(new S32(DOWNSTREAM_IFINDEX)), + eq(new S32(DOWNSTREAM_IFINDEX))); clearInvocations(mBpfDevMap); mConsumer.accept(new TestConntrackEvent.Builder() @@ -2431,15 +2429,14 @@ public class BpfCoordinatorTest { private static Tether4Key makeUpstream4Key(final int downstreamIfindex, @NonNull final MacAddress downstreamMac, @NonNull final Inet4Address privateAddr, final short privatePort) { - return new Tether4Key(downstreamIfindex, downstreamMac, (short) IPPROTO_TCP, - privateAddr.getAddress(), REMOTE_ADDR.getAddress(), privatePort, REMOTE_PORT); + return new Tether4Key(downstreamIfindex, downstreamMac, (short) IPPROTO_TCP, privateAddr, + REMOTE_ADDR, privatePort, REMOTE_PORT); } @NonNull private static Tether4Key makeDownstream4Key(final short publicPort) { return new Tether4Key(UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS /* dstMac (rawip) */, - (short) IPPROTO_TCP, REMOTE_ADDR.getAddress(), PUBLIC_ADDR.getAddress(), - REMOTE_PORT, publicPort); + (short) IPPROTO_TCP, REMOTE_ADDR, PUBLIC_ADDR, REMOTE_PORT, publicPort); } @NonNull @@ -2447,8 +2444,7 @@ public class BpfCoordinatorTest { return new Tether4Value(UPSTREAM_IFINDEX, MacAddress.ALL_ZEROS_ADDRESS /* ethDstMac (rawip) */, MacAddress.ALL_ZEROS_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP, - NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(PUBLIC_ADDR), - toIpv4MappedAddressBytes(REMOTE_ADDR), publicPort, REMOTE_PORT, + NetworkStackConstants.ETHER_MTU, PUBLIC_ADDR, REMOTE_ADDR, publicPort, REMOTE_PORT, 0 /* lastUsed */); } @@ -2457,8 +2453,8 @@ public class BpfCoordinatorTest { @NonNull final MacAddress clientMac, @NonNull final MacAddress downstreamMac, @NonNull final Inet4Address privateAddr, final short privatePort) { return new Tether4Value(downstreamIfindex, clientMac, downstreamMac, - ETH_P_IP, NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(REMOTE_ADDR), - toIpv4MappedAddressBytes(privateAddr), REMOTE_PORT, privatePort, 0 /* lastUsed */); + ETH_P_IP, NetworkStackConstants.ETHER_MTU, REMOTE_ADDR, privateAddr, REMOTE_PORT, + privatePort, 0 /* lastUsed */); } @NonNull @@ -2730,7 +2726,7 @@ public class BpfCoordinatorTest { // dumpStats mBpfStatsMap.insertEntry( - new TetherStatsKey(UPSTREAM_IFINDEX), + new S32(UPSTREAM_IFINDEX), new TetherStatsValue( 0L /* rxPackets */, 0L /* rxBytes */, 0L /* rxErrors */, 0L /* txPackets */, 0L /* txBytes */, 0L /* txErrors */)); @@ -2738,8 +2734,8 @@ public class BpfCoordinatorTest { // dumpDevmap coordinator.maybeAddUpstreamToLookupTable(UPSTREAM_IFINDEX, UPSTREAM_IFACE); mBpfDevMap.insertEntry( - new TetherDevKey(UPSTREAM_IFINDEX), - new TetherDevValue(UPSTREAM_IFINDEX)); + new S32(UPSTREAM_IFINDEX), + new S32(UPSTREAM_IFINDEX)); // dumpCounters // The error code is defined in packages/modules/Connectivity/bpf_progs/offload.h. @@ -3011,8 +3007,8 @@ public class BpfCoordinatorTest { // When the last rule is removed, tetherOffloadGetAndClearStats will log a WTF (and // potentially crash the test) if the stats map is empty. final TetherStatsValue allZeros = new TetherStatsValue(0, 0, 0, 0, 0, 0); - when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX))).thenReturn(allZeros); - when(mBpfStatsMap.getValue(new TetherStatsKey(UPSTREAM_IFINDEX2))).thenReturn(allZeros); + when(mBpfStatsMap.getValue(new S32(UPSTREAM_IFINDEX))).thenReturn(allZeros); + when(mBpfStatsMap.getValue(new S32(UPSTREAM_IFINDEX2))).thenReturn(allZeros); } @Test diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java index de39665ca3..419cf629b3 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/EntitlementManagerTest.java @@ -110,6 +110,7 @@ public final class EntitlementManagerTest { private static final String TEST_PACKAGE_NAME = "com.android.tethering.test"; private static final String FAILED_TETHERING_REASON = "Tethering provisioning failed."; private static final int RECHECK_TIMER_HOURS = 24; + private static final int TEST_SUBID = 1; @Mock private CarrierConfigManager mCarrierConfigManager; @Mock private Context mContext; @@ -326,7 +327,7 @@ public final class EntitlementManagerTest { when(mCarrierConfigManager.getConfigForSubId(anyInt())).thenReturn(mCarrierConfig); mCarrierConfig.putBoolean(CarrierConfigManager.KEY_REQUIRE_ENTITLEMENT_CHECKS_BOOL, true); mCarrierConfig.putBoolean(CarrierConfigManager.KEY_CARRIER_CONFIG_APPLIED_BOOL, true); - mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + mConfig = new FakeTetheringConfiguration(mMockContext, mLog, TEST_SUBID); } private void setupCarrierConfig(boolean carrierSupported) { @@ -349,14 +350,24 @@ public final class EntitlementManagerTest { setupForRequiredProvisioning(); when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) .thenReturn(null); - mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + mConfig = new FakeTetheringConfiguration(mMockContext, mLog, TEST_SUBID); assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig)); when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) .thenReturn(new String[] {"malformedApp"}); - mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + mConfig = new FakeTetheringConfiguration(mMockContext, mLog, TEST_SUBID); assertFalse(mEnMgr.isTetherProvisioningRequired(mConfig)); } + @Test + public void provisioningNotRequiredAfterSwitchToInvalidSubId() { + setupForRequiredProvisioning(); + assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig)); + + final TetheringConfiguration config = new FakeTetheringConfiguration(mMockContext, mLog, + INVALID_SUBSCRIPTION_ID); + assertFalse(mEnMgr.isTetherProvisioningRequired(config)); + } + @Test public void testRequestLastEntitlementCacheValue() throws Exception { // 1. Entitlement check is not required. @@ -759,7 +770,7 @@ public final class EntitlementManagerTest { mEnMgr.notifyUpstream(false); mLooper.dispatchAll(); setupCarrierConfig(false); - mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + mConfig = new FakeTetheringConfiguration(mMockContext, mLog, TEST_SUBID); mEnMgr.reevaluateSimCardProvisioning(mConfig); // Turn on upstream. @@ -799,7 +810,7 @@ public final class EntitlementManagerTest { setupCarrierConfig(false); when(mResources.getStringArray(R.array.config_mobile_hotspot_provision_app)) .thenReturn(new String[0]); - mConfig = new FakeTetheringConfiguration(mMockContext, mLog, INVALID_SUBSCRIPTION_ID); + mConfig = new FakeTetheringConfiguration(mMockContext, mLog, TEST_SUBID); if (SdkLevel.isAtLeastT()) { assertTrue(mEnMgr.isTetherProvisioningRequired(mConfig)); diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java index 15653692cd..ad75d6bb20 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/PrivateAddressCoordinatorTest.java @@ -20,6 +20,7 @@ import static android.net.NetworkCapabilities.TRANSPORT_VPN; import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.TetheringManager.CONNECTIVITY_SCOPE_GLOBAL; import static android.net.TetheringManager.CONNECTIVITY_SCOPE_LOCAL; +import static android.net.TetheringManager.TETHERING_BLUETOOTH; import static android.net.TetheringManager.TETHERING_ETHERNET; import static android.net.TetheringManager.TETHERING_USB; import static android.net.TetheringManager.TETHERING_WIFI; @@ -65,9 +66,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; -import java.util.Arrays; - @RunWith(AndroidJUnit4.class) @SmallTest public final class PrivateAddressCoordinatorTest { @@ -78,12 +76,13 @@ public final class PrivateAddressCoordinatorTest { @Mock private IpServer mUsbIpServer; @Mock private IpServer mEthernetIpServer; @Mock private IpServer mWifiP2pIpServer; + @Mock private IpServer mBluetoothIpServer; @Mock private Context mContext; @Mock private ConnectivityManager mConnectivityMgr; @Mock private PrivateAddressCoordinator.Dependencies mDeps; private PrivateAddressCoordinator mPrivateAddressCoordinator; - private final LinkAddress mBluetoothAddress = new LinkAddress("192.168.44.1/24"); + private final LinkAddress mLegacyBluetoothAddress = new LinkAddress("192.168.44.1/24"); private final LinkAddress mLegacyWifiP2pAddress = new LinkAddress("192.168.49.1/24"); private final Network mWifiNetwork = new Network(1); private final Network mMobileNetwork = new Network(2); @@ -95,10 +94,16 @@ public final class PrivateAddressCoordinatorTest { private final Network mMobileNetwork6 = new Network(8); private final Network[] mAllNetworks = {mMobileNetwork, mWifiNetwork, mVpnNetwork, mMobileNetwork2, mMobileNetwork3, mMobileNetwork4, mMobileNetwork5, mMobileNetwork6}; - private final ArrayList mTetheringPrefixes = new ArrayList<>(Arrays.asList( - new IpPrefix("192.168.0.0/16"), - new IpPrefix("172.16.0.0/12"), - new IpPrefix("10.0.0.0/8"))); + + private PrivateAddressCoordinator makePrivateAddressCoordinator() { + return makePrivateAddressCoordinator(false); + } + + private PrivateAddressCoordinator makePrivateAddressCoordinator( + boolean bluetoothTetheringUseRandomAddress) { + return spy(new PrivateAddressCoordinator( + mConnectivityMgr::getAllNetworks, mDeps, bluetoothTetheringUseRandomAddress)); + } private void setUpIpServer(IpServer ipServer, int interfaceType) throws Exception { when(ipServer.interfaceType()).thenReturn(interfaceType); @@ -120,6 +125,7 @@ public final class PrivateAddressCoordinatorTest { setUpIpServer(mHotspotIpServer, TETHERING_WIFI); setUpIpServer(mLocalHotspotIpServer, TETHERING_WIFI); setUpIpServer(mWifiP2pIpServer, TETHERING_WIFI_P2P); + setUpIpServer(mBluetoothIpServer, TETHERING_BLUETOOTH); } @Before @@ -130,8 +136,7 @@ public final class PrivateAddressCoordinatorTest { when(mContext.getSystemService(ConnectivityManager.class)).thenReturn(mConnectivityMgr); when(mConnectivityMgr.getAllNetworks()).thenReturn(mAllNetworks); setUpIpServers(); - mPrivateAddressCoordinator = - spy(new PrivateAddressCoordinator(mConnectivityMgr::getAllNetworks, mDeps)); + mPrivateAddressCoordinator = makePrivateAddressCoordinator(); } private LinkAddress requestStickyDownstreamAddress(final IpServer ipServer, int scope) @@ -162,7 +167,7 @@ public final class PrivateAddressCoordinatorTest { @Test public void testRequestDownstreamAddressWithoutUsingLastAddress() throws Exception { - final IpPrefix bluetoothPrefix = asIpPrefix(mBluetoothAddress); + final IpPrefix bluetoothPrefix = asIpPrefix(mLegacyBluetoothAddress); final LinkAddress address = requestDownstreamAddress(mHotspotIpServer); final IpPrefix hotspotPrefix = asIpPrefix(address); assertNotEquals(hotspotPrefix, bluetoothPrefix); @@ -185,11 +190,11 @@ public final class PrivateAddressCoordinatorTest { public void testReservedPrefix() throws Exception { // - Test bluetooth prefix is reserved. when(mPrivateAddressCoordinator.getRandomInt()).thenReturn( - getSubAddress(mBluetoothAddress.getAddress().getAddress())); + getSubAddress(mLegacyBluetoothAddress.getAddress().getAddress())); final LinkAddress hotspotAddress = requestStickyDownstreamAddress(mHotspotIpServer, CONNECTIVITY_SCOPE_GLOBAL); final IpPrefix hotspotPrefix = asIpPrefix(hotspotAddress); - assertNotEquals(asIpPrefix(mBluetoothAddress), hotspotPrefix); + assertNotEquals(asIpPrefix(mLegacyBluetoothAddress), hotspotPrefix); releaseDownstream(mHotspotIpServer); // - Test previous enabled hotspot prefix(cached prefix) is reserved. @@ -197,7 +202,7 @@ public final class PrivateAddressCoordinatorTest { getSubAddress(hotspotAddress.getAddress().getAddress())); final LinkAddress usbAddress = requestDownstreamAddress(mUsbIpServer); final IpPrefix usbPrefix = asIpPrefix(usbAddress); - assertNotEquals(asIpPrefix(mBluetoothAddress), usbPrefix); + assertNotEquals(asIpPrefix(mLegacyBluetoothAddress), usbPrefix); assertNotEquals(hotspotPrefix, usbPrefix); releaseDownstream(mUsbIpServer); @@ -207,7 +212,7 @@ public final class PrivateAddressCoordinatorTest { final LinkAddress etherAddress = requestDownstreamAddress(mEthernetIpServer); final IpPrefix etherPrefix = asIpPrefix(etherAddress); assertNotEquals(asIpPrefix(mLegacyWifiP2pAddress), etherPrefix); - assertNotEquals(asIpPrefix(mBluetoothAddress), etherPrefix); + assertNotEquals(asIpPrefix(mLegacyBluetoothAddress), etherPrefix); assertNotEquals(hotspotPrefix, etherPrefix); releaseDownstream(mEthernetIpServer); } @@ -298,15 +303,6 @@ public final class PrivateAddressCoordinatorTest { return (subnet << 8) + ipv4Address[3]; } - private void assertReseveredWifiP2pPrefix() throws Exception { - LinkAddress address = - requestStickyDownstreamAddress(mHotspotIpServer, CONNECTIVITY_SCOPE_GLOBAL); - final IpPrefix hotspotPrefix = asIpPrefix(address); - final IpPrefix legacyWifiP2pPrefix = asIpPrefix(mLegacyWifiP2pAddress); - assertNotEquals(legacyWifiP2pPrefix, hotspotPrefix); - releaseDownstream(mHotspotIpServer); - } - @Test public void testEnableSapAndLohsConcurrently() throws Exception { final LinkAddress hotspotAddress = @@ -351,8 +347,7 @@ public final class PrivateAddressCoordinatorTest { private void startedPrefixBaseTest(final String expected, final int randomIntForPrefixBase) throws Exception { - mPrivateAddressCoordinator = - spy(new PrivateAddressCoordinator(mConnectivityMgr::getAllNetworks, mDeps)); + mPrivateAddressCoordinator = makePrivateAddressCoordinator(); when(mPrivateAddressCoordinator.getRandomInt()).thenReturn(randomIntForPrefixBase); final LinkAddress address = requestDownstreamAddress(mHotspotIpServer); final IpPrefix prefixBase = new IpPrefix(expected); @@ -360,4 +355,22 @@ public final class PrivateAddressCoordinatorTest { prefixBase.containsPrefix(asIpPrefix(address))); } + + @Test + public void testBluetoothPrefix_legacy() throws Exception { + mPrivateAddressCoordinator = + makePrivateAddressCoordinator(false /* bluetoothTetheringUseRandomAddress */); + final LinkAddress bluetoothAddress = requestStickyDownstreamAddress(mBluetoothIpServer, + CONNECTIVITY_SCOPE_GLOBAL); + assertEquals(mLegacyBluetoothAddress, bluetoothAddress); + } + + @Test + public void testBluetoothPrefix_randomized() throws Exception { + mPrivateAddressCoordinator = + makePrivateAddressCoordinator(true /* bluetoothTetheringUseRandomAddress */); + final LinkAddress bluetoothAddress = requestStickyDownstreamAddress(mBluetoothIpServer, + CONNECTIVITY_SCOPE_GLOBAL); + assertNotEquals(mLegacyBluetoothAddress, bluetoothAddress); + } } diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java index 5614a35219..ed823730a0 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java @@ -77,6 +77,7 @@ import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; import static com.android.modules.utils.build.SdkLevel.isAtLeastS; import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.modules.utils.build.SdkLevel.isAtLeastV; +import static com.android.net.module.util.ConnectivityCommonFlags.USE_ROUTE_PARCEL_IPCS; import static com.android.net.module.util.Inet4AddressUtils.inet4AddressToIntHTH; import static com.android.net.module.util.Inet4AddressUtils.intToInet4AddressHTH; import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH; @@ -88,7 +89,7 @@ import static com.android.networkstack.tethering.Tethering.UserRestrictionAction import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_FORCE_USB_FUNCTIONS; import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_NCM_FUNCTION; import static com.android.networkstack.tethering.TetheringConfiguration.TETHER_USB_RNDIS_FUNCTION; -import static com.android.networkstack.tethering.TetheringFeatureFlags.TETHERING_LOCAL_NETWORK_AGENT; +import static com.android.networkstack.tethering.TetheringFeatureFlags.TETHERING_AND_P2P_GO_LOCAL_AGENT; import static com.android.networkstack.tethering.TetheringNotificationUpdater.DOWNSTREAM_NONE; import static com.android.networkstack.tethering.UpstreamNetworkMonitor.EVENT_ON_CAPABILITIES; import static com.android.testutils.TestPermissionUtil.runAsShell; @@ -211,6 +212,7 @@ import com.android.net.module.util.InterfaceParams; import com.android.net.module.util.PrivateAddressCoordinator; import com.android.net.module.util.RoutingCoordinatorManager; import com.android.net.module.util.RoutingCoordinatorService; +import com.android.net.module.util.SdkUtil; import com.android.net.module.util.SharedLog; import com.android.net.module.util.ip.IpNeighborMonitor; import com.android.networkstack.apishim.common.BluetoothPanShim; @@ -224,6 +226,7 @@ import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.MiscAsserts; import com.android.testutils.com.android.testutils.SetFeatureFlagsRule; +import com.android.tethering.mainline.beta.Flags; import org.junit.After; import org.junit.Before; @@ -294,6 +297,7 @@ public class TetheringTest { private static final int CELLULAR_NETID = 100; private static final int WIFI_NETID = 101; private static final int DUN_NETID = 102; + private static final int TEST_SUBID = 1; private static final int TETHER_USB_RNDIS_NCM_FUNCTIONS = 2; @@ -478,8 +482,24 @@ public class TetheringTest { } @Override - public boolean isFeatureEnabled(Context context, String name) { - return mFeatureFlags.getOrDefault(name, false); + public boolean isTetheringFeatureNotChickenedOut(@NonNull Context context, + @NonNull String name) { + return switch (name) { + // Use one flag for mocking to reduce test complexity. + case TETHERING_AND_P2P_GO_LOCAL_AGENT -> mFeatureFlags.getOrDefault( + Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT, false); + default -> throw new IllegalArgumentException("Unknown flag " + name); + }; + } + + @Override + public boolean isTetheringAndP2pGoLocalAgentBetaFlagEnabled() { + return mFeatureFlags.getOrDefault(Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT, false); + } + + @Override + public boolean isFeatureNotChickenedOut(Context context, String name) { + return mFeatureFlags.getOrDefault(name, true); } } @@ -544,7 +564,8 @@ public class TetheringTest { RoutingCoordinatorService service = new RoutingCoordinatorService( getINetd(context, log), cm::getAllNetworks, - mPrivateAddressCoordinatorDependencies); + mPrivateAddressCoordinatorDependencies, + false /* bluetoothTetheringUseRandomAddress */); mRoutingCoordinatorManager = spy(new RoutingCoordinatorManager(context, service)); return mRoutingCoordinatorManager; } @@ -980,11 +1001,10 @@ public class TetheringTest { verify(mNetd).tetherInterfaceAdd(ifname); if (expectAgentEnabled) { verify(mNetd, never()).networkAddInterface(anyInt(), anyString()); - verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verifyNeverNetworkAddRoute(); } else { verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, ifname); - verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifname), - anyString(), anyString()); + verifyNetworkAddRoute(ifname, 2); } } @@ -1076,7 +1096,8 @@ public class TetheringTest { } private boolean isTetheringNetworkAgentFeatureEnabled() { - return isAtLeastV() && mFeatureFlags.getOrDefault(TETHERING_LOCAL_NETWORK_AGENT, false); + return isAtLeastV() && mFeatureFlags.getOrDefault( + Flags.FLAG_TETHERING_AND_P2P_GO_LOCAL_AGENT, false); } private void verifyStopHotpot(boolean isLocalOnly) throws Exception { @@ -2235,11 +2256,10 @@ public class TetheringTest { verify(mNetd, times(1)).tetherInterfaceAdd(TEST_WLAN_IFNAME); if (isTetheringNetworkAgentFeatureEnabled()) { verify(mNetd, never()).networkAddInterface(anyInt(), anyString()); - verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verifyNeverNetworkAddRoute(); } else { verify(mNetd, times(1)).networkAddInterface(INetd.LOCAL_NET_ID, TEST_WLAN_IFNAME); - verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_WLAN_IFNAME), - anyString(), anyString()); + verifyNetworkAddRoute(TEST_WLAN_IFNAME, 2); } verify(mWifiManager).updateInterfaceIpState( TEST_WLAN_IFNAME, WifiManager.IFACE_IP_MODE_UNSPECIFIED); @@ -3530,6 +3550,7 @@ public class TetheringTest { @Test public void testExemptFromEntitlementCheck() throws Exception { initTetheringOnTestThread(); + mPhoneStateListener.onActiveDataSubscriptionIdChanged(TEST_SUBID); setupForRequiredProvisioning(); final TetheringRequest wifiNotExemptRequest = createTetheringRequest(TETHERING_WIFI, null, null, false, @@ -3565,6 +3586,7 @@ public class TetheringTest { // requests of the same type that are subject to carrier entitlement due to fuzzy-matching. mTetheringWithSoftApConfigEnabled = false; initTetheringOnTestThread(); + mPhoneStateListener.onActiveDataSubscriptionIdChanged(TEST_SUBID); setupForRequiredProvisioning(); final TetheringRequest wifiExemptRequest = createTetheringRequest(TETHERING_WIFI, null, null, true, @@ -3726,6 +3748,7 @@ public class TetheringTest { @Test public void testProvisioningNeededButUnavailable() throws Exception { initTetheringOnTestThread(); + mPhoneStateListener.onActiveDataSubscriptionIdChanged(TEST_SUBID); assertTrue(mTethering.isTetheringSupported()); verify(mPackageManager, never()).getPackageInfo(PROVISIONING_APP_NAME[0], GET_ACTIVITIES); @@ -4165,19 +4188,17 @@ public class TetheringTest { verify(mNetd).tetherInterfaceAdd(TEST_BT_IFNAME); if (isTetheringNetworkAgentFeatureEnabled()) { verify(mNetd, never()).networkAddInterface(anyInt(), anyString()); - verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verifyNeverNetworkAddRoute(); } else { verify(mNetd).networkAddInterface(INetd.LOCAL_NET_ID, TEST_BT_IFNAME); - verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME), - anyString(), anyString()); + verifyNetworkAddRoute(TEST_BT_IFNAME, 2); } verify(mNetd).ipfwdEnableForwarding(TETHERING_NAME); verify(mNetd).tetherStartWithConfiguration(any()); if (isTetheringNetworkAgentFeatureEnabled()) { - verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verifyNeverNetworkAddRoute(); } else { - verify(mNetd, times(2)).networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(TEST_BT_IFNAME), - anyString(), anyString()); + verifyNetworkAddRoute(TEST_BT_IFNAME, 2); } verifyNoMoreInteractions(mNetd); reset(mNetd); @@ -4190,6 +4211,29 @@ public class TetheringTest { return false; } + private void verifyNeverNetworkAddRoute() throws Exception { + verify(mNetd, never()).networkAddRoute(anyInt(), anyString(), anyString(), anyString()); + verify(mNetd, never()).networkAddRouteParcel(anyInt(), any()); + } + + private boolean useRouteParcelIpcs() { + return SdkUtil.isAtLeast25Q4() + || mFeatureFlags.getOrDefault(USE_ROUTE_PARCEL_IPCS, true); + } + + private void verifyNetworkAddRoute(String ifName, int times) + throws Exception { + if (useRouteParcelIpcs()) { + verify(mNetd, times(times)) + .networkAddRouteParcel(eq(INetd.LOCAL_NET_ID), + argThat((parcel) -> ifName.equals(parcel.ifName))); + } else { + verify(mNetd, times(times)) + .networkAddRoute(eq(INetd.LOCAL_NET_ID), eq(ifName), + anyString(), anyString()); + } + } + private void verifyNetdCommandForBtTearDown() throws Exception { verify(mNetd).tetherApplyDnsInterfaces(); verify(mNetd).tetherInterfaceRemove(TEST_BT_IFNAME); diff --git a/Vcn/OWNERS b/Vcn/OWNERS new file mode 100644 index 0000000000..ff2146e76b --- /dev/null +++ b/Vcn/OWNERS @@ -0,0 +1,6 @@ +evitayan@google.com +nharold@google.com +benedictwong@google.com #{LAST_RESORT_SUGGESTION} +yangji@google.com #{LAST_RESORT_SUGGESTION} + +include platform/packages/modules/common:/MODULES_OWNERS \ No newline at end of file diff --git a/Vcn/TEST_MAPPING b/Vcn/TEST_MAPPING new file mode 100644 index 0000000000..9ca5304eb8 --- /dev/null +++ b/Vcn/TEST_MAPPING @@ -0,0 +1,26 @@ +{ + "presubmit": [ + { + "name": "FrameworksVcnTests" + }, + { + "name": "CtsVcnTestCases" + } + ], + "postsubmit": [ + { + "name": "FrameworksVcnTests" + }, + { + "name": "CtsVcnTestCases" + } + ], + "tethering-mainline-presubmit": [ + { + "name": "FrameworksVcnTests" + }, + { + "name": "CtsVcnTestCases" + } + ] +} \ No newline at end of file diff --git a/Vcn/flags/Android.bp b/Vcn/flags/Android.bp new file mode 100644 index 0000000000..8d09fdbfa3 --- /dev/null +++ b/Vcn/flags/Android.bp @@ -0,0 +1,52 @@ +// +// Copyright (C) 2024 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 { + default_team: "trendy_team_enigma", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +aconfig_declarations { + name: "android.net.vcn.flags-aconfig", + package: "android.net.vcn", + container: "com.android.tethering", + exportable: true, + srcs: [ + "flags.aconfig", + ], +} + +// TODO: b/374174952 Remove this library when VCN modularization is done +java_aconfig_library { + name: "android.net.vcn.flags-aconfig-java-export", + aconfig_declarations: "android.net.vcn.flags-aconfig", + mode: "exported", + min_sdk_version: "35", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + apex_available: [ + "//apex_available:platform", + ], +} + +java_aconfig_library { + name: "android.net.vcn.flags-aconfig-java", + aconfig_declarations: "android.net.vcn.flags-aconfig", + min_sdk_version: "35", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + apex_available: [ + "com.android.tethering", + ], +} diff --git a/Vcn/flags/flags.aconfig b/Vcn/flags/flags.aconfig new file mode 100644 index 0000000000..b461f95fec --- /dev/null +++ b/Vcn/flags/flags.aconfig @@ -0,0 +1,18 @@ +package: "android.net.vcn" +container: "com.android.tethering" + +flag { + name: "safe_mode_config" + is_exported: true + namespace: "vcn" + description: "Feature flag for safe mode configurability" + bug: "276358140" +} + +flag { + name: "mainline_vcn_module_api" + namespace: "vcn" + description: "Expose APIs from VCN for mainline migration" + is_exported: true + bug: "376339506" +} \ No newline at end of file diff --git a/Vcn/framework-b/Android.bp b/Vcn/framework-b/Android.bp new file mode 100644 index 0000000000..f77636875c --- /dev/null +++ b/Vcn/framework-b/Android.bp @@ -0,0 +1,210 @@ +// +// Copyright (C) 2024 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 { + default_team: "trendy_team_enigma", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +filegroup { + name: "vcn-utils-platform-sources", + srcs: [ + "src/android/net/vcn/persistablebundleutils/**/*.java", + "src/android/net/vcn/util/**/*.java", + ], + path: "src", + visibility: [ + "//frameworks/base", // For VpnProfile.java and Vpn.java + ], +} + +// Utility library for the IPsec module +java_library { + name: "vcn-utils-framework-ipsec", + sdk_version: "module_current", + min_sdk_version: "30", + srcs: ["src/android/net/vcn/util/PersistableBundleUtils.java"], + libs: [ + "framework-annotations-lib", + "net-utils-framework-vcn", // For HexDump.java + ], + visibility: [ + "//packages/modules/IPsec", + ], + apex_available: ["com.android.ipsec"], +} + +java_defaults { + name: "framework-connectivity-b-defaults-base", + sdk_version: "module_current", + + defaults: ["framework-module-defaults"], // This is a boot jar + + srcs: [ + "src/**/*.java", + "src/**/*.aidl", + ], + + libs: [ + "android.net.ipsec.ike.stubs.module_lib", + "framework-wifi.stubs.module_lib", + "unsupportedappusage", + ], + + aidl: { + include_dirs: [ + // For connectivity-framework classes such as Network.aidl, NetworkCapabilities.aidl + "packages/modules/Connectivity/framework/aidl-export", + ], + }, +} + +soong_config_module_type { + name: "framework_connectivity_b_defaults_soong_config", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: [ + "is_vcn_in_mainline", + ], + properties: [ + "min_sdk_version", + "static_libs", + "apex_available", + ], +} + +framework_connectivity_b_defaults_soong_config { + name: "framework-connectivity-b-defaults", + defaults: [ + "framework-connectivity-b-defaults-base", + ], + soong_config_variables: { + is_vcn_in_mainline: { + min_sdk_version: "36", + static_libs: ["android.net.vcn.flags-aconfig-java"], + apex_available: ["com.android.tethering"], + + conditions_default: { + min_sdk_version: "35", + static_libs: ["android.net.vcn.flags-aconfig-java-export"], + apex_available: ["//apex_available:platform"], + }, + }, + }, +} + +soong_config_module_type { + name: "framework_connectivity_b_java_sdk_library_defaults_soong_config", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: [ + "is_vcn_in_mainline", + ], + properties: [ + "aconfig_declarations", + "jarjar_rules", + ], +} + +framework_connectivity_b_java_sdk_library_defaults_soong_config { + name: "framework-connectivity-b-java-sdk-library-defaults", + soong_config_variables: { + is_vcn_in_mainline: { + aconfig_declarations: ["android.net.vcn.flags-aconfig-java"], + + // TODO: b/375213246 Use the connectivity jarjar rule generator to create the + // jarjar rules. In the end state, use "framework-connectivity-jarjar-rules" + // after VCN code is moved to the Connectivity folder + jarjar_rules: "framework-vcn-jarjar-rules.txt", + + conditions_default: { + aconfig_declarations: ["android.net.vcn.flags-aconfig-java-export"], + + // Use "android.net.connectivity" as prefix would trigger + // "Hidden API flags are inconsistent" build error + jarjar_rules: "framework-vcn-jarjar-rules-platform.txt", + }, + }, + }, +} + +java_sdk_library { + name: "framework-connectivity-b", + defaults: [ + "framework-connectivity-b-defaults", + "framework-connectivity-b-java-sdk-library-defaults", + ], + + permitted_packages: [ + "android.net", + "android.net.vcn", + "com.android.server.vcn.util", + + ], + api_packages: [ + "android.net", + "android.net.vcn", + ], + + // Allow VCN APIs to reference APIs in IKE and Connectivity + stub_only_libs: [ + "android.net.ipsec.ike.stubs.module_lib", + "framework-connectivity.stubs.module_lib", + ], + + // To use non-jarjard names of utilities such as android.util.IndentingPrintWriter + impl_only_libs: [ + "framework-connectivity-pre-jarjar", + ], + + impl_library_visibility: [ + // Using for test only + "//cts/tests/netlegacy22.api", + + // TODO: b/374174952 Clean up VCN paths when the code location is finalized + "//cts/tests/tests/vcn", + "//packages/modules/Connectivity/Vcn/tests:__subpackages__", + + "//external/sl4a:__subpackages__", + "//frameworks/base/core/tests/bandwidthtests", + "//frameworks/base/core/tests/benchmarks", + "//frameworks/base/core/tests/utillib", + "//frameworks/base/services/tests/VpnTests", + "//frameworks/base/tests/vcn", + "//frameworks/opt/telephony/tests/telephonytests", + "//packages/modules/CaptivePortalLogin/tests", + "//packages/modules/Connectivity/staticlibs/testutils", + "//packages/modules/Connectivity/staticlibs/tests:__subpackages__", + "//packages/modules/Connectivity/Tethering/tests:__subpackages__", + "//packages/modules/Connectivity/tests:__subpackages__", + "//packages/modules/Connectivity/thread/tests:__subpackages__", + "//packages/modules/IPsec/tests/iketests", + "//packages/modules/NetworkStack", + "//packages/modules/NetworkStack/tests:__subpackages__", + "//packages/modules/Wifi/service/tests/wifitests", + ], + + visibility: ["//packages/modules/Connectivity:__subpackages__"], +} + +java_library { + name: "framework-connectivity-b-pre-jarjar", + defaults: ["framework-connectivity-b-defaults"], + installable: false, + libs: [ + "framework-connectivity-pre-jarjar", + ], +} diff --git a/Vcn/framework-b/api/current.txt b/Vcn/framework-b/api/current.txt new file mode 100644 index 0000000000..831b74158e --- /dev/null +++ b/Vcn/framework-b/api/current.txt @@ -0,0 +1,123 @@ +// Signature format: 2.0 +package android.net.vcn { + + public final class VcnCellUnderlyingNetworkTemplate extends android.net.vcn.VcnUnderlyingNetworkTemplate { + method public int getCbs(); + method public int getDun(); + method public int getIms(); + method public int getInternet(); + method public int getMms(); + method @NonNull public java.util.Set getOperatorPlmnIds(); + method public int getOpportunistic(); + method public int getRcs(); + method public int getRoaming(); + method @NonNull public java.util.Set getSimSpecificCarrierIds(); + } + + public static final class VcnCellUnderlyingNetworkTemplate.Builder { + ctor public VcnCellUnderlyingNetworkTemplate.Builder(); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate build(); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setCbs(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setDun(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setIms(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setInternet(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setMetered(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setMinDownstreamBandwidthKbps(int, int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setMinUpstreamBandwidthKbps(int, int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setMms(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setOperatorPlmnIds(@NonNull java.util.Set); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setOpportunistic(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setRcs(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setRoaming(int); + method @NonNull public android.net.vcn.VcnCellUnderlyingNetworkTemplate.Builder setSimSpecificCarrierIds(@NonNull java.util.Set); + } + + public final class VcnConfig implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.Set getGatewayConnectionConfigs(); + method @NonNull public java.util.Set getRestrictedUnderlyingNetworkTransports(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public static final class VcnConfig.Builder { + ctor public VcnConfig.Builder(@NonNull android.content.Context); + method @NonNull public android.net.vcn.VcnConfig.Builder addGatewayConnectionConfig(@NonNull android.net.vcn.VcnGatewayConnectionConfig); + method @NonNull public android.net.vcn.VcnConfig build(); + method @NonNull public android.net.vcn.VcnConfig.Builder setRestrictedUnderlyingNetworkTransports(@NonNull java.util.Set); + } + + public final class VcnGatewayConnectionConfig { + method @NonNull public int[] getExposedCapabilities(); + method @NonNull public String getGatewayConnectionName(); + method @IntRange(from=0x500) public int getMaxMtu(); + method public int getMinUdpPort4500NatTimeoutSeconds(); + method @NonNull public long[] getRetryIntervalsMillis(); + method @NonNull public java.util.List getVcnUnderlyingNetworkPriorities(); + method public boolean hasGatewayOption(int); + method @FlaggedApi("android.net.vcn.safe_mode_config") public boolean isSafeModeEnabled(); + field @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET = -1; // 0xffffffff + field public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; // 0x0 + } + + public static final class VcnGatewayConnectionConfig.Builder { + ctor public VcnGatewayConnectionConfig.Builder(@NonNull String, @NonNull android.net.ipsec.ike.IkeTunnelConnectionParams); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addExposedCapability(int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder addGatewayOption(int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig build(); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeExposedCapability(int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder removeGatewayOption(int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMaxMtu(@IntRange(from=0x500) int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setMinUdpPort4500NatTimeoutSeconds(@IntRange(from=0x78) int); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setRetryIntervalsMillis(@NonNull long[]); + method @FlaggedApi("android.net.vcn.safe_mode_config") @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setSafeModeEnabled(boolean); + method @NonNull public android.net.vcn.VcnGatewayConnectionConfig.Builder setVcnUnderlyingNetworkPriorities(@NonNull java.util.List); + } + + public class VcnManager { + method @RequiresPermission("carrier privileges") public void clearVcnConfig(@NonNull android.os.ParcelUuid) throws java.io.IOException; + method @NonNull public java.util.List getConfiguredSubscriptionGroups(); + method public void registerVcnStatusCallback(@NonNull android.os.ParcelUuid, @NonNull java.util.concurrent.Executor, @NonNull android.net.vcn.VcnManager.VcnStatusCallback); + method @RequiresPermission("carrier privileges") public void setVcnConfig(@NonNull android.os.ParcelUuid, @NonNull android.net.vcn.VcnConfig) throws java.io.IOException; + method public void unregisterVcnStatusCallback(@NonNull android.net.vcn.VcnManager.VcnStatusCallback); + field public static final int VCN_ERROR_CODE_CONFIG_ERROR = 1; // 0x1 + field public static final int VCN_ERROR_CODE_INTERNAL_ERROR = 0; // 0x0 + field public static final int VCN_ERROR_CODE_NETWORK_ERROR = 2; // 0x2 + field public static final int VCN_STATUS_CODE_ACTIVE = 2; // 0x2 + field public static final int VCN_STATUS_CODE_INACTIVE = 1; // 0x1 + field public static final int VCN_STATUS_CODE_NOT_CONFIGURED = 0; // 0x0 + field public static final int VCN_STATUS_CODE_SAFE_MODE = 3; // 0x3 + } + + public abstract static class VcnManager.VcnStatusCallback { + ctor public VcnManager.VcnStatusCallback(); + method public abstract void onGatewayConnectionError(@NonNull String, int, @Nullable Throwable); + method public abstract void onStatusChanged(int); + } + + public abstract class VcnUnderlyingNetworkTemplate { + method public int getMetered(); + method public int getMinEntryDownstreamBandwidthKbps(); + method public int getMinEntryUpstreamBandwidthKbps(); + method public int getMinExitDownstreamBandwidthKbps(); + method public int getMinExitUpstreamBandwidthKbps(); + field public static final int MATCH_ANY = 0; // 0x0 + field public static final int MATCH_FORBIDDEN = 2; // 0x2 + field public static final int MATCH_REQUIRED = 1; // 0x1 + } + + public final class VcnWifiUnderlyingNetworkTemplate extends android.net.vcn.VcnUnderlyingNetworkTemplate { + method @NonNull public java.util.Set getSsids(); + } + + public static final class VcnWifiUnderlyingNetworkTemplate.Builder { + ctor public VcnWifiUnderlyingNetworkTemplate.Builder(); + method @NonNull public android.net.vcn.VcnWifiUnderlyingNetworkTemplate build(); + method @NonNull public android.net.vcn.VcnWifiUnderlyingNetworkTemplate.Builder setMetered(int); + method @NonNull public android.net.vcn.VcnWifiUnderlyingNetworkTemplate.Builder setMinDownstreamBandwidthKbps(int, int); + method @NonNull public android.net.vcn.VcnWifiUnderlyingNetworkTemplate.Builder setMinUpstreamBandwidthKbps(int, int); + method @NonNull public android.net.vcn.VcnWifiUnderlyingNetworkTemplate.Builder setSsids(@NonNull java.util.Set); + } + +} + diff --git a/Vcn/framework-b/api/module-lib-current.txt b/Vcn/framework-b/api/module-lib-current.txt new file mode 100644 index 0000000000..8961b2830f --- /dev/null +++ b/Vcn/framework-b/api/module-lib-current.txt @@ -0,0 +1,28 @@ +// Signature format: 2.0 +package android.net { + + @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public final class ConnectivityFrameworkInitializerBaklava { + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static void registerServiceWrappers(); + } + +} + +package android.net.vcn { + + @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public final class VcnTransportInfo implements android.os.Parcelable android.net.TransportInfo { + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public int describeContents(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public long getApplicableRedactions(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public int getMinUdpPort4500NatTimeoutSeconds(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.TransportInfo makeCopy(long); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public void writeToParcel(@NonNull android.os.Parcel, int); + field @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public static final class VcnTransportInfo.Builder { + ctor @FlaggedApi("android.net.vcn.mainline_vcn_module_api") public VcnTransportInfo.Builder(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.vcn.VcnTransportInfo build(); + method @FlaggedApi("android.net.vcn.mainline_vcn_module_api") @NonNull public android.net.vcn.VcnTransportInfo.Builder setMinUdpPort4500NatTimeoutSeconds(@IntRange(from=0x78) int); + } + +} + diff --git a/Vcn/framework-b/api/module-lib-removed.txt b/Vcn/framework-b/api/module-lib-removed.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Vcn/framework-b/api/module-lib-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Vcn/framework-b/api/removed.txt b/Vcn/framework-b/api/removed.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Vcn/framework-b/api/removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Vcn/framework-b/api/system-current.txt b/Vcn/framework-b/api/system-current.txt new file mode 100644 index 0000000000..9c5a67701b --- /dev/null +++ b/Vcn/framework-b/api/system-current.txt @@ -0,0 +1,23 @@ +// Signature format: 2.0 +package android.net.vcn { + + public class VcnManager { + method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void addVcnNetworkPolicyChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.net.vcn.VcnManager.VcnNetworkPolicyChangeListener); + method @NonNull @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public android.net.vcn.VcnNetworkPolicyResult applyVcnNetworkPolicy(@NonNull android.net.NetworkCapabilities, @NonNull android.net.LinkProperties); + method @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) public void removeVcnNetworkPolicyChangeListener(@NonNull android.net.vcn.VcnManager.VcnNetworkPolicyChangeListener); + } + + public static interface VcnManager.VcnNetworkPolicyChangeListener { + method public void onPolicyChanged(); + } + + public final class VcnNetworkPolicyResult implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.net.NetworkCapabilities getNetworkCapabilities(); + method public boolean isTeardownRequested(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + +} + diff --git a/Vcn/framework-b/api/system-removed.txt b/Vcn/framework-b/api/system-removed.txt new file mode 100644 index 0000000000..d802177e24 --- /dev/null +++ b/Vcn/framework-b/api/system-removed.txt @@ -0,0 +1 @@ +// Signature format: 2.0 diff --git a/Vcn/framework-b/framework-vcn-jarjar-rules-platform.txt b/Vcn/framework-b/framework-vcn-jarjar-rules-platform.txt new file mode 100644 index 0000000000..757043bdbb --- /dev/null +++ b/Vcn/framework-b/framework-vcn-jarjar-rules-platform.txt @@ -0,0 +1,2 @@ +rule android.net.vcn.persistablebundleutils.** android.net.vcn.module.repackaged.persistablebundleutils.@1 +rule android.net.vcn.util.** android.net.vcn.module.repackaged.util.@1 \ No newline at end of file diff --git a/Vcn/framework-b/framework-vcn-jarjar-rules.txt b/Vcn/framework-b/framework-vcn-jarjar-rules.txt new file mode 100644 index 0000000000..33287b7f34 --- /dev/null +++ b/Vcn/framework-b/framework-vcn-jarjar-rules.txt @@ -0,0 +1,3 @@ +rule android.net.vcn.persistablebundleutils.** android.net.connectivity.android.net.vcn.persistablebundleutils.@1 +rule android.net.vcn.util.** android.net.connectivity.android.net.vcn.util.@1 +rule android.util.IndentingPrintWriter android.net.connectivity.android.util.IndentingPrintWriter \ No newline at end of file diff --git a/Vcn/framework-b/src/android/net/ConnectivityFrameworkInitializerBaklava.java b/Vcn/framework-b/src/android/net/ConnectivityFrameworkInitializerBaklava.java new file mode 100644 index 0000000000..de22ca6848 --- /dev/null +++ b/Vcn/framework-b/src/android/net/ConnectivityFrameworkInitializerBaklava.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2024 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; + +import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API; + +import android.annotation.FlaggedApi; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.SystemServiceRegistry; +import android.compat.Compatibility; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.vcn.IVcnManagementService; +import android.net.vcn.VcnManager; +import android.os.Build; +import android.os.SystemProperties; + +/** + * Class for performing registration for VCN service. + * + * @hide + */ +@FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class ConnectivityFrameworkInitializerBaklava { + + // This is a copy of TelephonyFrameworkInitializer.ENABLE_CHECKING_TELEPHONY_FEATURES. This + // ChangeId will replace ENABLE_CHECKING_TELEPHONY_FEATURES_FOR_VCN to gate VcnManager + // feature flag enforcement. + // This replacement is safe because both ChangeIds have been enabled since Android V and serve + // the same purpose: enforcing telephony feature flag checks before using telephony-based + // features. This also simplifies VCN modularization by avoiding the need to handle different + // states, such as: SDK < B vs. SDK >= B; VCN in platform vs. VCN in the apex. + private static final long ENABLE_CHECKING_TELEPHONY_FEATURES = 330583731; + + /** + * The corresponding vendor API for Android V + * + *

Starting with Android V, the vendor API format has switched to YYYYMM. + * + * @see Vendor API + * level + */ + private static final int VENDOR_API_FOR_ANDROID_V = 202404; + + private ConnectivityFrameworkInitializerBaklava() {} + + // Suppressing AndroidFrameworkCompatChange because we're querying vendor + // partition SDK level, not application's target SDK version (which BTW we + // also check through Compatibility framework a few lines below). + @Nullable + private static String getVcnFeatureDependency() { + // Check SDK version of the client app. Apps targeting pre-V SDK might + // have not checked for existence of these features. + if (!Compatibility.isChangeEnabled(ENABLE_CHECKING_TELEPHONY_FEATURES)) { + return null; + } + + // Check SDK version of the vendor partition. Pre-V devices might have + // incorrectly under-declared telephony features. + final int vendorApiLevel = + SystemProperties.getInt( + "ro.vendor.api_level", Build.VERSION.DEVICE_INITIAL_SDK_INT); + if (vendorApiLevel < VENDOR_API_FOR_ANDROID_V) { + return PackageManager.FEATURE_TELEPHONY; + } else { + return PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION; + } + } + + /** + * Register VCN service to {@link Context}, so that {@link Context#getSystemService} can return + * a VcnManager. + * + * @throws IllegalStateException if this is called anywhere besides {@link + * SystemServiceRegistry}. + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static void registerServiceWrappers() { + SystemServiceRegistry.registerContextAwareService( + VcnManager.VCN_MANAGEMENT_SERVICE_STRING, + VcnManager.class, + (context, serviceBinder) -> { + final String telephonyFeatureToCheck = getVcnFeatureDependency(); + if (telephonyFeatureToCheck != null + && !context.getPackageManager() + .hasSystemFeature(telephonyFeatureToCheck)) { + return null; + } + IVcnManagementService service = + IVcnManagementService.Stub.asInterface(serviceBinder); + return new VcnManager(context, service); + }); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/IVcnManagementService.aidl b/Vcn/framework-b/src/android/net/vcn/IVcnManagementService.aidl new file mode 100644 index 0000000000..e16f6b1677 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/IVcnManagementService.aidl @@ -0,0 +1,43 @@ +/* + * Copyright 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 android.net.LinkProperties; +import android.net.NetworkCapabilities; +import android.net.vcn.IVcnStatusCallback; +import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; +import android.net.vcn.VcnConfig; +import android.net.vcn.VcnUnderlyingNetworkPolicy; +import android.os.ParcelUuid; + +import java.util.List; + +/** + * @hide + */ +interface IVcnManagementService { + void setVcnConfig(in ParcelUuid subscriptionGroup, in VcnConfig config, in String opPkgName); + void clearVcnConfig(in ParcelUuid subscriptionGroup, in String opPkgName); + List getConfiguredSubscriptionGroups(in String opPkgName); + + void addVcnUnderlyingNetworkPolicyListener(in IVcnUnderlyingNetworkPolicyListener listener); + void removeVcnUnderlyingNetworkPolicyListener(in IVcnUnderlyingNetworkPolicyListener listener); + VcnUnderlyingNetworkPolicy getUnderlyingNetworkPolicy(in NetworkCapabilities nc, in LinkProperties lp); + + void registerVcnStatusCallback(in ParcelUuid subscriptionGroup, in IVcnStatusCallback callback, in String opPkgName); + void unregisterVcnStatusCallback(in IVcnStatusCallback callback); +} diff --git a/Vcn/framework-b/src/android/net/vcn/IVcnStatusCallback.aidl b/Vcn/framework-b/src/android/net/vcn/IVcnStatusCallback.aidl new file mode 100644 index 0000000000..11bc443c9d --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/IVcnStatusCallback.aidl @@ -0,0 +1,27 @@ +/* + * Copyright 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 android.net.vcn; + +/** @hide */ +oneway interface IVcnStatusCallback { + void onVcnStatusChanged(int statusCode); + void onGatewayConnectionError( + in String gatewayConnectionName, + int errorCode, + in String exceptionClass, + in String exceptionMessage); +} \ No newline at end of file diff --git a/common/src/com/android/net/module/util/bpf/TetherStatsKey.java b/Vcn/framework-b/src/android/net/vcn/IVcnUnderlyingNetworkPolicyListener.aidl similarity index 56% rename from common/src/com/android/net/module/util/bpf/TetherStatsKey.java rename to Vcn/framework-b/src/android/net/vcn/IVcnUnderlyingNetworkPolicyListener.aidl index 68111b6a05..62de8216ce 100644 --- a/common/src/com/android/net/module/util/bpf/TetherStatsKey.java +++ b/Vcn/framework-b/src/android/net/vcn/IVcnUnderlyingNetworkPolicyListener.aidl @@ -14,18 +14,9 @@ * limitations under the License. */ -package com.android.net.module.util.bpf; +package android.net.vcn; -import com.android.net.module.util.Struct; -import com.android.net.module.util.Struct.Field; -import com.android.net.module.util.Struct.Type; - -/** The key of BpfMap which is used for tethering stats. */ -public class TetherStatsKey extends Struct { - @Field(order = 0, type = Type.S32) - public final int ifindex; // upstream interface index - - public TetherStatsKey(final int ifindex) { - this.ifindex = ifindex; - } -} +/** @hide */ +oneway interface IVcnUnderlyingNetworkPolicyListener { + void onPolicyChanged(); +} \ No newline at end of file diff --git a/common/src/com/android/net/module/util/bpf/CookieTagMapKey.java b/Vcn/framework-b/src/android/net/vcn/Placeholder.java similarity index 54% rename from common/src/com/android/net/module/util/bpf/CookieTagMapKey.java rename to Vcn/framework-b/src/android/net/vcn/Placeholder.java index 17da7a02f5..fb5e15386c 100644 --- a/common/src/com/android/net/module/util/bpf/CookieTagMapKey.java +++ b/Vcn/framework-b/src/android/net/vcn/Placeholder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2024 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. @@ -14,20 +14,12 @@ * limitations under the License. */ -package com.android.net.module.util.bpf; - -import com.android.net.module.util.Struct; -import com.android.net.module.util.Struct.Field; -import com.android.net.module.util.Struct.Type; +package android.net.vcn; /** - * Key for cookie tag map. + * Placeholder class so new framework-vcn isn't empty + * + * @hide */ -public class CookieTagMapKey extends Struct { - @Field(order = 0, type = Type.S64) - public final long socketCookie; - - public CookieTagMapKey(final long socketCookie) { - this.socketCookie = socketCookie; - } -} +// This class will be removed once source code is migrated +public class Placeholder {} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java b/Vcn/framework-b/src/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java new file mode 100644 index 0000000000..ded94159a9 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnCellUnderlyingNetworkTemplate.java @@ -0,0 +1,740 @@ +/* + * 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 android.net.vcn; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_CBS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_DUN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_IMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_MMS; +import static android.net.NetworkCapabilities.NET_CAPABILITY_RCS; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_ANY; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.getMatchCriteriaString; +import static android.net.vcn.util.PersistableBundleUtils.INTEGER_DESERIALIZER; +import static android.net.vcn.util.PersistableBundleUtils.INTEGER_SERIALIZER; +import static android.net.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER; +import static android.net.vcn.util.PersistableBundleUtils.STRING_SERIALIZER; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.net.NetworkCapabilities; +import android.net.vcn.VcnUnderlyingNetworkTemplate.MatchCriteria; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This class represents a configuration for a network template class of underlying cellular + * networks. + * + *

See {@link VcnUnderlyingNetworkTemplate} + */ +public final class VcnCellUnderlyingNetworkTemplate extends VcnUnderlyingNetworkTemplate { + private static final String ALLOWED_NETWORK_PLMN_IDS_KEY = "mAllowedNetworkPlmnIds"; + @NonNull private final Set mAllowedNetworkPlmnIds; + private static final String ALLOWED_SPECIFIC_CARRIER_IDS_KEY = "mAllowedSpecificCarrierIds"; + @NonNull private final Set mAllowedSpecificCarrierIds; + + private static final String ROAMING_MATCH_KEY = "mRoamingMatchCriteria"; + private static final int DEFAULT_ROAMING_MATCH_CRITERIA = MATCH_ANY; + private final int mRoamingMatchCriteria; + + private static final String OPPORTUNISTIC_MATCH_KEY = "mOpportunisticMatchCriteria"; + private static final int DEFAULT_OPPORTUNISTIC_MATCH_CRITERIA = MATCH_ANY; + private final int mOpportunisticMatchCriteria; + + private static final String CAPABILITIES_MATCH_CRITERIA_KEY = "mCapabilitiesMatchCriteria"; + @NonNull private final Map mCapabilitiesMatchCriteria; + + private static final Map CAPABILITIES_MATCH_CRITERIA_DEFAULT; + + static { + Map capsMatchCriteria = new HashMap<>(); + capsMatchCriteria.put(NET_CAPABILITY_CBS, MATCH_ANY); + capsMatchCriteria.put(NET_CAPABILITY_DUN, MATCH_ANY); + capsMatchCriteria.put(NET_CAPABILITY_IMS, MATCH_ANY); + capsMatchCriteria.put(NET_CAPABILITY_INTERNET, MATCH_REQUIRED); + capsMatchCriteria.put(NET_CAPABILITY_MMS, MATCH_ANY); + capsMatchCriteria.put(NET_CAPABILITY_RCS, MATCH_ANY); + + CAPABILITIES_MATCH_CRITERIA_DEFAULT = Collections.unmodifiableMap(capsMatchCriteria); + } + + private VcnCellUnderlyingNetworkTemplate( + int meteredMatchCriteria, + int minEntryUpstreamBandwidthKbps, + int minExitUpstreamBandwidthKbps, + int minEntryDownstreamBandwidthKbps, + int minExitDownstreamBandwidthKbps, + Set allowedNetworkPlmnIds, + Set allowedSpecificCarrierIds, + int roamingMatchCriteria, + int opportunisticMatchCriteria, + Map capabilitiesMatchCriteria) { + super( + NETWORK_PRIORITY_TYPE_CELL, + meteredMatchCriteria, + minEntryUpstreamBandwidthKbps, + minExitUpstreamBandwidthKbps, + minEntryDownstreamBandwidthKbps, + minExitDownstreamBandwidthKbps); + mAllowedNetworkPlmnIds = new ArraySet<>(allowedNetworkPlmnIds); + mAllowedSpecificCarrierIds = new ArraySet<>(allowedSpecificCarrierIds); + mRoamingMatchCriteria = roamingMatchCriteria; + mOpportunisticMatchCriteria = opportunisticMatchCriteria; + mCapabilitiesMatchCriteria = new HashMap<>(capabilitiesMatchCriteria); + + validate(); + } + + /** @hide */ + @Override + protected void validate() { + super.validate(); + validatePlmnIds(mAllowedNetworkPlmnIds); + validateCapabilitiesMatchCriteria(mCapabilitiesMatchCriteria); + Objects.requireNonNull(mAllowedSpecificCarrierIds, "matchingCarrierIds is null"); + validateMatchCriteria(mRoamingMatchCriteria, "mRoamingMatchCriteria"); + validateMatchCriteria(mOpportunisticMatchCriteria, "mOpportunisticMatchCriteria"); + } + + private static void validatePlmnIds(Set matchingOperatorPlmnIds) { + Objects.requireNonNull(matchingOperatorPlmnIds, "matchingOperatorPlmnIds is null"); + + // A valid PLMN is a concatenation of MNC and MCC, and thus consists of 5 or 6 decimal + // digits. + for (String id : matchingOperatorPlmnIds) { + if ((id.length() == 5 || id.length() == 6) && id.matches("[0-9]+")) { + continue; + } else { + throw new IllegalArgumentException("Found invalid PLMN ID: " + id); + } + } + } + + private static void validateCapabilitiesMatchCriteria( + Map capabilitiesMatchCriteria) { + Objects.requireNonNull(capabilitiesMatchCriteria, "capabilitiesMatchCriteria is null"); + + boolean requiredCapabilityFound = false; + for (Map.Entry entry : capabilitiesMatchCriteria.entrySet()) { + final int capability = entry.getKey(); + final int matchCriteria = entry.getValue(); + + Preconditions.checkArgument( + CAPABILITIES_MATCH_CRITERIA_DEFAULT.containsKey(capability), + "NetworkCapability " + capability + "out of range"); + validateMatchCriteria(matchCriteria, "capability " + capability); + + requiredCapabilityFound |= (matchCriteria == MATCH_REQUIRED); + } + + if (!requiredCapabilityFound) { + throw new IllegalArgumentException("No required capabilities found"); + } + } + + /** @hide */ + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public static VcnCellUnderlyingNetworkTemplate fromPersistableBundle( + @NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle is null"); + + final int meteredMatchCriteria = in.getInt(METERED_MATCH_KEY); + + final int minEntryUpstreamBandwidthKbps = + in.getInt(MIN_ENTRY_UPSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + final int minExitUpstreamBandwidthKbps = + in.getInt(MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + final int minEntryDownstreamBandwidthKbps = + in.getInt(MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + final int minExitDownstreamBandwidthKbps = + in.getInt(MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + + final PersistableBundle plmnIdsBundle = + in.getPersistableBundle(ALLOWED_NETWORK_PLMN_IDS_KEY); + Objects.requireNonNull(plmnIdsBundle, "plmnIdsBundle is null"); + final Set allowedNetworkPlmnIds = + new ArraySet( + PersistableBundleUtils.toList(plmnIdsBundle, STRING_DESERIALIZER)); + + final PersistableBundle specificCarrierIdsBundle = + in.getPersistableBundle(ALLOWED_SPECIFIC_CARRIER_IDS_KEY); + Objects.requireNonNull(specificCarrierIdsBundle, "specificCarrierIdsBundle is null"); + final Set allowedSpecificCarrierIds = + new ArraySet( + PersistableBundleUtils.toList( + specificCarrierIdsBundle, INTEGER_DESERIALIZER)); + + final PersistableBundle capabilitiesMatchCriteriaBundle = + in.getPersistableBundle(CAPABILITIES_MATCH_CRITERIA_KEY); + final Map capabilitiesMatchCriteria; + if (capabilitiesMatchCriteriaBundle == null) { + capabilitiesMatchCriteria = CAPABILITIES_MATCH_CRITERIA_DEFAULT; + } else { + capabilitiesMatchCriteria = + PersistableBundleUtils.toMap( + capabilitiesMatchCriteriaBundle, + INTEGER_DESERIALIZER, + INTEGER_DESERIALIZER); + } + + final int roamingMatchCriteria = in.getInt(ROAMING_MATCH_KEY); + final int opportunisticMatchCriteria = in.getInt(OPPORTUNISTIC_MATCH_KEY); + + return new VcnCellUnderlyingNetworkTemplate( + meteredMatchCriteria, + minEntryUpstreamBandwidthKbps, + minExitUpstreamBandwidthKbps, + minEntryDownstreamBandwidthKbps, + minExitDownstreamBandwidthKbps, + allowedNetworkPlmnIds, + allowedSpecificCarrierIds, + roamingMatchCriteria, + opportunisticMatchCriteria, + capabilitiesMatchCriteria); + } + + /** @hide */ + @Override + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = super.toPersistableBundle(); + + final PersistableBundle plmnIdsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mAllowedNetworkPlmnIds), STRING_SERIALIZER); + result.putPersistableBundle(ALLOWED_NETWORK_PLMN_IDS_KEY, plmnIdsBundle); + + final PersistableBundle specificCarrierIdsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mAllowedSpecificCarrierIds), INTEGER_SERIALIZER); + result.putPersistableBundle(ALLOWED_SPECIFIC_CARRIER_IDS_KEY, specificCarrierIdsBundle); + + final PersistableBundle capabilitiesMatchCriteriaBundle = + PersistableBundleUtils.fromMap( + mCapabilitiesMatchCriteria, INTEGER_SERIALIZER, INTEGER_SERIALIZER); + result.putPersistableBundle( + CAPABILITIES_MATCH_CRITERIA_KEY, capabilitiesMatchCriteriaBundle); + + result.putInt(ROAMING_MATCH_KEY, mRoamingMatchCriteria); + result.putInt(OPPORTUNISTIC_MATCH_KEY, mOpportunisticMatchCriteria); + + return result; + } + + /** + * Retrieve the matching operator PLMN IDs, or an empty set if any PLMN ID is acceptable. + * + * @see Builder#setOperatorPlmnIds(Set) + */ + @NonNull + public Set getOperatorPlmnIds() { + return Collections.unmodifiableSet(mAllowedNetworkPlmnIds); + } + + /** + * Retrieve the matching sim specific carrier IDs, or an empty set if any sim specific carrier + * ID is acceptable. + * + * @see Builder#setSimSpecificCarrierIds(Set) + */ + @NonNull + public Set getSimSpecificCarrierIds() { + return Collections.unmodifiableSet(mAllowedSpecificCarrierIds); + } + + /** + * Return the matching criteria for roaming networks. + * + * @see Builder#setRoaming(int) + */ + @MatchCriteria + public int getRoaming() { + return mRoamingMatchCriteria; + } + + /** + * Return the matching criteria for opportunistic cellular subscriptions. + * + * @see Builder#setOpportunistic(int) + */ + @MatchCriteria + public int getOpportunistic() { + return mOpportunisticMatchCriteria; + } + + /** + * Returns the matching criteria for CBS networks. + * + * @see Builder#setCbs(int) + */ + @MatchCriteria + public int getCbs() { + return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_CBS); + } + + /** + * Returns the matching criteria for DUN networks. + * + * @see Builder#setDun(int) + */ + @MatchCriteria + public int getDun() { + return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_DUN); + } + /** + * Returns the matching criteria for IMS networks. + * + * @see Builder#setIms(int) + */ + @MatchCriteria + public int getIms() { + return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_IMS); + } + /** + * Returns the matching criteria for INTERNET networks. + * + * @see Builder#setInternet(int) + */ + @MatchCriteria + public int getInternet() { + return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_INTERNET); + } + /** + * Returns the matching criteria for MMS networks. + * + * @see Builder#setMms(int) + */ + @MatchCriteria + public int getMms() { + return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_MMS); + } + + /** + * Returns the matching criteria for RCS networks. + * + * @see Builder#setRcs(int) + */ + @MatchCriteria + public int getRcs() { + return mCapabilitiesMatchCriteria.get(NET_CAPABILITY_RCS); + } + + /** @hide */ + @Override + public Map getCapabilitiesMatchCriteria() { + return Collections.unmodifiableMap(new HashMap<>(mCapabilitiesMatchCriteria)); + } + + @Override + public int hashCode() { + return Objects.hash( + super.hashCode(), + mAllowedNetworkPlmnIds, + mAllowedSpecificCarrierIds, + mCapabilitiesMatchCriteria, + mRoamingMatchCriteria, + mOpportunisticMatchCriteria); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!super.equals(other)) { + return false; + } + + if (!(other instanceof VcnCellUnderlyingNetworkTemplate)) { + return false; + } + + final VcnCellUnderlyingNetworkTemplate rhs = (VcnCellUnderlyingNetworkTemplate) other; + return Objects.equals(mAllowedNetworkPlmnIds, rhs.mAllowedNetworkPlmnIds) + && Objects.equals(mAllowedSpecificCarrierIds, rhs.mAllowedSpecificCarrierIds) + && Objects.equals(mCapabilitiesMatchCriteria, rhs.mCapabilitiesMatchCriteria) + && mRoamingMatchCriteria == rhs.mRoamingMatchCriteria + && mOpportunisticMatchCriteria == rhs.mOpportunisticMatchCriteria; + } + + /** @hide */ + @Override + void dumpTransportSpecificFields(IndentingPrintWriter pw) { + if (!mAllowedNetworkPlmnIds.isEmpty()) { + pw.println("mAllowedNetworkPlmnIds: " + mAllowedNetworkPlmnIds); + } + if (!mAllowedNetworkPlmnIds.isEmpty()) { + pw.println("mAllowedSpecificCarrierIds: " + mAllowedSpecificCarrierIds); + } + pw.println("mCapabilitiesMatchCriteria: " + mCapabilitiesMatchCriteria); + if (mRoamingMatchCriteria != DEFAULT_ROAMING_MATCH_CRITERIA) { + pw.println("mRoamingMatchCriteria: " + getMatchCriteriaString(mRoamingMatchCriteria)); + } + if (mOpportunisticMatchCriteria != DEFAULT_OPPORTUNISTIC_MATCH_CRITERIA) { + pw.println( + "mOpportunisticMatchCriteria: " + + getMatchCriteriaString(mOpportunisticMatchCriteria)); + } + } + + /** This class is used to incrementally build VcnCellUnderlyingNetworkTemplate objects. */ + public static final class Builder { + private int mMeteredMatchCriteria = DEFAULT_METERED_MATCH_CRITERIA; + + @NonNull private final Set mAllowedNetworkPlmnIds = new ArraySet<>(); + @NonNull private final Set mAllowedSpecificCarrierIds = new ArraySet<>(); + @NonNull private final Map mCapabilitiesMatchCriteria = new HashMap<>(); + + private int mRoamingMatchCriteria = DEFAULT_ROAMING_MATCH_CRITERIA; + private int mOpportunisticMatchCriteria = DEFAULT_OPPORTUNISTIC_MATCH_CRITERIA; + + private int mMinEntryUpstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + private int mMinExitUpstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + private int mMinEntryDownstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + private int mMinExitDownstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + + /** Construct a Builder object. */ + public Builder() { + mCapabilitiesMatchCriteria.putAll(CAPABILITIES_MATCH_CRITERIA_DEFAULT); + } + + /** + * Set the matching criteria for metered networks. + * + *

A template where setMetered(MATCH_REQUIRED) will only match metered networks (one + * without NET_CAPABILITY_NOT_METERED). A template where setMetered(MATCH_FORBIDDEN) will + * only match a network that is not metered (one with NET_CAPABILITY_NOT_METERED). + * + * @param matchCriteria the matching criteria for metered networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_NOT_METERED + */ + // The matching getter is defined in the super class. Please see {@link + // VcnUnderlyingNetworkTemplate#getMetered()} + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder setMetered(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setMetered"); + + mMeteredMatchCriteria = matchCriteria; + return this; + } + + /** + * Set operator PLMN IDs with which a network can match this template. + * + *

This is used to distinguish cases where roaming agreements may dictate a different + * priority from a partner's networks. + * + * @param operatorPlmnIds the matching operator PLMN IDs in String. Network with one of the + * matching PLMN IDs can match this template. If the set is empty, any PLMN ID will + * match. The default is an empty set. A valid PLMN is a concatenation of MNC and MCC, + * and thus consists of 5 or 6 decimal digits. + * @see SubscriptionInfo#getMccString() + * @see SubscriptionInfo#getMncString() + */ + @NonNull + public Builder setOperatorPlmnIds(@NonNull Set operatorPlmnIds) { + validatePlmnIds(operatorPlmnIds); + + mAllowedNetworkPlmnIds.clear(); + mAllowedNetworkPlmnIds.addAll(operatorPlmnIds); + return this; + } + + /** + * Set sim specific carrier IDs with which a network can match this template. + * + * @param simSpecificCarrierIds the matching sim specific carrier IDs. Network with one of + * the sim specific carrier IDs can match this template. If the set is empty, any + * carrier ID will match. The default is an empty set. + * @see TelephonyManager#getSimSpecificCarrierId() + */ + @NonNull + public Builder setSimSpecificCarrierIds(@NonNull Set simSpecificCarrierIds) { + Objects.requireNonNull(simSpecificCarrierIds, "simSpecificCarrierIds is null"); + + mAllowedSpecificCarrierIds.clear(); + mAllowedSpecificCarrierIds.addAll(simSpecificCarrierIds); + return this; + } + + /** + * Set the matching criteria for roaming networks. + * + *

A template where setRoaming(MATCH_REQUIRED) will only match roaming networks (one + * without NET_CAPABILITY_NOT_ROAMING). A template where setRoaming(MATCH_FORBIDDEN) will + * only match a network that is not roaming (one with NET_CAPABILITY_NOT_ROAMING). + * + * @param matchCriteria the matching criteria for roaming networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING + */ + @NonNull + public Builder setRoaming(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setRoaming"); + + mRoamingMatchCriteria = matchCriteria; + return this; + } + + /** + * Set the matching criteria for opportunistic cellular subscriptions. + * + * @param matchCriteria the matching criteria for opportunistic cellular subscriptions. + * Defaults to {@link #MATCH_ANY}. + * @see SubscriptionManager#setOpportunistic(boolean, int) + */ + @NonNull + public Builder setOpportunistic(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setOpportunistic"); + + mOpportunisticMatchCriteria = matchCriteria; + return this; + } + + /** + * Set the minimum upstream bandwidths that this template will match. + * + *

This template will not match a network that does not provide at least the bandwidth + * passed as the entry bandwidth, except in the case that the network is selected as the VCN + * Gateway Connection's underlying network, where it will continue to match until the + * bandwidth drops under the exit bandwidth. + * + *

The entry criteria MUST be greater than, or equal to the exit criteria to avoid the + * invalid case where a network fulfills the entry criteria, but at the same time fails the + * exit criteria. + * + *

Estimated bandwidth of a network is provided by the transport layer, and reported in + * {@link NetworkCapabilities}. The provided estimates will be used without modification. + * + * @param minEntryUpstreamBandwidthKbps the minimum accepted upstream bandwidth for networks + * that ARE NOT the already-selected underlying network, or {@code 0} to disable this + * requirement. Disabled by default. + * @param minExitUpstreamBandwidthKbps the minimum accepted upstream bandwidth for a network + * that IS the already-selected underlying network, or {@code 0} to disable this + * requirement. Disabled by default. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + // The getter for the two integers are separated, and in the superclass. Please see {@link + // VcnUnderlyingNetworkTemplate#getMinEntryUpstreamBandwidthKbps()} and {@link + // VcnUnderlyingNetworkTemplate#getMinExitUpstreamBandwidthKbps()} + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setMinUpstreamBandwidthKbps( + int minEntryUpstreamBandwidthKbps, int minExitUpstreamBandwidthKbps) { + validateMinBandwidthKbps(minEntryUpstreamBandwidthKbps, minExitUpstreamBandwidthKbps); + + mMinEntryUpstreamBandwidthKbps = minEntryUpstreamBandwidthKbps; + mMinExitUpstreamBandwidthKbps = minExitUpstreamBandwidthKbps; + + return this; + } + + /** + * Set the minimum upstream bandwidths that this template will match. + * + *

This template will not match a network that does not provide at least the bandwidth + * passed as the entry bandwidth, except in the case that the network is selected as the VCN + * Gateway Connection's underlying network, where it will continue to match until the + * bandwidth drops under the exit bandwidth. + * + *

The entry criteria MUST be greater than, or equal to the exit criteria to avoid the + * invalid case where a network fulfills the entry criteria, but at the same time fails the + * exit criteria. + * + *

Estimated bandwidth of a network is provided by the transport layer, and reported in + * {@link NetworkCapabilities}. The provided estimates will be used without modification. + * + * @param minEntryDownstreamBandwidthKbps the minimum accepted downstream bandwidth for + * networks that ARE NOT the already-selected underlying network, or {@code 0} to + * disable this requirement. Disabled by default. + * @param minExitDownstreamBandwidthKbps the minimum accepted downstream bandwidth for a + * network that IS the already-selected underlying network, or {@code 0} to disable this + * requirement. Disabled by default. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + // The getter for the two integers are separated, and in the superclass. Please see {@link + // VcnUnderlyingNetworkTemplate#getMinEntryDownstreamBandwidthKbps()} and {@link + // VcnUnderlyingNetworkTemplate#getMinExitDownstreamBandwidthKbps()} + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setMinDownstreamBandwidthKbps( + int minEntryDownstreamBandwidthKbps, int minExitDownstreamBandwidthKbps) { + validateMinBandwidthKbps( + minEntryDownstreamBandwidthKbps, minExitDownstreamBandwidthKbps); + + mMinEntryDownstreamBandwidthKbps = minEntryDownstreamBandwidthKbps; + mMinExitDownstreamBandwidthKbps = minExitDownstreamBandwidthKbps; + + return this; + } + + /** + * Sets the matching criteria for CBS networks. + * + *

A template where {@code setCbs(MATCH_REQUIRED)} is called will only match CBS networks + * (ones with NET_CAPABILITY_CBS). A template where {@code setCbs(MATCH_FORBIDDEN)} is + * called will only match networks that do not support CBS (ones without + * NET_CAPABILITY_CBS). + * + * @param matchCriteria the matching criteria for CBS networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_CBS + */ + @NonNull + public Builder setCbs(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setCbs"); + + mCapabilitiesMatchCriteria.put(NET_CAPABILITY_CBS, matchCriteria); + return this; + } + + /** + * Sets the matching criteria for DUN networks. + * + *

A template where {@code setDun(MATCH_REQUIRED)} is called will only match DUN networks + * (ones with NET_CAPABILITY_DUN). A template where {@code setDun(MATCH_FORBIDDEN)} is + * called will only match networks that do not support DUN (ones without + * NET_CAPABILITY_DUN). + * + * @param matchCriteria the matching criteria for DUN networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_DUN + */ + @NonNull + public Builder setDun(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setDun"); + + mCapabilitiesMatchCriteria.put(NET_CAPABILITY_DUN, matchCriteria); + return this; + } + + /** + * Sets the matching criteria for IMS networks. + * + *

A template where {@code setIms(MATCH_REQUIRED)} is called will only match IMS networks + * (ones with NET_CAPABILITY_IMS). A template where {@code setIms(MATCH_FORBIDDEN)} is + * called will only match networks that do not support IMS (ones without + * NET_CAPABILITY_IMS). + * + * @param matchCriteria the matching criteria for IMS networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_IMS + */ + @NonNull + public Builder setIms(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setIms"); + + mCapabilitiesMatchCriteria.put(NET_CAPABILITY_IMS, matchCriteria); + return this; + } + + /** + * Sets the matching criteria for INTERNET networks. + * + *

A template where {@code setInternet(MATCH_REQUIRED)} is called will only match + * INTERNET networks (ones with NET_CAPABILITY_INTERNET). A template where {@code + * setInternet(MATCH_FORBIDDEN)} is called will only match networks that do not support + * INTERNET (ones without NET_CAPABILITY_INTERNET). + * + * @param matchCriteria the matching criteria for INTERNET networks. Defaults to {@link + * #MATCH_REQUIRED}. + * @see NetworkCapabilities#NET_CAPABILITY_INTERNET + */ + @NonNull + public Builder setInternet(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setInternet"); + + mCapabilitiesMatchCriteria.put(NET_CAPABILITY_INTERNET, matchCriteria); + return this; + } + + /** + * Sets the matching criteria for MMS networks. + * + *

A template where {@code setMms(MATCH_REQUIRED)} is called will only match MMS networks + * (ones with NET_CAPABILITY_MMS). A template where {@code setMms(MATCH_FORBIDDEN)} is + * called will only match networks that do not support MMS (ones without + * NET_CAPABILITY_MMS). + * + * @param matchCriteria the matching criteria for MMS networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_MMS + */ + @NonNull + public Builder setMms(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setMms"); + + mCapabilitiesMatchCriteria.put(NET_CAPABILITY_MMS, matchCriteria); + return this; + } + + /** + * Sets the matching criteria for RCS networks. + * + *

A template where {@code setRcs(MATCH_REQUIRED)} is called will only match RCS networks + * (ones with NET_CAPABILITY_RCS). A template where {@code setRcs(MATCH_FORBIDDEN)} is + * called will only match networks that do not support RCS (ones without + * NET_CAPABILITY_RCS). + * + * @param matchCriteria the matching criteria for RCS networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_RCS + */ + @NonNull + public Builder setRcs(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setRcs"); + + mCapabilitiesMatchCriteria.put(NET_CAPABILITY_RCS, matchCriteria); + return this; + } + + /** Build the VcnCellUnderlyingNetworkTemplate. */ + @NonNull + public VcnCellUnderlyingNetworkTemplate build() { + return new VcnCellUnderlyingNetworkTemplate( + mMeteredMatchCriteria, + mMinEntryUpstreamBandwidthKbps, + mMinExitUpstreamBandwidthKbps, + mMinEntryDownstreamBandwidthKbps, + mMinExitDownstreamBandwidthKbps, + mAllowedNetworkPlmnIds, + mAllowedSpecificCarrierIds, + mRoamingMatchCriteria, + mOpportunisticMatchCriteria, + mCapabilitiesMatchCriteria); + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java b/Vcn/framework-b/src/android/net/vcn/VcnConfig.aidl similarity index 55% rename from Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java rename to Vcn/framework-b/src/android/net/vcn/VcnConfig.aidl index 68d694a748..67006a42a7 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetherLimitKey.java +++ b/Vcn/framework-b/src/android/net/vcn/VcnConfig.aidl @@ -14,18 +14,7 @@ * limitations under the License. */ -package com.android.networkstack.tethering; +package android.net.vcn; -import com.android.net.module.util.Struct; -import com.android.net.module.util.Struct.Field; -import com.android.net.module.util.Struct.Type; - -/** The key of BpfMap which is used for tethering per-interface limit. */ -public class TetherLimitKey extends Struct { - @Field(order = 0, type = Type.S32) - public final int ifindex; // upstream interface index - - public TetherLimitKey(final int ifindex) { - this.ifindex = ifindex; - } -} +/** @hide */ +parcelable VcnConfig; diff --git a/Vcn/framework-b/src/android/net/vcn/VcnConfig.java b/Vcn/framework-b/src/android/net/vcn/VcnConfig.java new file mode 100644 index 0000000000..0d0efb2f73 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnConfig.java @@ -0,0 +1,385 @@ +/* + * 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 android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.vcn.util.PersistableBundleUtils.INTEGER_DESERIALIZER; +import static android.net.vcn.util.PersistableBundleUtils.INTEGER_SERIALIZER; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.PersistableBundle; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +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 + */ +public final class VcnConfig implements Parcelable { + @NonNull private static final String TAG = VcnConfig.class.getSimpleName(); + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"TRANSPORT_"}, + value = { + NetworkCapabilities.TRANSPORT_CELLULAR, + NetworkCapabilities.TRANSPORT_WIFI, + }) + @Target({ElementType.TYPE_USE}) + public @interface VcnUnderlyingNetworkTransport {} + + private static final Set ALLOWED_TRANSPORTS = new ArraySet<>(); + + static { + ALLOWED_TRANSPORTS.add(TRANSPORT_WIFI); + ALLOWED_TRANSPORTS.add(TRANSPORT_CELLULAR); + ALLOWED_TRANSPORTS.add(TRANSPORT_TEST); + } + + private static final String PACKAGE_NAME_KEY = "mPackageName"; + @NonNull private final String mPackageName; + + private static final String GATEWAY_CONNECTION_CONFIGS_KEY = "mGatewayConnectionConfigs"; + @NonNull private final Set mGatewayConnectionConfigs; + + private static final Set RESTRICTED_TRANSPORTS_DEFAULT = + Collections.singleton(TRANSPORT_WIFI); + private static final String RESTRICTED_TRANSPORTS_KEY = "mRestrictedTransports"; + @NonNull private final Set mRestrictedTransports; + + private static final String IS_TEST_MODE_PROFILE_KEY = "mIsTestModeProfile"; + private final boolean mIsTestModeProfile; + + private VcnConfig( + @NonNull String packageName, + @NonNull Set gatewayConnectionConfigs, + @NonNull Set restrictedTransports, + boolean isTestModeProfile) { + mPackageName = packageName; + mGatewayConnectionConfigs = + Collections.unmodifiableSet(new ArraySet<>(gatewayConnectionConfigs)); + mRestrictedTransports = Collections.unmodifiableSet(new ArraySet<>(restrictedTransports)); + mIsTestModeProfile = isTestModeProfile; + + validate(); + } + + /** + * Deserializes a VcnConfig from a PersistableBundle. + * + * @hide + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public VcnConfig(@NonNull PersistableBundle in) { + mPackageName = in.getString(PACKAGE_NAME_KEY); + + final PersistableBundle gatewayConnectionConfigsBundle = + in.getPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY); + mGatewayConnectionConfigs = + new ArraySet<>( + PersistableBundleUtils.toList( + gatewayConnectionConfigsBundle, VcnGatewayConnectionConfig::new)); + + final PersistableBundle restrictedTransportsBundle = + in.getPersistableBundle(RESTRICTED_TRANSPORTS_KEY); + if (restrictedTransportsBundle == null) { + // RESTRICTED_TRANSPORTS_KEY was added in U and does not exist in VcnConfigs created in + // older platforms + mRestrictedTransports = RESTRICTED_TRANSPORTS_DEFAULT; + } else { + mRestrictedTransports = + new ArraySet( + PersistableBundleUtils.toList( + restrictedTransportsBundle, INTEGER_DESERIALIZER)); + } + + mIsTestModeProfile = in.getBoolean(IS_TEST_MODE_PROFILE_KEY); + + validate(); + } + + private void validate() { + Objects.requireNonNull(mPackageName, "packageName was null"); + Preconditions.checkCollectionNotEmpty( + mGatewayConnectionConfigs, "gatewayConnectionConfigs was empty"); + + final Iterator iterator = mRestrictedTransports.iterator(); + while (iterator.hasNext()) { + final int transport = iterator.next(); + if (!ALLOWED_TRANSPORTS.contains(transport)) { + iterator.remove(); + Log.w( + TAG, + "Found invalid transport " + + transport + + " which might be from a new version of VcnConfig"); + } + + if (transport == TRANSPORT_TEST && !mIsTestModeProfile) { + throw new IllegalArgumentException( + "Found TRANSPORT_TEST in a non-test-mode profile"); + } + } + } + + /** + * Retrieve the package name of the provisioning app. + * + * @hide + */ + @NonNull + public String getProvisioningPackageName() { + return mPackageName; + } + + /** Retrieves the set of configured GatewayConnection(s). */ + @NonNull + public Set getGatewayConnectionConfigs() { + return Collections.unmodifiableSet(mGatewayConnectionConfigs); + } + + /** + * Retrieve the transports that will be restricted by the VCN. + * + * @see Builder#setRestrictedUnderlyingNetworkTransports(Set) + */ + @NonNull + public Set<@VcnUnderlyingNetworkTransport Integer> getRestrictedUnderlyingNetworkTransports() { + return Collections.unmodifiableSet(mRestrictedTransports); + } + + /** + * Returns whether or not this VcnConfig is restricted to test networks. + * + * @hide + */ + public boolean isTestModeProfile() { + return mIsTestModeProfile; + } + + /** + * Serializes this object to a PersistableBundle. + * + * @hide + */ + @NonNull + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putString(PACKAGE_NAME_KEY, mPackageName); + + final PersistableBundle gatewayConnectionConfigsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mGatewayConnectionConfigs), + VcnGatewayConnectionConfig::toPersistableBundle); + result.putPersistableBundle(GATEWAY_CONNECTION_CONFIGS_KEY, gatewayConnectionConfigsBundle); + + final PersistableBundle restrictedTransportsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mRestrictedTransports), INTEGER_SERIALIZER); + result.putPersistableBundle(RESTRICTED_TRANSPORTS_KEY, restrictedTransportsBundle); + + result.putBoolean(IS_TEST_MODE_PROFILE_KEY, mIsTestModeProfile); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash( + mPackageName, mGatewayConnectionConfigs, mRestrictedTransports, mIsTestModeProfile); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof VcnConfig)) { + return false; + } + + final VcnConfig rhs = (VcnConfig) other; + return mPackageName.equals(rhs.mPackageName) + && mGatewayConnectionConfigs.equals(rhs.mGatewayConnectionConfigs) + && mRestrictedTransports.equals(rhs.mRestrictedTransports) + && mIsTestModeProfile == rhs.mIsTestModeProfile; + } + + // Parcelable methods + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel out, int flags) { + out.writeParcelable(toPersistableBundle(), flags); + } + + @NonNull + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @NonNull + public VcnConfig createFromParcel(Parcel in) { + return new VcnConfig((PersistableBundle) in.readParcelable(null, android.os.PersistableBundle.class)); + } + + @NonNull + public VcnConfig[] newArray(int size) { + return new VcnConfig[size]; + } + }; + + /** This class is used to incrementally build {@link VcnConfig} objects. */ + public static final class Builder { + @NonNull private final String mPackageName; + + @NonNull + private final Set mGatewayConnectionConfigs = new ArraySet<>(); + + @NonNull private final Set mRestrictedTransports = new ArraySet<>(); + + private boolean mIsTestModeProfile = false; + + public Builder(@NonNull Context context) { + Objects.requireNonNull(context, "context was null"); + + mPackageName = context.getOpPackageName(); + mRestrictedTransports.addAll(RESTRICTED_TRANSPORTS_DEFAULT); + } + + /** + * Adds a configuration for an individual gateway connection. + * + * @param gatewayConnectionConfig the configuration for an individual gateway connection + * @return this {@link Builder} instance, for chaining + * @throws IllegalArgumentException if a VcnGatewayConnectionConfig has already been set for + * this {@link VcnConfig} with the same GatewayConnection name (as returned via {@link + * VcnGatewayConnectionConfig#getGatewayConnectionName()}). + */ + @NonNull + public Builder addGatewayConnectionConfig( + @NonNull VcnGatewayConnectionConfig gatewayConnectionConfig) { + Objects.requireNonNull(gatewayConnectionConfig, "gatewayConnectionConfig was null"); + + for (final VcnGatewayConnectionConfig vcnGatewayConnectionConfig : + mGatewayConnectionConfigs) { + if (vcnGatewayConnectionConfig + .getGatewayConnectionName() + .equals(gatewayConnectionConfig.getGatewayConnectionName())) { + throw new IllegalArgumentException( + "GatewayConnection for specified name already exists"); + } + } + + mGatewayConnectionConfigs.add(gatewayConnectionConfig); + return this; + } + + private void validateRestrictedTransportsOrThrow(Set restrictedTransports) { + Objects.requireNonNull(restrictedTransports, "transports was null"); + + for (int transport : restrictedTransports) { + if (!ALLOWED_TRANSPORTS.contains(transport)) { + throw new IllegalArgumentException("Invalid transport " + transport); + } + } + } + + /** + * Sets transports that will be restricted by the VCN. + * + *

In general, apps will not be able to bind to, or use a restricted network. In other + * words, unless the network type is marked restricted, any app can opt to use underlying + * networks, instead of through the VCN. + * + * @param transports transports that will be restricted by VCN. Networks that include any of + * the transports will be marked as restricted. {@link + * NetworkCapabilities#TRANSPORT_WIFI} is marked restricted by default. + * @return this {@link Builder} instance, for chaining + * @throws IllegalArgumentException if the input contains unsupported transport types. + * @see NetworkCapabilities#NET_CAPABILITY_NOT_RESTRICTED + */ + @NonNull + public Builder setRestrictedUnderlyingNetworkTransports( + @NonNull Set<@VcnUnderlyingNetworkTransport Integer> transports) { + validateRestrictedTransportsOrThrow(transports); + + mRestrictedTransports.clear(); + mRestrictedTransports.addAll(transports); + return this; + } + + /** + * Restricts this VcnConfig to matching with test networks (only). + * + *

This method is for testing only, and must not be used by apps. Calling {@link + * VcnManager#setVcnConfig(ParcelUuid, VcnConfig)} with a VcnConfig where test-network usage + * is enabled will require the MANAGE_TEST_NETWORKS permission. + * + * @return this {@link Builder} instance, for chaining + * @hide + */ + @NonNull + public Builder setIsTestModeProfile() { + mIsTestModeProfile = true; + return this; + } + + /** + * Builds and validates the VcnConfig. + * + * @return an immutable VcnConfig instance + */ + @NonNull + public VcnConfig build() { + return new VcnConfig( + mPackageName, + mGatewayConnectionConfigs, + mRestrictedTransports, + mIsTestModeProfile); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnGatewayConnectionConfig.java b/Vcn/framework-b/src/android/net/vcn/VcnGatewayConnectionConfig.java new file mode 100644 index 0000000000..067144e6f4 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnGatewayConnectionConfig.java @@ -0,0 +1,876 @@ +/* + * 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 android.net.ipsec.ike.IkeSessionParams.IKE_OPTION_MOBIKE; +import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API; +import static android.net.vcn.Flags.FLAG_SAFE_MODE_CONFIG; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.ipsec.ike.IkeTunnelConnectionParams; +import android.net.vcn.persistablebundleutils.TunnelConnectionParamsUtils; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; +import android.util.ArraySet; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +/** + * This class represents a configuration for a connection to a Virtual Carrier Network gateway. + * + *

Each VcnGatewayConnectionConfig represents a single logical connection to a carrier gateway, + * and may provide one or more telephony services (as represented by network capabilities). Each + * gateway is expected to provide mobility for a given session as the device roams across {@link + * Network}s. + * + *

A VCN connection based on this configuration will be brought up dynamically based on device + * settings, and filed NetworkRequests. Underlying Networks must provide INTERNET connectivity, and + * must be part of the subscription group under which this configuration is registered (see {@link + * VcnManager#setVcnConfig}). + * + *

As an abstraction of a cellular network, services that can be provided by a VCN network are + * limited to services provided by cellular networks: + * + *

    + *
  • {@link NetworkCapabilities#NET_CAPABILITY_MMS} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_SUPL} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_DUN} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_FOTA} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_IMS} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_CBS} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_IA} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_RCS} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_XCAP} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_EIMS} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_INTERNET} + *
  • {@link NetworkCapabilities#NET_CAPABILITY_MCX} + *
+ */ +public final class VcnGatewayConnectionConfig { + /** + * Minimum NAT timeout not set. + * + *

When the timeout is not set, the device will automatically choose a keepalive interval and + * may reduce the keepalive frequency for power-optimization. + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + // This constant does not represent a minimum value. It indicates the value is not configured. + @SuppressLint("MinMaxConstant") + public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET = -1; + + /** @hide */ + public static final int MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS = 120; + + // TODO: Use MIN_MTU_V6 once it is public, @hide + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int MIN_MTU_V6 = 1280; + + /** + * The set of allowed capabilities for exposed capabilities. + * + * @hide + */ + public 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); + } + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"NET_CAPABILITY_"}, + value = { + NetworkCapabilities.NET_CAPABILITY_MMS, + NetworkCapabilities.NET_CAPABILITY_SUPL, + NetworkCapabilities.NET_CAPABILITY_DUN, + NetworkCapabilities.NET_CAPABILITY_FOTA, + NetworkCapabilities.NET_CAPABILITY_IMS, + NetworkCapabilities.NET_CAPABILITY_CBS, + NetworkCapabilities.NET_CAPABILITY_IA, + NetworkCapabilities.NET_CAPABILITY_RCS, + NetworkCapabilities.NET_CAPABILITY_XCAP, + NetworkCapabilities.NET_CAPABILITY_EIMS, + NetworkCapabilities.NET_CAPABILITY_INTERNET, + NetworkCapabilities.NET_CAPABILITY_MCX, + }) + public @interface VcnSupportedCapability {} + + /** + * Perform mobility update to attempt recovery from suspected data stalls. + * + *

If set, the gateway connection will monitor the data stall detection of the VCN network. + * When there is a suspected data stall, the gateway connection will attempt recovery by + * performing a mobility update on the underlying IKE session. + */ + public static final int VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY = 0; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"VCN_GATEWAY_OPTION_"}, + value = { + VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY, + }) + public @interface VcnGatewayOption {} + + private static final Set ALLOWED_GATEWAY_OPTIONS = new ArraySet<>(); + + static { + ALLOWED_GATEWAY_OPTIONS.add(VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY); + } + + 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#setRetryIntervalsMillis()} + */ + 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) + }; + + /** @hide */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static final List DEFAULT_UNDERLYING_NETWORK_TEMPLATES = + new ArrayList<>(); + + static { + DEFAULT_UNDERLYING_NETWORK_TEMPLATES.add( + new VcnCellUnderlyingNetworkTemplate.Builder() + .setOpportunistic(MATCH_REQUIRED) + .build()); + + DEFAULT_UNDERLYING_NETWORK_TEMPLATES.add( + new VcnWifiUnderlyingNetworkTemplate.Builder() + .build()); + + DEFAULT_UNDERLYING_NETWORK_TEMPLATES.add( + new VcnCellUnderlyingNetworkTemplate.Builder() + .build()); + } + + private static final String GATEWAY_CONNECTION_NAME_KEY = "mGatewayConnectionName"; + @NonNull private final String mGatewayConnectionName; + + private static final String TUNNEL_CONNECTION_PARAMS_KEY = "mTunnelConnectionParams"; + @NonNull private IkeTunnelConnectionParams mTunnelConnectionParams; + + private static final String EXPOSED_CAPABILITIES_KEY = "mExposedCapabilities"; + @NonNull private final SortedSet mExposedCapabilities; + + /** @hide */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static final String UNDERLYING_NETWORK_TEMPLATES_KEY = "mUnderlyingNetworkTemplates"; + + @NonNull private final List mUnderlyingNetworkTemplates; + + 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; + + private static final String MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS_KEY = + "mMinUdpPort4500NatTimeoutSeconds"; + private final int mMinUdpPort4500NatTimeoutSeconds; + + private static final String IS_SAFE_MODE_DISABLED_KEY = "mIsSafeModeDisabled"; + private final boolean mIsSafeModeDisabled; + + private static final String GATEWAY_OPTIONS_KEY = "mGatewayOptions"; + @NonNull private final Set mGatewayOptions; + + /** Builds a VcnGatewayConnectionConfig with the specified parameters. */ + private VcnGatewayConnectionConfig( + @NonNull String gatewayConnectionName, + @NonNull IkeTunnelConnectionParams tunnelConnectionParams, + @NonNull Set exposedCapabilities, + @NonNull List underlyingNetworkTemplates, + @NonNull long[] retryIntervalsMs, + @IntRange(from = MIN_MTU_V6) int maxMtu, + @NonNull int minUdpPort4500NatTimeoutSeconds, + boolean isSafeModeDisabled, + @NonNull Set gatewayOptions) { + mGatewayConnectionName = gatewayConnectionName; + mTunnelConnectionParams = tunnelConnectionParams; + mExposedCapabilities = new TreeSet(exposedCapabilities); + mRetryIntervalsMs = retryIntervalsMs; + mMaxMtu = maxMtu; + mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds; + mGatewayOptions = Collections.unmodifiableSet(new ArraySet(gatewayOptions)); + mIsSafeModeDisabled = isSafeModeDisabled; + + mUnderlyingNetworkTemplates = new ArrayList<>(underlyingNetworkTemplates); + if (mUnderlyingNetworkTemplates.isEmpty()) { + mUnderlyingNetworkTemplates.addAll(DEFAULT_UNDERLYING_NETWORK_TEMPLATES); + } + + validate(); + } + + // Null check MUST be done for all new fields added to VcnGatewayConnectionConfig, to avoid + // crashes when parsing PersistableBundle built on old platforms. + /** @hide */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public VcnGatewayConnectionConfig(@NonNull PersistableBundle in) { + final PersistableBundle tunnelConnectionParamsBundle = + in.getPersistableBundle(TUNNEL_CONNECTION_PARAMS_KEY); + Objects.requireNonNull( + tunnelConnectionParamsBundle, "tunnelConnectionParamsBundle was null"); + + final PersistableBundle exposedCapsBundle = + in.getPersistableBundle(EXPOSED_CAPABILITIES_KEY); + mGatewayConnectionName = in.getString(GATEWAY_CONNECTION_NAME_KEY); + mTunnelConnectionParams = + TunnelConnectionParamsUtils.fromPersistableBundle(tunnelConnectionParamsBundle); + mExposedCapabilities = new TreeSet<>(PersistableBundleUtils.toList( + exposedCapsBundle, PersistableBundleUtils.INTEGER_DESERIALIZER)); + + final PersistableBundle networkTemplatesBundle = + in.getPersistableBundle(UNDERLYING_NETWORK_TEMPLATES_KEY); + + if (networkTemplatesBundle == null) { + // UNDERLYING_NETWORK_TEMPLATES_KEY was added in Android T. Thus + // VcnGatewayConnectionConfig created on old platforms will not have this data and will + // be assigned with the default value + mUnderlyingNetworkTemplates = new ArrayList<>(DEFAULT_UNDERLYING_NETWORK_TEMPLATES); + } else { + mUnderlyingNetworkTemplates = + PersistableBundleUtils.toList( + networkTemplatesBundle, + VcnUnderlyingNetworkTemplate::fromPersistableBundle); + } + + final PersistableBundle gatewayOptionsBundle = in.getPersistableBundle(GATEWAY_OPTIONS_KEY); + + if (gatewayOptionsBundle == null) { + // GATEWAY_OPTIONS_KEY was added in Android U. Thus VcnGatewayConnectionConfig created + // on old platforms will not have this data and will be assigned with the default value + mGatewayOptions = Collections.emptySet(); + } else { + mGatewayOptions = + new ArraySet<>( + PersistableBundleUtils.toList( + gatewayOptionsBundle, + PersistableBundleUtils.INTEGER_DESERIALIZER)); + } + + mRetryIntervalsMs = in.getLongArray(RETRY_INTERVAL_MS_KEY); + mMaxMtu = in.getInt(MAX_MTU_KEY); + mMinUdpPort4500NatTimeoutSeconds = + in.getInt( + MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS_KEY, + MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET); + mIsSafeModeDisabled = in.getBoolean(IS_SAFE_MODE_DISABLED_KEY); + + validate(); + } + + private void validate() { + Objects.requireNonNull(mGatewayConnectionName, "gatewayConnectionName was null"); + Objects.requireNonNull(mTunnelConnectionParams, "tunnel connection parameter was null"); + + Preconditions.checkArgument( + mExposedCapabilities != null && !mExposedCapabilities.isEmpty(), + "exposedCapsBundle was null or empty"); + for (Integer cap : getAllExposedCapabilities()) { + checkValidCapability(cap); + } + + validateNetworkTemplateList(mUnderlyingNetworkTemplates); + Objects.requireNonNull(mRetryIntervalsMs, "retryIntervalsMs was null"); + validateRetryInterval(mRetryIntervalsMs); + + Preconditions.checkArgument( + mMaxMtu >= MIN_MTU_V6, "maxMtu must be at least IPv6 min MTU (1280)"); + + Preconditions.checkArgument( + mMinUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET + || mMinUdpPort4500NatTimeoutSeconds + >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, + "minUdpPort4500NatTimeoutSeconds must be at least 120s"); + + for (int option : mGatewayOptions) { + validateGatewayOption(option); + } + } + + 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); + } + } + + private static void validateNetworkTemplateList( + List networkPriorityRules) { + Objects.requireNonNull(networkPriorityRules, "networkPriorityRules is null"); + + Set existingRules = new ArraySet<>(); + for (VcnUnderlyingNetworkTemplate rule : networkPriorityRules) { + Objects.requireNonNull(rule, "Found null value VcnUnderlyingNetworkTemplate"); + if (!existingRules.add(rule)) { + throw new IllegalArgumentException("Found duplicate VcnUnderlyingNetworkTemplate"); + } + } + } + + private static void validateGatewayOption(int option) { + if (!ALLOWED_GATEWAY_OPTIONS.contains(option)) { + throw new IllegalArgumentException("Invalid vcn gateway option: " + option); + } + } + + /** + * Returns the configured Gateway Connection name. + * + *

This name is used by the configuring apps to distinguish between + * VcnGatewayConnectionConfigs configured on a single {@link VcnConfig}. This will be used as + * the identifier in VcnStatusCallback invocations. + * + * @see VcnManager.VcnStatusCallback#onGatewayConnectionError + */ + @NonNull + public String getGatewayConnectionName() { + return mGatewayConnectionName; + } + + /** + * Returns tunnel connection parameters. + * + * @hide + */ + @NonNull + public IkeTunnelConnectionParams getTunnelConnectionParams() { + return mTunnelConnectionParams; + } + + /** + * Returns all exposed capabilities. + * + *

The returned integer-value capabilities will not contain duplicates, and will be sorted in + * ascending numerical order. + * + * @see Builder#addExposedCapability(int) + * @see Builder#removeExposedCapability(int) + */ + @NonNull + public int[] getExposedCapabilities() { + // Sorted set guarantees ordering + final int[] caps = new int[mExposedCapabilities.size()]; + + int i = 0; + for (int c : mExposedCapabilities) { + caps[i++] = c; + } + + return caps; + } + + /** + * Returns all exposed capabilities. + * + *

Left to prevent the need to make major changes while changes are actively in flight. + * + * @deprecated use getExposedCapabilities() instead + * @hide + */ + @Deprecated + @NonNull + public Set getAllExposedCapabilities() { + return Collections.unmodifiableSet(mExposedCapabilities); + } + + /** + * Retrieve the VcnUnderlyingNetworkTemplate list, or a default list if it is not configured. + * + * @see Builder#setVcnUnderlyingNetworkPriorities(List) + */ + @NonNull + public List getVcnUnderlyingNetworkPriorities() { + return new ArrayList<>(mUnderlyingNetworkTemplates); + } + + /** + * Retrieves the configured retry intervals. + * + * @see Builder#setRetryIntervalsMillis(long[]) + */ + @NonNull + public long[] getRetryIntervalsMillis() { + return Arrays.copyOf(mRetryIntervalsMs, mRetryIntervalsMs.length); + } + + /** + * Retrieves the maximum MTU allowed for this Gateway Connection. + * + * @see Builder#setMaxMtu(int) + */ + @IntRange(from = MIN_MTU_V6) + public int getMaxMtu() { + return mMaxMtu; + } + + /** + * Retrieves the maximum supported IKEv2/IPsec NATT keepalive timeout. + * + * @see Builder#setMinUdpPort4500NatTimeoutSeconds(int) + */ + public int getMinUdpPort4500NatTimeoutSeconds() { + return mMinUdpPort4500NatTimeoutSeconds; + } + + /** + * Check whether safe mode is enabled + * + * @see Builder#setSafeModeEnabled(boolean) + */ + @FlaggedApi(FLAG_SAFE_MODE_CONFIG) + public boolean isSafeModeEnabled() { + return !mIsSafeModeDisabled; + } + + /** + * Checks if the given VCN gateway option is enabled. + * + * @param option the option to check. + * @throws IllegalArgumentException if the provided option is invalid. + * @see Builder#addGatewayOption(int) + * @see Builder#removeGatewayOption(int) + */ + public boolean hasGatewayOption(@VcnGatewayOption int option) { + validateGatewayOption(option); + return mGatewayOptions.contains(option); + } + + /** + * Converts this config to a PersistableBundle. + * + * @hide + */ + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + final PersistableBundle tunnelConnectionParamsBundle = + TunnelConnectionParamsUtils.toPersistableBundle(mTunnelConnectionParams); + final PersistableBundle exposedCapsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mExposedCapabilities), + PersistableBundleUtils.INTEGER_SERIALIZER); + final PersistableBundle networkTemplatesBundle = + PersistableBundleUtils.fromList( + mUnderlyingNetworkTemplates, + VcnUnderlyingNetworkTemplate::toPersistableBundle); + final PersistableBundle gatewayOptionsBundle = + PersistableBundleUtils.fromList( + new ArrayList<>(mGatewayOptions), + PersistableBundleUtils.INTEGER_SERIALIZER); + + result.putString(GATEWAY_CONNECTION_NAME_KEY, mGatewayConnectionName); + result.putPersistableBundle(TUNNEL_CONNECTION_PARAMS_KEY, tunnelConnectionParamsBundle); + result.putPersistableBundle(EXPOSED_CAPABILITIES_KEY, exposedCapsBundle); + result.putPersistableBundle(UNDERLYING_NETWORK_TEMPLATES_KEY, networkTemplatesBundle); + result.putPersistableBundle(GATEWAY_OPTIONS_KEY, gatewayOptionsBundle); + result.putLongArray(RETRY_INTERVAL_MS_KEY, mRetryIntervalsMs); + result.putInt(MAX_MTU_KEY, mMaxMtu); + result.putInt(MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS_KEY, mMinUdpPort4500NatTimeoutSeconds); + result.putBoolean(IS_SAFE_MODE_DISABLED_KEY, mIsSafeModeDisabled); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash( + mGatewayConnectionName, + mTunnelConnectionParams, + mExposedCapabilities, + mUnderlyingNetworkTemplates, + Arrays.hashCode(mRetryIntervalsMs), + mMaxMtu, + mMinUdpPort4500NatTimeoutSeconds, + mIsSafeModeDisabled, + mGatewayOptions); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof VcnGatewayConnectionConfig)) { + return false; + } + + final VcnGatewayConnectionConfig rhs = (VcnGatewayConnectionConfig) other; + return mGatewayConnectionName.equals(rhs.mGatewayConnectionName) + && mTunnelConnectionParams.equals(rhs.mTunnelConnectionParams) + && mExposedCapabilities.equals(rhs.mExposedCapabilities) + && mUnderlyingNetworkTemplates.equals(rhs.mUnderlyingNetworkTemplates) + && Arrays.equals(mRetryIntervalsMs, rhs.mRetryIntervalsMs) + && mMaxMtu == rhs.mMaxMtu + && mMinUdpPort4500NatTimeoutSeconds == rhs.mMinUdpPort4500NatTimeoutSeconds + && mIsSafeModeDisabled == rhs.mIsSafeModeDisabled + && mGatewayOptions.equals(rhs.mGatewayOptions); + } + + /** + * This class is used to incrementally build {@link VcnGatewayConnectionConfig} objects. + */ + public static final class Builder { + @NonNull private final String mGatewayConnectionName; + @NonNull private final IkeTunnelConnectionParams mTunnelConnectionParams; + @NonNull private final Set mExposedCapabilities = new ArraySet(); + + @NonNull + private final List mUnderlyingNetworkTemplates = + new ArrayList<>(DEFAULT_UNDERLYING_NETWORK_TEMPLATES); + + @NonNull private long[] mRetryIntervalsMs = DEFAULT_RETRY_INTERVALS_MS; + private int mMaxMtu = DEFAULT_MAX_MTU; + private int mMinUdpPort4500NatTimeoutSeconds = MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET; + private boolean mIsSafeModeDisabled = false; + + @NonNull private final Set mGatewayOptions = new ArraySet<>(); + + // 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. + + /** + * Construct a Builder object. + * + * @param gatewayConnectionName the String GatewayConnection name for this + * VcnGatewayConnectionConfig. Each VcnGatewayConnectionConfig within a {@link + * VcnConfig} must be given a unique name. This name is used by the caller to + * distinguish between VcnGatewayConnectionConfigs configured on a single {@link + * VcnConfig}. This will be used as the identifier in VcnStatusCallback invocations. + * @param tunnelConnectionParams the IKE tunnel connection configuration + * @throws IllegalArgumentException if the provided IkeTunnelConnectionParams is not + * configured to support MOBIKE + * @see IkeTunnelConnectionParams + * @see VcnManager.VcnStatusCallback#onGatewayConnectionError + */ + public Builder( + @NonNull String gatewayConnectionName, + @NonNull IkeTunnelConnectionParams tunnelConnectionParams) { + Objects.requireNonNull(gatewayConnectionName, "gatewayConnectionName was null"); + Objects.requireNonNull(tunnelConnectionParams, "tunnelConnectionParams was null"); + if (!tunnelConnectionParams.getIkeSessionParams().hasIkeOption(IKE_OPTION_MOBIKE)) { + throw new IllegalArgumentException( + "MOBIKE must be configured for the provided IkeSessionParams"); + } + + mGatewayConnectionName = gatewayConnectionName; + mTunnelConnectionParams = tunnelConnectionParams; + } + + /** + * 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 + */ + @NonNull + public Builder addExposedCapability(@VcnSupportedCapability 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 + */ + @NonNull + @SuppressLint("BuilderSetStyle") // For consistency with NetCaps.Builder add/removeCap + public Builder removeExposedCapability(@VcnSupportedCapability int exposedCapability) { + checkValidCapability(exposedCapability); + + mExposedCapabilities.remove(exposedCapability); + return this; + } + + /** + * Set the list of templates to match underlying networks against, in high-to-low priority + * order. + * + *

To select the VCN underlying network, the VCN connection will go through all the + * network candidates and return a network matching the highest priority rule. + * + *

If multiple networks match the same rule, the VCN will prefer an already-selected + * network as opposed to a new/unselected network. However, if both are new/unselected + * networks, a network will be chosen arbitrarily amongst the networks matching the highest + * priority rule. + * + *

If all networks fail to match the rules provided, a carrier-owned underlying network + * will still be selected (if available, at random if necessary). + * + * @param underlyingNetworkTemplates a list of unique VcnUnderlyingNetworkTemplates that are + * ordered from most to least preferred, or an empty list to use the default + * prioritization. The default network prioritization order is Opportunistic cellular, + * Carrier WiFi and then Macro cellular. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + public Builder setVcnUnderlyingNetworkPriorities( + @NonNull List underlyingNetworkTemplates) { + validateNetworkTemplateList(underlyingNetworkTemplates); + + mUnderlyingNetworkTemplates.clear(); + + if (underlyingNetworkTemplates.isEmpty()) { + mUnderlyingNetworkTemplates.addAll(DEFAULT_UNDERLYING_NETWORK_TEMPLATES); + } else { + mUnderlyingNetworkTemplates.addAll(underlyingNetworkTemplates); + } + + 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 setRetryIntervalsMillis(@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; + } + + /** + * Sets the maximum supported IKEv2/IPsec NATT keepalive timeout. + * + *

This is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs, + * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data. + * + * @param minUdpPort4500NatTimeoutSeconds the maximum keepalive timeout supported by the VCN + * Gateway Connection, generally the minimum duration a NAT mapping is cached on the VCN + * Gateway; or {@link MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET} to clear this value. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + public Builder setMinUdpPort4500NatTimeoutSeconds( + @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS) + int minUdpPort4500NatTimeoutSeconds) { + if (Flags.mainlineVcnModuleApi()) { + Preconditions.checkArgument( + minUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET + || minUdpPort4500NatTimeoutSeconds + >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, + "Timeout must be at least 120s or MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET"); + } else { + Preconditions.checkArgument( + minUdpPort4500NatTimeoutSeconds >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, + "Timeout must be at least 120s"); + } + + mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds; + return this; + } + + /** + * Enables the specified VCN gateway option. + * + * @param option the option to be enabled + * @return this {@link Builder} instance, for chaining + * @throws IllegalArgumentException if the provided option is invalid + */ + @NonNull + public Builder addGatewayOption(@VcnGatewayOption int option) { + validateGatewayOption(option); + mGatewayOptions.add(option); + return this; + } + + /** + * Resets (disables) the specified VCN gateway option. + * + * @param option the option to be disabled + * @return this {@link Builder} instance, for chaining + * @throws IllegalArgumentException if the provided option is invalid + */ + @NonNull + public Builder removeGatewayOption(@VcnGatewayOption int option) { + validateGatewayOption(option); + mGatewayOptions.remove(option); + return this; + } + + /** + * Enable/disable safe mode + * + *

If a VCN fails to provide connectivity within a system-provided timeout, it will enter + * safe mode. In safe mode, the VCN Network will be torn down and the system will restore + * connectivity by allowing underlying cellular or WiFi networks to be used as default. At + * the same time, VCN will continue to retry until it succeeds. + * + *

When safe mode is disabled and VCN connection fails to provide connectivity, end users + * might not have connectivity, and may not have access to carrier-owned underlying + * networks. + * + * @param enabled whether safe mode should be enabled. Defaults to {@code true} + */ + @FlaggedApi(FLAG_SAFE_MODE_CONFIG) + @NonNull + public Builder setSafeModeEnabled(boolean enabled) { + mIsSafeModeDisabled = !enabled; + return this; + } + + /** + * Builds and validates the VcnGatewayConnectionConfig. + * + * @return an immutable VcnGatewayConnectionConfig instance + */ + @NonNull + public VcnGatewayConnectionConfig build() { + return new VcnGatewayConnectionConfig( + mGatewayConnectionName, + mTunnelConnectionParams, + mExposedCapabilities, + mUnderlyingNetworkTemplates, + mRetryIntervalsMs, + mMaxMtu, + mMinUdpPort4500NatTimeoutSeconds, + mIsSafeModeDisabled, + mGatewayOptions); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnManager.java b/Vcn/framework-b/src/android/net/vcn/VcnManager.java new file mode 100644 index 0000000000..33da18697c --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnManager.java @@ -0,0 +1,775 @@ +/* + * 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 java.util.Objects.requireNonNull; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresFeature; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.content.pm.PackageManager; +import android.net.LinkProperties; +import android.net.NetworkCapabilities; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.os.ServiceSpecificException; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.net.module.util.BinderUtils; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; + +/** + * VcnManager publishes APIs for applications to configure and manage Virtual Carrier Networks. + * + *

A VCN creates a virtualization layer to allow carriers to aggregate heterogeneous physical + * networks, unifying them as a single carrier network. This enables infrastructure flexibility on + * the part of carriers without impacting user connectivity, abstracting the physical network + * technologies as an implementation detail of their public network. + * + *

Each VCN virtualizes a carrier's network by building tunnels to a carrier's core network over + * carrier-managed physical links and supports a IP mobility layer to ensure seamless transitions + * between the underlying networks. Each VCN is configured based on a Subscription Group (see {@link + * android.telephony.SubscriptionManager}) and aggregates all networks that are brought up based on + * a profile or suggestion in the specified Subscription Group. + * + *

The VCN can be configured to expose one or more {@link android.net.Network}(s), each with + * different capabilities, allowing for APN virtualization. + * + *

If a tunnel fails to connect, or otherwise encounters a fatal error, the VCN will attempt to + * reestablish the connection. If the tunnel still has not reconnected after a system-determined + * timeout, the VCN Safe Mode (see below) will be entered. + * + *

The VCN Safe Mode ensures that users (and carriers) have a fallback to restore system + * connectivity to update profiles, diagnose issues, contact support, or perform other remediation + * tasks. In Safe Mode, the system will allow underlying cellular networks to be used as default. + * Additionally, during Safe Mode, the VCN will continue to retry the connections, and will + * automatically exit Safe Mode if all active tunnels connect successfully. + * + *

Apps targeting Android 15 or newer should check the existence of {@link + * PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION} before querying the service. If the feature is + * absent, {@link Context#getSystemService} may return null. + */ +@SystemService(VcnManager.VCN_MANAGEMENT_SERVICE_STRING) +@RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) +public class VcnManager { + @NonNull private static final String TAG = VcnManager.class.getSimpleName(); + + // TODO: b/366598445: Expose and use Context.VCN_MANAGEMENT_SERVICE + /** @hide */ + public static final String VCN_MANAGEMENT_SERVICE_STRING = "vcn_management"; + + /** + * Key for WiFi entry RSSI thresholds + * + *

The VCN will only migrate to a Carrier WiFi network that has a signal strength greater + * than, or equal to this threshold. + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY = + "vcn_network_selection_wifi_entry_rssi_threshold"; + + /** + * Key for WiFi entry RSSI thresholds + * + *

If the VCN's selected Carrier WiFi network has a signal strength less than this threshold, + * the VCN will attempt to migrate away from the Carrier WiFi network. + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY = + "vcn_network_selection_wifi_exit_rssi_threshold"; + + /** + * Key for the interval to poll IpSecTransformState for packet loss monitoring + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY = + "vcn_network_selection_poll_ipsec_state_interval_seconds"; + + /** + * Key for the threshold of IPSec packet loss rate + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY = + "vcn_network_selection_ipsec_packet_loss_percent_threshold"; + + /** + * Key for detecting unusually large increases in IPsec packet sequence numbers. + * + *

If the sequence number increases by more than this value within a second, it may indicate + * an intentional leap on the server's downlink. To avoid false positives, the packet loss + * detector will suppress loss reporting. + * + *

By default, there's no maximum limit enforced, prioritizing detection of lossy networks. + * To reduce false positives, consider setting an appropriate maximum threshold. + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY = + "vcn_network_selection_max_seq_num_increase_per_second"; + + /** + * Key for the list of timeouts in minute to stop penalizing an underlying network candidate + * + * @hide + */ + @NonNull + public static final String VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY = + "vcn_network_selection_penalty_timeout_minutes_list"; + + // TODO: Add separate signal strength thresholds for 2.4 GHz and 5GHz + + /** + * Key for transports that need to be marked as restricted by the VCN + * + *

Defaults to TRANSPORT_WIFI if the config does not exist + * + * @hide + */ + public static final String VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY = + "vcn_restricted_transports"; + + /** + * Key for number of seconds to wait before entering safe mode + * + *

A VcnGatewayConnection will enter safe mode when it takes over the configured timeout to + * enter {@link ConnectedState}. + * + *

Defaults to 30, unless overridden by carrier config + * + * @hide + */ + @NonNull + public static final String VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY = + "vcn_safe_mode_timeout_seconds_key"; + + /** + * Key for maximum number of parallel SAs for tunnel aggregation + * + *

If set to a value > 1, multiple tunnels will be set up, and inbound traffic will be + * aggregated over the various tunnels. + * + *

Defaults to 1, unless overridden by carrier config + * + * @hide + */ + @NonNull + public static final String VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY = + "vcn_tunnel_aggregation_sa_count_max"; + + /** + * List of Carrier Config options to extract from Carrier Config bundles. + * @hide + */ + @NonNull + public static final String[] VCN_RELATED_CARRIER_CONFIG_KEYS = + new String[] { + VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, + VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY, + VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY, + VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY, + VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY, + VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY, + VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY, + VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY, + VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY, + }; + + private static final Map< + VcnNetworkPolicyChangeListener, VcnUnderlyingNetworkPolicyListenerBinder> + REGISTERED_POLICY_LISTENERS = new ConcurrentHashMap<>(); + + @NonNull private final Context mContext; + @NonNull private final IVcnManagementService mService; + + /** + * Construct an instance of VcnManager within an application context. + * + * @param ctx the application context for this manager + * @param service the VcnManagementService binder backing this manager + * + * @hide + */ + public VcnManager(@NonNull Context ctx, @NonNull IVcnManagementService service) { + mContext = requireNonNull(ctx, "missing context"); + mService = requireNonNull(service, "missing service"); + } + + /** + * Get all currently registered VcnNetworkPolicyChangeListeners for testing purposes. + * + * @hide + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + @NonNull + public static Map + getAllPolicyListeners() { + return Collections.unmodifiableMap(REGISTERED_POLICY_LISTENERS); + } + + /** + * Sets the VCN configuration for a given subscription group. + * + *

An app that has carrier privileges for any of the subscriptions in the given group may set + * a VCN configuration. If a configuration already exists for the given subscription group, it + * will be overridden. Any active VCN(s) may be forced to restart to use the new configuration. + * + *

This API is ONLY permitted for callers running as the primary user. + * + * @param subscriptionGroup the subscription group that the configuration should be applied to + * @param config the configuration parameters for the VCN + * @throws SecurityException if the caller does not have carrier privileges for the provided + * subscriptionGroup, or is not running as the primary user + * @throws IOException if the configuration failed to be saved and persisted to disk. This may + * occur due to temporary disk errors, or more permanent conditions such as a full disk. + */ + @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant + 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, mContext.getOpPackageName()); + } catch (ServiceSpecificException e) { + throw new IOException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Clears the VCN configuration for a given subscription group. + * + *

An app that has carrier privileges for any of the subscriptions in the given group may + * clear a VCN configuration. This API is ONLY permitted for callers running as the primary + * user. Any active VCN associated with this configuration will be torn down. + * + * @param subscriptionGroup the subscription group that the configuration should be applied to + * @throws SecurityException if the caller does not have carrier privileges, is not the owner of + * the associated configuration, or is not running as the primary user + * @throws IOException if the configuration failed to be cleared from disk. This may occur due + * to temporary disk errors, or more permanent conditions such as a full disk. + */ + @RequiresPermission("carrier privileges") // TODO (b/72967236): Define a system-wide constant + public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup) throws IOException { + requireNonNull(subscriptionGroup, "subscriptionGroup was null"); + + try { + mService.clearVcnConfig(subscriptionGroup, mContext.getOpPackageName()); + } catch (ServiceSpecificException e) { + throw new IOException(e); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Retrieves the list of Subscription Groups for which a VCN Configuration has been set. + * + *

The returned list will include only subscription groups for which an associated {@link + * VcnConfig} exists, and the app is either: + * + *

    + *
  • Carrier privileged for that subscription group, or + *
  • Is the provisioning package of the config + *
+ * + * @throws SecurityException if the caller is not running as the primary user + */ + @NonNull + public List getConfiguredSubscriptionGroups() { + try { + return mService.getConfiguredSubscriptionGroups(mContext.getOpPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + // TODO(b/180537630): remove all VcnUnderlyingNetworkPolicyListener refs once Telephony is using + // the new VcnNetworkPolicyChangeListener API + /** + * VcnUnderlyingNetworkPolicyListener is the interface through which internal system components + * can register to receive updates for VCN-underlying Network policies from the System Server. + * + * @hide + */ + public interface VcnUnderlyingNetworkPolicyListener extends VcnNetworkPolicyChangeListener {} + + /** + * Add a listener for VCN-underlying network policy updates. + * + * @param executor the Executor that will be used for invoking all calls to the specified + * Listener + * @param listener the VcnUnderlyingNetworkPolicyListener to be added + * @throws SecurityException if the caller does not have the required permission + * @throws IllegalStateException if the specified VcnUnderlyingNetworkPolicyListener is already + * registered + * @hide + */ + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public void addVcnUnderlyingNetworkPolicyListener( + @NonNull Executor executor, @NonNull VcnUnderlyingNetworkPolicyListener listener) { + addVcnNetworkPolicyChangeListener(executor, listener); + } + + /** + * Remove the specified VcnUnderlyingNetworkPolicyListener from VcnManager. + * + *

If the specified listener is not currently registered, this is a no-op. + * + * @param listener the VcnUnderlyingNetworkPolicyListener that will be removed + * @hide + */ + public void removeVcnUnderlyingNetworkPolicyListener( + @NonNull VcnUnderlyingNetworkPolicyListener listener) { + removeVcnNetworkPolicyChangeListener(listener); + } + + /** + * Queries the underlying network policy for a network with the given parameters. + * + *

Prior to a new NetworkAgent being registered, or upon notification that Carrier VCN policy + * may have changed via {@link VcnUnderlyingNetworkPolicyListener#onPolicyChanged()}, a Network + * Provider MUST poll for the updated Network policy based on that Network's capabilities and + * properties. + * + * @param networkCapabilities the NetworkCapabilities to be used in determining the Network + * policy for this Network. + * @param linkProperties the LinkProperties to be used in determining the Network policy for + * this Network. + * @throws SecurityException if the caller does not have permission NETWORK_FACTORY + * @return the VcnUnderlyingNetworkPolicy to be used for this Network. + * @hide + */ + @NonNull + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public VcnUnderlyingNetworkPolicy getUnderlyingNetworkPolicy( + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties) { + requireNonNull(networkCapabilities, "networkCapabilities must not be null"); + requireNonNull(linkProperties, "linkProperties must not be null"); + + try { + return mService.getUnderlyingNetworkPolicy(networkCapabilities, linkProperties); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * VcnNetworkPolicyChangeListener is the interface through which internal system components + * (e.g. Network Factories) can register to receive updates for VCN-underlying Network policies + * from the System Server. + * + *

Any Network Factory that brings up Networks capable of being VCN-underlying Networks + * should register a VcnNetworkPolicyChangeListener. VcnManager will then use this listener to + * notify the registrant when VCN Network policies change. Upon receiving this signal, the + * listener must check {@link VcnManager} for the current Network policy result for each of its + * Networks via {@link #applyVcnNetworkPolicy(NetworkCapabilities, LinkProperties)}. + * + * @hide + */ + @SystemApi + public interface VcnNetworkPolicyChangeListener { + /** + * Notifies the implementation that the VCN's underlying Network policy has changed. + * + *

After receiving this callback, implementations should get the current {@link + * VcnNetworkPolicyResult} via {@link #applyVcnNetworkPolicy(NetworkCapabilities, + * LinkProperties)}. + */ + void onPolicyChanged(); + } + + /** + * Add a listener for VCN-underlying Network policy updates. + * + *

A {@link VcnNetworkPolicyChangeListener} is eligible to begin receiving callbacks once it + * is registered. No callbacks are guaranteed upon registration. + * + * @param executor the Executor that will be used for invoking all calls to the specified + * Listener + * @param listener the VcnNetworkPolicyChangeListener to be added + * @throws SecurityException if the caller does not have the required permission + * @throws IllegalStateException if the specified VcnNetworkPolicyChangeListener is already + * registered + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public void addVcnNetworkPolicyChangeListener( + @NonNull Executor executor, @NonNull VcnNetworkPolicyChangeListener listener) { + requireNonNull(executor, "executor must not be null"); + requireNonNull(listener, "listener must not be null"); + + VcnUnderlyingNetworkPolicyListenerBinder binder = + new VcnUnderlyingNetworkPolicyListenerBinder(executor, listener); + if (REGISTERED_POLICY_LISTENERS.putIfAbsent(listener, binder) != null) { + throw new IllegalStateException("listener is already registered with VcnManager"); + } + + try { + mService.addVcnUnderlyingNetworkPolicyListener(binder); + } catch (RemoteException e) { + REGISTERED_POLICY_LISTENERS.remove(listener); + throw e.rethrowFromSystemServer(); + } + } + + /** + * Remove the specified VcnNetworkPolicyChangeListener from VcnManager. + * + *

If the specified listener is not currently registered, this is a no-op. + * + * @param listener the VcnNetworkPolicyChangeListener that will be removed + * @throws SecurityException if the caller does not have the required permission + * @hide + */ + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public void removeVcnNetworkPolicyChangeListener( + @NonNull VcnNetworkPolicyChangeListener listener) { + requireNonNull(listener, "listener must not be null"); + + VcnUnderlyingNetworkPolicyListenerBinder binder = + REGISTERED_POLICY_LISTENERS.remove(listener); + if (binder == null) { + return; + } + + try { + mService.removeVcnUnderlyingNetworkPolicyListener(binder); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Applies the network policy for a {@link android.net.Network} with the given parameters. + * + *

Prior to a new NetworkAgent being registered, or upon notification that Carrier VCN policy + * may have changed via {@link VcnNetworkPolicyChangeListener#onPolicyChanged()}, a Network + * Provider MUST poll for the updated Network policy based on that Network's capabilities and + * properties. + * + * @param networkCapabilities the NetworkCapabilities to be used in determining the Network + * policy result for this Network. + * @param linkProperties the LinkProperties to be used in determining the Network policy result + * for this Network. + * @throws SecurityException if the caller does not have the required permission + * @return the {@link VcnNetworkPolicyResult} to be used for this Network. + * @hide + */ + @NonNull + @SystemApi + @RequiresPermission(android.Manifest.permission.NETWORK_FACTORY) + public VcnNetworkPolicyResult applyVcnNetworkPolicy( + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties) { + requireNonNull(networkCapabilities, "networkCapabilities must not be null"); + requireNonNull(linkProperties, "linkProperties must not be null"); + + final VcnUnderlyingNetworkPolicy policy = + getUnderlyingNetworkPolicy(networkCapabilities, linkProperties); + return new VcnNetworkPolicyResult( + policy.isTeardownRequested(), policy.getMergedNetworkCapabilities()); + } + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + VCN_STATUS_CODE_NOT_CONFIGURED, + VCN_STATUS_CODE_INACTIVE, + VCN_STATUS_CODE_ACTIVE, + VCN_STATUS_CODE_SAFE_MODE + }) + public @interface VcnStatusCode {} + + /** + * Value indicating that the VCN for the subscription group is not configured, or that the + * callback is not privileged for the subscription group. + */ + public static final int VCN_STATUS_CODE_NOT_CONFIGURED = 0; + + /** + * Value indicating that the VCN for the subscription group is inactive. + * + *

A VCN is inactive if a {@link VcnConfig} is present for the subscription group, but the + * provisioning package is not privileged. + */ + public static final int VCN_STATUS_CODE_INACTIVE = 1; + + /** + * Value indicating that the VCN for the subscription group is active. + * + *

A VCN is active if a {@link VcnConfig} is present for the subscription, the provisioning + * package is privileged, and the VCN is not in Safe Mode. In other words, a VCN is considered + * active while it is connecting, fully connected, and disconnecting. + */ + public static final int VCN_STATUS_CODE_ACTIVE = 2; + + /** + * Value indicating that the VCN for the subscription group is in Safe Mode. + * + *

A VCN will be put into Safe Mode if any of the gateway connections were unable to + * establish a connection within a system-determined timeout (while underlying networks were + * available). + */ + public static final int VCN_STATUS_CODE_SAFE_MODE = 3; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + VCN_ERROR_CODE_INTERNAL_ERROR, + VCN_ERROR_CODE_CONFIG_ERROR, + VCN_ERROR_CODE_NETWORK_ERROR + }) + public @interface VcnErrorCode {} + + /** + * Value indicating that an internal failure occurred in this Gateway Connection. + */ + public static final int VCN_ERROR_CODE_INTERNAL_ERROR = 0; + + /** + * Value indicating that an error with this Gateway Connection's configuration occurred. + * + *

For example, this error code will be returned after authentication failures. + */ + public static final int VCN_ERROR_CODE_CONFIG_ERROR = 1; + + /** + * Value indicating that a Network error occurred with this Gateway Connection. + * + *

For example, this error code will be returned if an underlying {@link android.net.Network} + * for this Gateway Connection is lost, or if an error occurs while resolving the connection + * endpoint address. + */ + public static final int VCN_ERROR_CODE_NETWORK_ERROR = 2; + + /** + * VcnStatusCallback is the interface for Carrier apps to receive updates for their VCNs. + * + *

VcnStatusCallbacks may be registered before {@link VcnConfig}s are provided for a + * subscription group. + */ + public abstract static class VcnStatusCallback { + private VcnStatusCallbackBinder mCbBinder; + + /** + * Invoked when status of the VCN for this callback's subscription group changes. + * + * @param statusCode the code for the status change encountered by this {@link + * VcnStatusCallback}'s subscription group. This value will be one of VCN_STATUS_CODE_*. + */ + public abstract void onStatusChanged(@VcnStatusCode int statusCode); + + /** + * Invoked when a VCN Gateway Connection corresponding to this callback's subscription group + * encounters an error. + * + * @param gatewayConnectionName the String GatewayConnection name for the GatewayConnection + * encountering an error. This will match the name for exactly one {@link + * VcnGatewayConnectionConfig} for the {@link VcnConfig} configured for this callback's + * subscription group + * @param errorCode the code to indicate the error that occurred. This value will be one of + * VCN_ERROR_CODE_*. + * @param detail Throwable to provide additional information about the error, or {@code + * null} if none + */ + public abstract void onGatewayConnectionError( + @NonNull String gatewayConnectionName, + @VcnErrorCode int errorCode, + @Nullable Throwable detail); + } + + /** + * Registers the given callback to receive status updates for the specified subscription. + * + *

Callbacks can be registered for a subscription before {@link VcnConfig}s are set for it. + * + *

A {@link VcnStatusCallback} may only be registered for one subscription at a time. {@link + * VcnStatusCallback}s may be reused once unregistered. + * + *

A {@link VcnStatusCallback} will only be invoked if the registering package has carrier + * privileges for the specified subscription at the time of invocation. + * + *

A {@link VcnStatusCallback} is eligible to begin receiving callbacks once it is registered + * and there is a VCN active for its specified subscription group (this may happen after the + * callback is registered). + * + *

{@link VcnStatusCallback#onStatusChanged(int)} will be invoked on registration with the + * current status for the specified subscription group's VCN. If the registrant is not + * privileged for this subscription group, {@link #VCN_STATUS_CODE_NOT_CONFIGURED} will be + * returned. + * + * @param subscriptionGroup The subscription group to match for callbacks + * @param executor The {@link Executor} to be used for invoking callbacks + * @param callback The VcnStatusCallback to be registered + * @throws IllegalStateException if callback is currently registered with VcnManager + */ + public void registerVcnStatusCallback( + @NonNull ParcelUuid subscriptionGroup, + @NonNull Executor executor, + @NonNull VcnStatusCallback callback) { + requireNonNull(subscriptionGroup, "subscriptionGroup must not be null"); + requireNonNull(executor, "executor must not be null"); + requireNonNull(callback, "callback must not be null"); + + synchronized (callback) { + if (callback.mCbBinder != null) { + throw new IllegalStateException("callback is already registered with VcnManager"); + } + callback.mCbBinder = new VcnStatusCallbackBinder(executor, callback); + + try { + mService.registerVcnStatusCallback( + subscriptionGroup, callback.mCbBinder, mContext.getOpPackageName()); + } catch (RemoteException e) { + callback.mCbBinder = null; + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Unregisters the given callback. + * + *

Once unregistered, the callback will stop receiving status updates for the subscription it + * was registered with. + * + * @param callback The callback to be unregistered + */ + public void unregisterVcnStatusCallback(@NonNull VcnStatusCallback callback) { + requireNonNull(callback, "callback must not be null"); + + synchronized (callback) { + if (callback.mCbBinder == null) { + // no Binder attached to this callback, so it's not currently registered + return; + } + + try { + mService.unregisterVcnStatusCallback(callback.mCbBinder); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + callback.mCbBinder = null; + } + } + } + + /** + * Binder wrapper for added VcnNetworkPolicyChangeListeners to receive signals from System + * Server. + * + * @hide + */ + private static class VcnUnderlyingNetworkPolicyListenerBinder + extends IVcnUnderlyingNetworkPolicyListener.Stub { + @NonNull private final Executor mExecutor; + @NonNull private final VcnNetworkPolicyChangeListener mListener; + + private VcnUnderlyingNetworkPolicyListenerBinder( + Executor executor, VcnNetworkPolicyChangeListener listener) { + mExecutor = executor; + mListener = listener; + } + + @Override + public void onPolicyChanged() { + BinderUtils.withCleanCallingIdentity( + () -> mExecutor.execute(() -> mListener.onPolicyChanged())); + } + } + + /** + * Binder wrapper for VcnStatusCallbacks to receive signals from VcnManagementService. + * + * @hide + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class VcnStatusCallbackBinder extends IVcnStatusCallback.Stub { + @NonNull private final Executor mExecutor; + @NonNull private final VcnStatusCallback mCallback; + + public VcnStatusCallbackBinder( + @NonNull Executor executor, @NonNull VcnStatusCallback callback) { + mExecutor = executor; + mCallback = callback; + } + + @Override + public void onVcnStatusChanged(@VcnStatusCode int statusCode) { + BinderUtils.withCleanCallingIdentity( + () -> mExecutor.execute(() -> mCallback.onStatusChanged(statusCode))); + } + + // TODO(b/180521637): use ServiceSpecificException for safer Exception 'parceling' + @Override + public void onGatewayConnectionError( + @NonNull String gatewayConnectionName, + @VcnErrorCode int errorCode, + @Nullable String exceptionClass, + @Nullable String exceptionMessage) { + final Throwable cause = createThrowableByClassName(exceptionClass, exceptionMessage); + + BinderUtils.withCleanCallingIdentity( + () -> + mExecutor.execute( + () -> + mCallback.onGatewayConnectionError( + gatewayConnectionName, errorCode, cause))); + } + + private static Throwable createThrowableByClassName( + @Nullable String className, @Nullable String message) { + if (className == null) { + return null; + } + + try { + Class c = Class.forName(className); + return (Throwable) c.getConstructor(String.class).newInstance(message); + } catch (ReflectiveOperationException | ClassCastException e) { + return new RuntimeException(className + ": " + message); + } + } + } +} diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java b/Vcn/framework-b/src/android/net/vcn/VcnNetworkPolicyResult.aidl similarity index 56% rename from Tethering/src/com/android/networkstack/tethering/TetherDevValue.java rename to Vcn/framework-b/src/android/net/vcn/VcnNetworkPolicyResult.aidl index b6e0c737d3..3f13abe869 100644 --- a/Tethering/src/com/android/networkstack/tethering/TetherDevValue.java +++ b/Vcn/framework-b/src/android/net/vcn/VcnNetworkPolicyResult.aidl @@ -14,18 +14,7 @@ * limitations under the License. */ -package com.android.networkstack.tethering; +package android.net.vcn; -import com.android.net.module.util.Struct; -import com.android.net.module.util.Struct.Field; -import com.android.net.module.util.Struct.Type; - -/** The key of BpfMap which is used for mapping interface index. */ -public class TetherDevValue extends Struct { - @Field(order = 0, type = Type.S32) - public final int ifIndex; // interface index - - public TetherDevValue(final int ifIndex) { - this.ifIndex = ifIndex; - } -} +/** @hide */ +parcelable VcnNetworkPolicyResult; diff --git a/Vcn/framework-b/src/android/net/vcn/VcnNetworkPolicyResult.java b/Vcn/framework-b/src/android/net/vcn/VcnNetworkPolicyResult.java new file mode 100644 index 0000000000..fca084a00a --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnNetworkPolicyResult.java @@ -0,0 +1,124 @@ +/* + * 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 android.net.vcn; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.net.NetworkCapabilities; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * VcnNetworkPolicyResult represents the Network policy result for a Network transport applying its + * VCN policy via {@link VcnManager#applyVcnNetworkPolicy(NetworkCapabilities, LinkProperties)}. + * + *

Bearers that are bringing up networks capable of acting as a VCN's underlying network should + * query for Network policy results upon any capability changes (e.g. changing of TRUSTED bit), and + * when prompted by VcnManagementService via {@link VcnManager.VcnNetworkPolicyListener}. + * + * @hide + */ +@SystemApi +public final class VcnNetworkPolicyResult implements Parcelable { + private final boolean mIsTearDownRequested; + private final NetworkCapabilities mNetworkCapabilities; + + /** + * Constructs a VcnNetworkPolicyResult with the specified parameters. + * + * @hide + */ + public VcnNetworkPolicyResult( + boolean isTearDownRequested, @NonNull NetworkCapabilities networkCapabilities) { + Objects.requireNonNull(networkCapabilities, "networkCapabilities must be non-null"); + + mIsTearDownRequested = isTearDownRequested; + mNetworkCapabilities = networkCapabilities; + } + + /** + * Returns whether this VCN policy result requires that the underlying Network should be torn + * down. + * + *

Upon querying for the current Network policy result, the bearer must check this method, + * and MUST tear down the corresponding Network if it returns true. + */ + public boolean isTeardownRequested() { + return mIsTearDownRequested; + } + + /** + * Returns the NetworkCapabilities that the bearer should be using for the corresponding + * Network. + */ + @NonNull + public NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities; + } + + @Override + public int hashCode() { + return Objects.hash(mIsTearDownRequested, mNetworkCapabilities); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof VcnNetworkPolicyResult)) return false; + final VcnNetworkPolicyResult that = (VcnNetworkPolicyResult) o; + + return mIsTearDownRequested == that.mIsTearDownRequested + && mNetworkCapabilities.equals(that.mNetworkCapabilities); + } + + @Override + public String toString() { + return "VcnNetworkPolicyResult { " + + "mIsTeardownRequested = " + + mIsTearDownRequested + + ", mNetworkCapabilities" + + mNetworkCapabilities + + " }"; + } + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mIsTearDownRequested); + dest.writeParcelable(mNetworkCapabilities, flags); + } + + /** Implement the Parcelable interface */ + public static final @NonNull Creator CREATOR = + new Creator() { + public VcnNetworkPolicyResult createFromParcel(Parcel in) { + return new VcnNetworkPolicyResult(in.readBoolean(), in.readParcelable(null, android.net.NetworkCapabilities.class)); + } + + public VcnNetworkPolicyResult[] newArray(int size) { + return new VcnNetworkPolicyResult[size]; + } + }; +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnTransportInfo.java b/Vcn/framework-b/src/android/net/vcn/VcnTransportInfo.java new file mode 100644 index 0000000000..a760b12ac8 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnTransportInfo.java @@ -0,0 +1,318 @@ +/* + * 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 android.net.vcn; + +import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; +import static android.net.vcn.Flags.FLAG_MAINLINE_VCN_MODULE_API; +import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS; +import static android.net.vcn.VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; + +import android.annotation.FlaggedApi; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.net.NetworkCapabilities; +import android.net.TransportInfo; +import android.net.wifi.WifiInfo; +import android.os.Parcel; +import android.os.Parcelable; +import android.telephony.SubscriptionManager; + +import com.android.internal.util.Preconditions; + +import java.util.Objects; + +/** + * VcnTransportInfo contains information about the VCN's underlying transports for SysUi. + * + *

Presence of this class in the NetworkCapabilities.TransportInfo implies that the network is a + * VCN. + * + *

VcnTransportInfo must exist on top of either an underlying Wifi or Cellular Network. If the + * underlying Network is WiFi, the subId will be {@link + * SubscriptionManager#INVALID_SUBSCRIPTION_ID}. If the underlying Network is Cellular, the WifiInfo + * will be {@code null}. + * + *

Receipt of a VcnTransportInfo requires the NETWORK_SETTINGS permission; else the entire + * VcnTransportInfo instance will be redacted. + * + * @hide + */ +// TODO: Do not store WifiInfo and subscription ID in VcnTransportInfo anymore +@FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public final class VcnTransportInfo implements TransportInfo, Parcelable { + @Nullable private final WifiInfo mWifiInfo; + private final int mSubId; + private final int mMinUdpPort4500NatTimeoutSeconds; + + /** @hide */ + public VcnTransportInfo(@NonNull WifiInfo wifiInfo) { + this(wifiInfo, INVALID_SUBSCRIPTION_ID, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET); + } + + /** @hide */ + public VcnTransportInfo(@NonNull WifiInfo wifiInfo, int minUdpPort4500NatTimeoutSeconds) { + this(wifiInfo, INVALID_SUBSCRIPTION_ID, minUdpPort4500NatTimeoutSeconds); + } + + /** @hide */ + public VcnTransportInfo(int subId) { + this(null /* wifiInfo */, subId, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET); + } + + /** @hide */ + public VcnTransportInfo(int subId, int minUdpPort4500NatTimeoutSeconds) { + this(null /* wifiInfo */, subId, minUdpPort4500NatTimeoutSeconds); + } + + private VcnTransportInfo( + @Nullable WifiInfo wifiInfo, int subId, int minUdpPort4500NatTimeoutSeconds) { + mWifiInfo = wifiInfo; + mSubId = subId; + mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds; + } + + /** + * Get the {@link WifiInfo} for this VcnTransportInfo. + * + *

If the underlying Network for the associated VCN is Cellular, returns null. + * + * @return the WifiInfo if there is an underlying WiFi connection, else null. + * @hide + */ + @Nullable + public WifiInfo getWifiInfo() { + return mWifiInfo; + } + + /** + * Get the subId for the VCN Network associated with this VcnTransportInfo. + * + *

If the underlying Network for the associated VCN is WiFi, returns {@link + * SubscriptionManager#INVALID_SUBSCRIPTION_ID}. + * + * @return the Subscription ID if a cellular underlying Network is present, else {@link + * android.telephony.SubscriptionManager#INVALID_SUBSCRIPTION_ID}. + * @hide + */ + public int getSubId() { + return mSubId; + } + + /** + * Get the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping. + * + *

To ensure uninterrupted connectivity, the device must send keepalive packets before the + * timeout. Failure to do so may result in the mapping being cleared and connection termination. + * This value is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs, + * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data. + * + * @return the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping, or + * VcnGatewayConnectionConfig.MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET if not set. + * @see VcnGatewayConnectionConfig.Builder#setMinUdpPort4500NatTimeoutSeconds(int) + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public int getMinUdpPort4500NatTimeoutSeconds() { + return mMinUdpPort4500NatTimeoutSeconds; + } + + @Override + public int hashCode() { + return Objects.hash(mWifiInfo, mSubId, mMinUdpPort4500NatTimeoutSeconds); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof VcnTransportInfo)) return false; + final VcnTransportInfo that = (VcnTransportInfo) o; + return Objects.equals(mWifiInfo, that.mWifiInfo) + && mSubId == that.mSubId + && mMinUdpPort4500NatTimeoutSeconds == that.mMinUdpPort4500NatTimeoutSeconds; + } + + /** + * {@inheritDoc} + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @Override + public int describeContents() { + return 0; + } + + /** + * Create a copy of a {@link VcnTransportInfo} with some fields redacted based on the + * permissions held by the receiving app. + * + * @param redactions bitmask of redactions that needs to be performed on this instance. + * @return the copy of this instance with the necessary redactions. + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @Override + @NonNull + public TransportInfo makeCopy(long redactions) { + if ((redactions & NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS) != 0) { + return new VcnTransportInfo( + null, INVALID_SUBSCRIPTION_ID, MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET); + } + + return new VcnTransportInfo( + (mWifiInfo == null) ? null : mWifiInfo.makeCopy(redactions), + mSubId, + mMinUdpPort4500NatTimeoutSeconds); + } + + /** @hide */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @Override + public long getApplicableRedactions() { + long redactions = REDACT_FOR_NETWORK_SETTINGS; + + // Add additional wifi redactions if necessary + if (mWifiInfo != null) { + redactions |= mWifiInfo.getApplicableRedactions(); + } + + return redactions; + } + + /** + * {@inheritDoc} + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSubId); + dest.writeParcelable(mWifiInfo, flags); + dest.writeInt(mMinUdpPort4500NatTimeoutSeconds); + } + + @Override + public String toString() { + return "VcnTransportInfo { mWifiInfo = " + mWifiInfo + ", mSubId = " + mSubId + " }"; + } + + /** + * Implement the Parcelable interface + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final @NonNull Creator CREATOR = + new Creator() { + public VcnTransportInfo createFromParcel(Parcel in) { + final int subId = in.readInt(); + final WifiInfo wifiInfo = + in.readParcelable(null, android.net.wifi.WifiInfo.class); + final int minUdpPort4500NatTimeoutSeconds = in.readInt(); + + // If all fields are their null values, return null TransportInfo to avoid + // leaking information about this being a VCN Network (instead of macro + // cellular, etc) + if (wifiInfo == null + && subId == INVALID_SUBSCRIPTION_ID + && minUdpPort4500NatTimeoutSeconds + == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET) { + return null; + } + + return new VcnTransportInfo(wifiInfo, subId, minUdpPort4500NatTimeoutSeconds); + } + + public VcnTransportInfo[] newArray(int size) { + return new VcnTransportInfo[size]; + } + }; + + /** + * This class can be used to construct a {@link VcnTransportInfo}. + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static final class Builder { + private int mMinUdpPort4500NatTimeoutSeconds = MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET; + + /** + * Construct Builder + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public Builder() {} + + /** + * Set the minimum duration that the VCN Gateway guarantees to preserve a NAT mapping. + * + *

This is used as a power-optimization hint for other IKEv2/IPsec use cases (e.g. VPNs, + * or IWLAN) to reduce the necessary keepalive frequency, thus conserving power and data. + * + * @param minUdpPort4500NatTimeoutSeconds the minimum duration that the VCN Gateway + * guarantees to preserve a NAT mapping, or {@link MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET} + * to clear this value. To ensure uninterrupted connectivity, the device must send + * keepalive packets within this interval. Failure to do so may result in the mapping + * being cleared and connection termination. + * @return this {@link Builder} instance, for chaining + * @see VcnGatewayConnectionConfig.Builder#setMinUdpPort4500NatTimeoutSeconds(int) + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + public Builder setMinUdpPort4500NatTimeoutSeconds( + @IntRange(from = MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS) + int minUdpPort4500NatTimeoutSeconds) { + Preconditions.checkArgument( + minUdpPort4500NatTimeoutSeconds == MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET + || minUdpPort4500NatTimeoutSeconds + >= MIN_UDP_PORT_4500_NAT_TIMEOUT_SECONDS, + "Timeout must be at least 120s or MIN_UDP_PORT_4500_NAT_TIMEOUT_UNSET"); + + mMinUdpPort4500NatTimeoutSeconds = minUdpPort4500NatTimeoutSeconds; + return Builder.this; + } + + /** + * Build a VcnTransportInfo instance + * + * @hide + */ + @FlaggedApi(FLAG_MAINLINE_VCN_MODULE_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @NonNull + public VcnTransportInfo build() { + return new VcnTransportInfo( + null /* wifiInfo */, INVALID_SUBSCRIPTION_ID, mMinUdpPort4500NatTimeoutSeconds); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkPolicy.aidl b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkPolicy.aidl new file mode 100644 index 0000000000..6cb6ee685a --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkPolicy.aidl @@ -0,0 +1,20 @@ +/* + * 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; + +/** @hide */ +parcelable VcnUnderlyingNetworkPolicy; diff --git a/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkPolicy.java b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkPolicy.java new file mode 100644 index 0000000000..2b5305d05d --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkPolicy.java @@ -0,0 +1,116 @@ +/* + * 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 android.annotation.NonNull; +import android.net.NetworkCapabilities; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * VcnUnderlyingNetworkPolicy represents the Network policy for a VCN-managed Network. + * + *

Transports that are bringing up networks capable of acting as a VCN's underlying network + * should query for policy state upon major capability changes (e.g. changing of TRUSTED bit), and + * when prompted by VcnManagementService via VcnUnderlyingNetworkPolicyListener. + * + * @hide + */ +public final class VcnUnderlyingNetworkPolicy implements Parcelable { + private final VcnNetworkPolicyResult mVcnNetworkPolicyResult; + + /** + * Constructs a VcnUnderlyingNetworkPolicy with the specified parameters. + * + * @hide + */ + public VcnUnderlyingNetworkPolicy( + boolean isTearDownRequested, @NonNull NetworkCapabilities mergedNetworkCapabilities) { + Objects.requireNonNull( + mergedNetworkCapabilities, "mergedNetworkCapabilities must be nonnull"); + + mVcnNetworkPolicyResult = + new VcnNetworkPolicyResult(isTearDownRequested, mergedNetworkCapabilities); + } + + private VcnUnderlyingNetworkPolicy(@NonNull VcnNetworkPolicyResult vcnNetworkPolicyResult) { + this.mVcnNetworkPolicyResult = + Objects.requireNonNull(vcnNetworkPolicyResult, "vcnNetworkPolicyResult"); + } + + /** + * Returns whether this Carrier VCN policy policy indicates that the underlying Network should + * be torn down. + */ + public boolean isTeardownRequested() { + return mVcnNetworkPolicyResult.isTeardownRequested(); + } + + /** + * Returns the NetworkCapabilities with Carrier VCN policy bits merged into the provided + * capabilities. + */ + @NonNull + public NetworkCapabilities getMergedNetworkCapabilities() { + return mVcnNetworkPolicyResult.getNetworkCapabilities(); + } + + @Override + public int hashCode() { + return Objects.hash(mVcnNetworkPolicyResult); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof VcnUnderlyingNetworkPolicy)) return false; + final VcnUnderlyingNetworkPolicy that = (VcnUnderlyingNetworkPolicy) o; + + return mVcnNetworkPolicyResult.equals(that.mVcnNetworkPolicyResult); + } + + @Override + public String toString() { + return mVcnNetworkPolicyResult.toString(); + } + + /** {@inheritDoc} */ + @Override + public int describeContents() { + return 0; + } + + /** {@inheritDoc} */ + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mVcnNetworkPolicyResult, flags); + } + + /** Implement the Parcelable interface */ + public static final @NonNull Creator CREATOR = + new Creator() { + public VcnUnderlyingNetworkPolicy createFromParcel(Parcel in) { + return new VcnUnderlyingNetworkPolicy(in.readParcelable(null, android.net.vcn.VcnNetworkPolicyResult.class)); + } + + public VcnUnderlyingNetworkPolicy[] newArray(int size) { + return new VcnUnderlyingNetworkPolicy[size]; + } + }; +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkSpecifier.java b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkSpecifier.java new file mode 100644 index 0000000000..e1d1b3c65c --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkSpecifier.java @@ -0,0 +1,129 @@ +/* + * 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 android.net.vcn; + +import android.annotation.NonNull; +import android.net.NetworkSpecifier; +import android.net.TelephonyNetworkSpecifier; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; + +import java.util.Arrays; +import java.util.Objects; + +/** + * NetworkSpecifier object for VCN underlying network requests. + * + *

This matches any underlying network with the appropriate subIds. + * + * @hide + */ +public final class VcnUnderlyingNetworkSpecifier extends NetworkSpecifier implements Parcelable { + @NonNull private final int[] mSubIds; + + /** + * Builds a new VcnUnderlyingNetworkSpecifier with the given list of subIds + * + * @hide + */ + public VcnUnderlyingNetworkSpecifier(@NonNull int[] subIds) { + mSubIds = Objects.requireNonNull(subIds, "subIds were null"); + } + + /** + * Retrieves the list of subIds supported by this VcnUnderlyingNetworkSpecifier + * + * @hide + */ + @NonNull + @VisibleForTesting(visibility = Visibility.PRIVATE) + public int[] getSubIds() { + return mSubIds; + } + + public static final @NonNull Creator CREATOR = + new Creator() { + @Override + public VcnUnderlyingNetworkSpecifier createFromParcel(Parcel in) { + int[] subIds = in.createIntArray(); + return new VcnUnderlyingNetworkSpecifier(subIds); + } + + @Override + public VcnUnderlyingNetworkSpecifier[] newArray(int size) { + return new VcnUnderlyingNetworkSpecifier[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeIntArray(mSubIds); + } + + @Override + public int hashCode() { + return Arrays.hashCode(mSubIds); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof VcnUnderlyingNetworkSpecifier)) { + return false; + } + + VcnUnderlyingNetworkSpecifier lhs = (VcnUnderlyingNetworkSpecifier) obj; + return Arrays.equals(mSubIds, lhs.mSubIds); + } + + @Override + public String toString() { + return new StringBuilder() + .append("VcnUnderlyingNetworkSpecifier [") + .append("mSubIds = ").append(Arrays.toString(mSubIds)) + .append("]") + .toString(); + } + + /** @hide */ + @Override + public boolean canBeSatisfiedBy(NetworkSpecifier other) { + if (other instanceof TelephonyNetworkSpecifier) { + final int targetSubId = ((TelephonyNetworkSpecifier) other).getSubscriptionId(); + for (int subId : mSubIds) { + if (targetSubId == subId) { + return true; + } + } + return false; + } + // TODO(b/180140053): Allow matching against WifiNetworkAgentSpecifier + + // MatchAllNetworkSpecifier matched in NetworkCapabilities. + return equals(other); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkTemplate.java b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkTemplate.java new file mode 100644 index 0000000000..16114dd135 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnUnderlyingNetworkTemplate.java @@ -0,0 +1,314 @@ +/* + * 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 android.net.vcn; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.PersistableBundle; +import android.util.IndentingPrintWriter; +import android.util.SparseArray; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Map; +import java.util.Objects; + +/** + * This class represents a template containing set of underlying network requirements for doing + * route selection. + * + *

Apps provisioning a VCN can configure the underlying network priority for each Gateway + * Connection by setting a list (in priority order, most to least preferred) of the appropriate + * subclasses in the VcnGatewayConnectionConfig. See {@link + * VcnGatewayConnectionConfig.Builder#setVcnUnderlyingNetworkPriorities} + */ +public abstract class VcnUnderlyingNetworkTemplate { + /** @hide */ + static final int NETWORK_PRIORITY_TYPE_WIFI = 1; + /** @hide */ + static final int NETWORK_PRIORITY_TYPE_CELL = 2; + + /** + * Used to configure the matching criteria of a network characteristic. This may include network + * capabilities, or cellular subscription information. Denotes that networks with or without the + * characteristic are both acceptable to match this template. + */ + public static final int MATCH_ANY = 0; + + /** + * Used to configure the matching criteria of a network characteristic. This may include network + * capabilities, or cellular subscription information. Denotes that a network MUST have the + * capability in order to match this template. + */ + public static final int MATCH_REQUIRED = 1; + + /** + * Used to configure the matching criteria of a network characteristic. This may include network + * capabilities, or cellular subscription information. Denotes that a network MUST NOT have the + * capability in order to match this template. + */ + public static final int MATCH_FORBIDDEN = 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({MATCH_ANY, MATCH_REQUIRED, MATCH_FORBIDDEN}) + public @interface MatchCriteria {} + + private static final SparseArray MATCH_CRITERIA_TO_STRING_MAP = new SparseArray<>(); + + static { + MATCH_CRITERIA_TO_STRING_MAP.put(MATCH_ANY, "MATCH_ANY"); + MATCH_CRITERIA_TO_STRING_MAP.put(MATCH_REQUIRED, "MATCH_REQUIRED"); + MATCH_CRITERIA_TO_STRING_MAP.put(MATCH_FORBIDDEN, "MATCH_FORBIDDEN"); + } + + private static final String NETWORK_PRIORITY_TYPE_KEY = "mNetworkPriorityType"; + private final int mNetworkPriorityType; + + /** @hide */ + static final String METERED_MATCH_KEY = "mMeteredMatchCriteria"; + + /** @hide */ + static final int DEFAULT_METERED_MATCH_CRITERIA = MATCH_ANY; + + private final int mMeteredMatchCriteria; + + /** @hide */ + public static final int DEFAULT_MIN_BANDWIDTH_KBPS = 0; + + /** @hide */ + static final String MIN_ENTRY_UPSTREAM_BANDWIDTH_KBPS_KEY = "mMinEntryUpstreamBandwidthKbps"; + + private final int mMinEntryUpstreamBandwidthKbps; + + /** @hide */ + static final String MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS_KEY = "mMinExitUpstreamBandwidthKbps"; + + private final int mMinExitUpstreamBandwidthKbps; + + /** @hide */ + static final String MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS_KEY = + "mMinEntryDownstreamBandwidthKbps"; + + private final int mMinEntryDownstreamBandwidthKbps; + + /** @hide */ + static final String MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS_KEY = "mMinExitDownstreamBandwidthKbps"; + + private final int mMinExitDownstreamBandwidthKbps; + + /** @hide */ + VcnUnderlyingNetworkTemplate( + int networkPriorityType, + int meteredMatchCriteria, + int minEntryUpstreamBandwidthKbps, + int minExitUpstreamBandwidthKbps, + int minEntryDownstreamBandwidthKbps, + int minExitDownstreamBandwidthKbps) { + mNetworkPriorityType = networkPriorityType; + mMeteredMatchCriteria = meteredMatchCriteria; + mMinEntryUpstreamBandwidthKbps = minEntryUpstreamBandwidthKbps; + mMinExitUpstreamBandwidthKbps = minExitUpstreamBandwidthKbps; + mMinEntryDownstreamBandwidthKbps = minEntryDownstreamBandwidthKbps; + mMinExitDownstreamBandwidthKbps = minExitDownstreamBandwidthKbps; + } + + /** @hide */ + static void validateMatchCriteria(int matchCriteria, String matchingCapability) { + Preconditions.checkArgument( + MATCH_CRITERIA_TO_STRING_MAP.contains(matchCriteria), + "Invalid matching criteria: " + matchCriteria + " for " + matchingCapability); + } + + /** @hide */ + static void validateMinBandwidthKbps(int minEntryBandwidth, int minExitBandwidth) { + Preconditions.checkArgument( + minEntryBandwidth >= 0, "Invalid minEntryBandwidth, must be >= 0"); + Preconditions.checkArgument( + minExitBandwidth >= 0, "Invalid minExitBandwidth, must be >= 0"); + Preconditions.checkArgument( + minEntryBandwidth >= minExitBandwidth, + "Minimum entry bandwidth must be >= exit bandwidth"); + } + + /** @hide */ + protected void validate() { + validateMatchCriteria(mMeteredMatchCriteria, "mMeteredMatchCriteria"); + validateMinBandwidthKbps(mMinEntryUpstreamBandwidthKbps, mMinExitUpstreamBandwidthKbps); + validateMinBandwidthKbps(mMinEntryDownstreamBandwidthKbps, mMinExitDownstreamBandwidthKbps); + } + + /** @hide */ + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public static VcnUnderlyingNetworkTemplate fromPersistableBundle( + @NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle is null"); + + final int networkPriorityType = in.getInt(NETWORK_PRIORITY_TYPE_KEY); + switch (networkPriorityType) { + case NETWORK_PRIORITY_TYPE_WIFI: + return VcnWifiUnderlyingNetworkTemplate.fromPersistableBundle(in); + case NETWORK_PRIORITY_TYPE_CELL: + return VcnCellUnderlyingNetworkTemplate.fromPersistableBundle(in); + default: + throw new IllegalArgumentException( + "Invalid networkPriorityType:" + networkPriorityType); + } + } + + /** @hide */ + @NonNull + PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(NETWORK_PRIORITY_TYPE_KEY, mNetworkPriorityType); + result.putInt(METERED_MATCH_KEY, mMeteredMatchCriteria); + result.putInt(MIN_ENTRY_UPSTREAM_BANDWIDTH_KBPS_KEY, mMinEntryUpstreamBandwidthKbps); + result.putInt(MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS_KEY, mMinExitUpstreamBandwidthKbps); + result.putInt(MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS_KEY, mMinEntryDownstreamBandwidthKbps); + result.putInt(MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS_KEY, mMinExitDownstreamBandwidthKbps); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash( + mNetworkPriorityType, + mMeteredMatchCriteria, + mMinEntryUpstreamBandwidthKbps, + mMinExitUpstreamBandwidthKbps, + mMinEntryDownstreamBandwidthKbps, + mMinExitDownstreamBandwidthKbps); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof VcnUnderlyingNetworkTemplate)) { + return false; + } + + final VcnUnderlyingNetworkTemplate rhs = (VcnUnderlyingNetworkTemplate) other; + return mNetworkPriorityType == rhs.mNetworkPriorityType + && mMeteredMatchCriteria == rhs.mMeteredMatchCriteria + && mMinEntryUpstreamBandwidthKbps == rhs.mMinEntryUpstreamBandwidthKbps + && mMinExitUpstreamBandwidthKbps == rhs.mMinExitUpstreamBandwidthKbps + && mMinEntryDownstreamBandwidthKbps == rhs.mMinEntryDownstreamBandwidthKbps + && mMinExitDownstreamBandwidthKbps == rhs.mMinExitDownstreamBandwidthKbps; + } + + /** @hide */ + static String getNameString(SparseArray toStringMap, int key) { + return toStringMap.get(key, "Invalid value " + key); + } + + /** @hide */ + static String getMatchCriteriaString(int matchCriteria) { + return getNameString(MATCH_CRITERIA_TO_STRING_MAP, matchCriteria); + } + + /** @hide */ + abstract void dumpTransportSpecificFields(IndentingPrintWriter pw); + + /** + * Dumps the state of this record for logging and debugging purposes. + * + * @hide + */ + public void dump(IndentingPrintWriter pw) { + pw.println(this.getClass().getSimpleName() + ":"); + pw.increaseIndent(); + + if (mMeteredMatchCriteria != DEFAULT_METERED_MATCH_CRITERIA) { + pw.println("mMeteredMatchCriteria: " + getMatchCriteriaString(mMeteredMatchCriteria)); + } + if (mMinEntryUpstreamBandwidthKbps != DEFAULT_MIN_BANDWIDTH_KBPS) { + pw.println("mMinEntryUpstreamBandwidthKbps: " + mMinEntryUpstreamBandwidthKbps); + } + if (mMinExitUpstreamBandwidthKbps != DEFAULT_MIN_BANDWIDTH_KBPS) { + pw.println("mMinExitUpstreamBandwidthKbps: " + mMinExitUpstreamBandwidthKbps); + } + if (mMinEntryDownstreamBandwidthKbps != DEFAULT_MIN_BANDWIDTH_KBPS) { + pw.println("mMinEntryDownstreamBandwidthKbps: " + mMinEntryDownstreamBandwidthKbps); + } + if (mMinExitDownstreamBandwidthKbps != DEFAULT_MIN_BANDWIDTH_KBPS) { + pw.println("mMinExitDownstreamBandwidthKbps: " + mMinExitDownstreamBandwidthKbps); + } + dumpTransportSpecificFields(pw); + + pw.decreaseIndent(); + } + + /** + * Return the matching criteria for metered networks. + * + * @see VcnWifiUnderlyingNetworkTemplate.Builder#setMetered(int) + * @see VcnCellUnderlyingNetworkTemplate.Builder#setMetered(int) + */ + public int getMetered() { + return mMeteredMatchCriteria; + } + + /** + * Returns the minimum entry upstream bandwidth allowed by this template. + * + * @see VcnWifiUnderlyingNetworkTemplate.Builder#setMinUpstreamBandwidthKbps(int, int) + * @see VcnCellUnderlyingNetworkTemplate.Builder#setMinUpstreamBandwidthKbps(int, int) + */ + public int getMinEntryUpstreamBandwidthKbps() { + return mMinEntryUpstreamBandwidthKbps; + } + + /** + * Returns the minimum exit upstream bandwidth allowed by this template. + * + * @see VcnWifiUnderlyingNetworkTemplate.Builder#setMinUpstreamBandwidthKbps(int, int) + * @see VcnCellUnderlyingNetworkTemplate.Builder#setMinUpstreamBandwidthKbps(int, int) + */ + public int getMinExitUpstreamBandwidthKbps() { + return mMinExitUpstreamBandwidthKbps; + } + + /** + * Returns the minimum entry downstream bandwidth allowed by this template. + * + * @see VcnWifiUnderlyingNetworkTemplate.Builder#setMinDownstreamBandwidthKbps(int, int) + * @see VcnCellUnderlyingNetworkTemplate.Builder#setMinDownstreamBandwidthKbps(int, int) + */ + public int getMinEntryDownstreamBandwidthKbps() { + return mMinEntryDownstreamBandwidthKbps; + } + + /** + * Returns the minimum exit downstream bandwidth allowed by this template. + * + * @see VcnWifiUnderlyingNetworkTemplate.Builder#setMinDownstreamBandwidthKbps(int, int) + * @see VcnCellUnderlyingNetworkTemplate.Builder#setMinDownstreamBandwidthKbps(int, int) + */ + public int getMinExitDownstreamBandwidthKbps() { + return mMinExitDownstreamBandwidthKbps; + } + + /** @hide */ + public abstract Map getCapabilitiesMatchCriteria(); +} diff --git a/Vcn/framework-b/src/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java b/Vcn/framework-b/src/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java new file mode 100644 index 0000000000..770a8c118a --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/VcnWifiUnderlyingNetworkTemplate.java @@ -0,0 +1,315 @@ +/* + * 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 android.net.vcn; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_ANY; +import static android.net.vcn.util.PersistableBundleUtils.STRING_DESERIALIZER; +import static android.net.vcn.util.PersistableBundleUtils.STRING_SERIALIZER; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.net.NetworkCapabilities; +import android.net.vcn.VcnUnderlyingNetworkTemplate.MatchCriteria; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * This class represents a configuration for a network template class of underlying Carrier WiFi + * networks. + * + *

See {@link VcnUnderlyingNetworkTemplate} + */ +public final class VcnWifiUnderlyingNetworkTemplate extends VcnUnderlyingNetworkTemplate { + private static final String SSIDS_KEY = "mSsids"; + @Nullable private final Set mSsids; + + private VcnWifiUnderlyingNetworkTemplate( + int meteredMatchCriteria, + int minEntryUpstreamBandwidthKbps, + int minExitUpstreamBandwidthKbps, + int minEntryDownstreamBandwidthKbps, + int minExitDownstreamBandwidthKbps, + Set ssids) { + super( + NETWORK_PRIORITY_TYPE_WIFI, + meteredMatchCriteria, + minEntryUpstreamBandwidthKbps, + minExitUpstreamBandwidthKbps, + minEntryDownstreamBandwidthKbps, + minExitDownstreamBandwidthKbps); + mSsids = new ArraySet<>(ssids); + + validate(); + } + + /** @hide */ + @Override + protected void validate() { + super.validate(); + validateSsids(mSsids); + } + + private static void validateSsids(Set ssids) { + Objects.requireNonNull(ssids, "ssids is null"); + + for (String ssid : ssids) { + Objects.requireNonNull(ssid, "found null value ssid"); + } + } + + /** @hide */ + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public static VcnWifiUnderlyingNetworkTemplate fromPersistableBundle( + @NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle is null"); + + final int meteredMatchCriteria = in.getInt(METERED_MATCH_KEY); + + final int minEntryUpstreamBandwidthKbps = + in.getInt(MIN_ENTRY_UPSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + final int minExitUpstreamBandwidthKbps = + in.getInt(MIN_EXIT_UPSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + final int minEntryDownstreamBandwidthKbps = + in.getInt(MIN_ENTRY_DOWNSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + final int minExitDownstreamBandwidthKbps = + in.getInt(MIN_EXIT_DOWNSTREAM_BANDWIDTH_KBPS_KEY, DEFAULT_MIN_BANDWIDTH_KBPS); + + final PersistableBundle ssidsBundle = in.getPersistableBundle(SSIDS_KEY); + Objects.requireNonNull(ssidsBundle, "ssidsBundle is null"); + final Set ssids = + new ArraySet( + PersistableBundleUtils.toList(ssidsBundle, STRING_DESERIALIZER)); + return new VcnWifiUnderlyingNetworkTemplate( + meteredMatchCriteria, + minEntryUpstreamBandwidthKbps, + minExitUpstreamBandwidthKbps, + minEntryDownstreamBandwidthKbps, + minExitDownstreamBandwidthKbps, + ssids); + } + + /** @hide */ + @Override + @NonNull + @VisibleForTesting(visibility = Visibility.PROTECTED) + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = super.toPersistableBundle(); + + final PersistableBundle ssidsBundle = + PersistableBundleUtils.fromList(new ArrayList<>(mSsids), STRING_SERIALIZER); + result.putPersistableBundle(SSIDS_KEY, ssidsBundle); + + return result; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), mSsids); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!super.equals(other)) { + return false; + } + + if (!(other instanceof VcnWifiUnderlyingNetworkTemplate)) { + return false; + } + + final VcnWifiUnderlyingNetworkTemplate rhs = (VcnWifiUnderlyingNetworkTemplate) other; + return mSsids.equals(rhs.mSsids); + } + + /** @hide */ + @Override + void dumpTransportSpecificFields(IndentingPrintWriter pw) { + if (!mSsids.isEmpty()) { + pw.println("mSsids: " + mSsids); + } + } + + /** + * Retrieve the matching SSIDs, or an empty set if any SSID is acceptable. + * + * @see Builder#setSsids(Set) + */ + @NonNull + public Set getSsids() { + return Collections.unmodifiableSet(mSsids); + } + + /** @hide */ + @Override + public Map getCapabilitiesMatchCriteria() { + return Collections.singletonMap(NET_CAPABILITY_INTERNET, MATCH_REQUIRED); + } + + /** This class is used to incrementally build VcnWifiUnderlyingNetworkTemplate objects. */ + public static final class Builder { + private int mMeteredMatchCriteria = MATCH_ANY; + @NonNull private final Set mSsids = new ArraySet<>(); + + private int mMinEntryUpstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + private int mMinExitUpstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + private int mMinEntryDownstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + private int mMinExitDownstreamBandwidthKbps = DEFAULT_MIN_BANDWIDTH_KBPS; + + /** Construct a Builder object. */ + public Builder() {} + + /** + * Set the matching criteria for metered networks. + * + *

A template where setMetered(MATCH_REQUIRED) will only match metered networks (one + * without NET_CAPABILITY_NOT_METERED). A template where setMetered(MATCH_FORBIDDEN) will + * only match a network that is not metered (one with NET_CAPABILITY_NOT_METERED). + * + * @param matchCriteria the matching criteria for metered networks. Defaults to {@link + * #MATCH_ANY}. + * @see NetworkCapabilities#NET_CAPABILITY_NOT_METERED + */ + // The matching getter is defined in the super class. Please see {@link + // VcnUnderlyingNetworkTemplate#getMetered()} + @SuppressLint("MissingGetterMatchingBuilder") + @NonNull + public Builder setMetered(@MatchCriteria int matchCriteria) { + validateMatchCriteria(matchCriteria, "setMetered"); + + mMeteredMatchCriteria = matchCriteria; + return this; + } + + /** + * Set the SSIDs with which a network can match this priority rule. + * + * @param ssids the matching SSIDs. Network with one of the matching SSIDs can match this + * priority rule. If the set is empty, any SSID will match. The default is an empty set. + */ + @NonNull + public Builder setSsids(@NonNull Set ssids) { + validateSsids(ssids); + + mSsids.clear(); + mSsids.addAll(ssids); + return this; + } + + /** + * Set the minimum upstream bandwidths that this template will match. + * + *

This template will not match a network that does not provide at least the bandwidth + * passed as the entry bandwidth, except in the case that the network is selected as the VCN + * Gateway Connection's underlying network, where it will continue to match until the + * bandwidth drops under the exit bandwidth. + * + *

The entry criteria MUST be greater than, or equal to the exit criteria to avoid the + * invalid case where a network fulfills the entry criteria, but at the same time fails the + * exit criteria. + * + *

Estimated bandwidth of a network is provided by the transport layer, and reported in + * {@link NetworkCapabilities}. The provided estimates will be used without modification. + * + * @param minEntryUpstreamBandwidthKbps the minimum accepted upstream bandwidth for networks + * that ARE NOT the already-selected underlying network, or {@code 0} to disable this + * requirement. Disabled by default. + * @param minExitUpstreamBandwidthKbps the minimum accepted upstream bandwidth for a network + * that IS the already-selected underlying network, or {@code 0} to disable this + * requirement. Disabled by default. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + // The getter for the two integers are separated, and in the superclass. Please see {@link + // VcnUnderlyingNetworkTemplate#getMinEntryUpstreamBandwidthKbps()} and {@link + // VcnUnderlyingNetworkTemplate#getMinExitUpstreamBandwidthKbps()} + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setMinUpstreamBandwidthKbps( + int minEntryUpstreamBandwidthKbps, int minExitUpstreamBandwidthKbps) { + validateMinBandwidthKbps(minEntryUpstreamBandwidthKbps, minExitUpstreamBandwidthKbps); + + mMinEntryUpstreamBandwidthKbps = minEntryUpstreamBandwidthKbps; + mMinExitUpstreamBandwidthKbps = minExitUpstreamBandwidthKbps; + + return this; + } + + /** + * Set the minimum upstream bandwidths that this template will match. + * + *

This template will not match a network that does not provide at least the bandwidth + * passed as the entry bandwidth, except in the case that the network is selected as the VCN + * Gateway Connection's underlying network, where it will continue to match until the + * bandwidth drops under the exit bandwidth. + * + *

The entry criteria MUST be greater than, or equal to the exit criteria to avoid the + * invalid case where a network fulfills the entry criteria, but at the same time fails the + * exit criteria. + * + *

Estimated bandwidth of a network is provided by the transport layer, and reported in + * {@link NetworkCapabilities}. The provided estimates will be used without modification. + * + * @param minEntryDownstreamBandwidthKbps the minimum accepted downstream bandwidth for + * networks that ARE NOT the already-selected underlying network, or {@code 0} to + * disable this requirement. Disabled by default. + * @param minExitDownstreamBandwidthKbps the minimum accepted downstream bandwidth for a + * network that IS the already-selected underlying network, or {@code 0} to disable this + * requirement. Disabled by default. + * @return this {@link Builder} instance, for chaining + */ + @NonNull + // The getter for the two integers are separated, and in the superclass. Please see {@link + // VcnUnderlyingNetworkTemplate#getMinEntryDownstreamBandwidthKbps()} and {@link + // VcnUnderlyingNetworkTemplate#getMinExitDownstreamBandwidthKbps()} + @SuppressLint("MissingGetterMatchingBuilder") + public Builder setMinDownstreamBandwidthKbps( + int minEntryDownstreamBandwidthKbps, int minExitDownstreamBandwidthKbps) { + validateMinBandwidthKbps( + minEntryDownstreamBandwidthKbps, minExitDownstreamBandwidthKbps); + + mMinEntryDownstreamBandwidthKbps = minEntryDownstreamBandwidthKbps; + mMinExitDownstreamBandwidthKbps = minExitDownstreamBandwidthKbps; + + return this; + } + + /** Build the VcnWifiUnderlyingNetworkTemplate. */ + @NonNull + public VcnWifiUnderlyingNetworkTemplate build() { + return new VcnWifiUnderlyingNetworkTemplate( + mMeteredMatchCriteria, + mMinEntryUpstreamBandwidthKbps, + mMinExitUpstreamBandwidthKbps, + mMinEntryDownstreamBandwidthKbps, + mMinExitDownstreamBandwidthKbps, + mSsids); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/CertUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/CertUtils.java new file mode 100644 index 0000000000..35b3186877 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/CertUtils.java @@ -0,0 +1,66 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Objects; + +/** + * CertUtils provides utility methods for constructing Certificate and PrivateKey. + * + * @hide + */ +public class CertUtils { + private static final String CERT_TYPE_X509 = "X.509"; + private static final String PRIVATE_KEY_TYPE_RSA = "RSA"; + + /** Decodes an ASN.1 DER encoded Certificate */ + public static X509Certificate certificateFromByteArray(byte[] derEncoded) { + Objects.requireNonNull(derEncoded, "derEncoded is null"); + + try { + CertificateFactory certFactory = CertificateFactory.getInstance(CERT_TYPE_X509); + InputStream in = new ByteArrayInputStream(derEncoded); + return (X509Certificate) certFactory.generateCertificate(in); + } catch (CertificateException e) { + throw new IllegalArgumentException("Fail to decode certificate", e); + } + } + + /** Decodes a PKCS#8 encoded RSA private key */ + public static RSAPrivateKey privateKeyFromByteArray(byte[] pkcs8Encoded) { + Objects.requireNonNull(pkcs8Encoded, "pkcs8Encoded was null"); + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(pkcs8Encoded); + + try { + KeyFactory keyFactory = KeyFactory.getInstance(PRIVATE_KEY_TYPE_RSA); + + return (RSAPrivateKey) keyFactory.generatePrivate(privateKeySpec); + } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { + throw new IllegalArgumentException("Fail to decode PrivateKey", e); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/ChildSaProposalUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/ChildSaProposalUtils.java new file mode 100644 index 0000000000..48c1b25a97 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/ChildSaProposalUtils.java @@ -0,0 +1,73 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.net.ipsec.ike.ChildSaProposal; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; +import java.util.Objects; + +/** + * Provides utility methods to convert ChildSaProposal to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class ChildSaProposalUtils extends SaProposalUtilsBase { + /** Serializes a ChildSaProposal to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(ChildSaProposal proposal) { + return SaProposalUtilsBase.toPersistableBundle(proposal); + } + + /** Constructs a ChildSaProposal by deserializing a PersistableBundle. */ + @NonNull + public static ChildSaProposal fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + final ChildSaProposal.Builder builder = new ChildSaProposal.Builder(); + + final PersistableBundle encryptionBundle = in.getPersistableBundle(ENCRYPT_ALGO_KEY); + Objects.requireNonNull(encryptionBundle, "Encryption algo bundle was null"); + final List encryptList = + PersistableBundleUtils.toList(encryptionBundle, EncryptionAlgoKeyLenPair::new); + for (EncryptionAlgoKeyLenPair t : encryptList) { + builder.addEncryptionAlgorithm(t.encryptionAlgo, t.keyLen); + } + + final int[] integrityAlgoIdArray = in.getIntArray(INTEGRITY_ALGO_KEY); + Objects.requireNonNull(integrityAlgoIdArray, "Integrity algo array was null"); + for (int algo : integrityAlgoIdArray) { + builder.addIntegrityAlgorithm(algo); + } + + final int[] dhGroupArray = in.getIntArray(DH_GROUP_KEY); + Objects.requireNonNull(dhGroupArray, "DH Group array was null"); + for (int dh : dhGroupArray) { + builder.addDhGroup(dh); + } + + return builder.build(); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/EapSessionConfigUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/EapSessionConfigUtils.java new file mode 100644 index 0000000000..dc1ee36b71 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/EapSessionConfigUtils.java @@ -0,0 +1,276 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.net.eap.EapSessionConfig; +import android.net.eap.EapSessionConfig.EapAkaConfig; +import android.net.eap.EapSessionConfig.EapAkaPrimeConfig; +import android.net.eap.EapSessionConfig.EapMethodConfig; +import android.net.eap.EapSessionConfig.EapMsChapV2Config; +import android.net.eap.EapSessionConfig.EapSimConfig; +import android.net.eap.EapSessionConfig.EapTtlsConfig; +import android.net.eap.EapSessionConfig.EapUiccConfig; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Objects; + +/** + * Provides utility methods to convert EapSessionConfig to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class EapSessionConfigUtils { + private static final String EAP_ID_KEY = "EAP_ID_KEY"; + private static final String EAP_SIM_CONFIG_KEY = "EAP_SIM_CONFIG_KEY"; + private static final String EAP_TTLS_CONFIG_KEY = "EAP_TTLS_CONFIG_KEY"; + private static final String EAP_AKA_CONFIG_KEY = "EAP_AKA_CONFIG_KEY"; + private static final String EAP_MSCHAP_V2_CONFIG_KEY = "EAP_MSCHAP_V2_CONFIG_KEY"; + private static final String EAP_AKA_PRIME_CONFIG_KEY = "EAP_AKA_PRIME_CONFIG_KEY"; + + /** Serializes an EapSessionConfig to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull EapSessionConfig config) { + final PersistableBundle result = new PersistableBundle(); + + result.putPersistableBundle( + EAP_ID_KEY, PersistableBundleUtils.fromByteArray(config.getEapIdentity())); + + if (config.getEapSimConfig() != null) { + result.putPersistableBundle( + EAP_SIM_CONFIG_KEY, + EapSimConfigUtils.toPersistableBundle(config.getEapSimConfig())); + } + + if (config.getEapTtlsConfig() != null) { + result.putPersistableBundle( + EAP_TTLS_CONFIG_KEY, + EapTtlsConfigUtils.toPersistableBundle(config.getEapTtlsConfig())); + } + + if (config.getEapAkaConfig() != null) { + result.putPersistableBundle( + EAP_AKA_CONFIG_KEY, + EapAkaConfigUtils.toPersistableBundle(config.getEapAkaConfig())); + } + + if (config.getEapMsChapV2Config() != null) { + result.putPersistableBundle( + EAP_MSCHAP_V2_CONFIG_KEY, + EapMsChapV2ConfigUtils.toPersistableBundle(config.getEapMsChapV2Config())); + } + + if (config.getEapAkaPrimeConfig() != null) { + result.putPersistableBundle( + EAP_AKA_PRIME_CONFIG_KEY, + EapAkaPrimeConfigUtils.toPersistableBundle(config.getEapAkaPrimeConfig())); + } + + return result; + } + + /** Constructs an EapSessionConfig by deserializing a PersistableBundle. */ + @NonNull + public static EapSessionConfig fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + final EapSessionConfig.Builder builder = new EapSessionConfig.Builder(); + + final PersistableBundle eapIdBundle = in.getPersistableBundle(EAP_ID_KEY); + Objects.requireNonNull(eapIdBundle, "EAP ID was null"); + builder.setEapIdentity(PersistableBundleUtils.toByteArray(eapIdBundle)); + + final PersistableBundle simBundle = in.getPersistableBundle(EAP_SIM_CONFIG_KEY); + if (simBundle != null) { + EapSimConfigUtils.setBuilderByReadingPersistableBundle(simBundle, builder); + } + + final PersistableBundle ttlsBundle = in.getPersistableBundle(EAP_TTLS_CONFIG_KEY); + if (ttlsBundle != null) { + EapTtlsConfigUtils.setBuilderByReadingPersistableBundle(ttlsBundle, builder); + } + + final PersistableBundle akaBundle = in.getPersistableBundle(EAP_AKA_CONFIG_KEY); + if (akaBundle != null) { + EapAkaConfigUtils.setBuilderByReadingPersistableBundle(akaBundle, builder); + } + + final PersistableBundle msChapV2Bundle = in.getPersistableBundle(EAP_MSCHAP_V2_CONFIG_KEY); + if (msChapV2Bundle != null) { + EapMsChapV2ConfigUtils.setBuilderByReadingPersistableBundle(msChapV2Bundle, builder); + } + + final PersistableBundle akaPrimeBundle = in.getPersistableBundle(EAP_AKA_PRIME_CONFIG_KEY); + if (akaPrimeBundle != null) { + EapAkaPrimeConfigUtils.setBuilderByReadingPersistableBundle(akaPrimeBundle, builder); + } + + return builder.build(); + } + + private static class EapMethodConfigUtils { + private static final String METHOD_TYPE = "METHOD_TYPE"; + + /** Serializes an EapMethodConfig to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull EapMethodConfig config) { + final PersistableBundle result = new PersistableBundle(); + result.putInt(METHOD_TYPE, config.getMethodType()); + return result; + } + } + + private static class EapUiccConfigUtils extends EapMethodConfigUtils { + static final String SUB_ID_KEY = "SUB_ID_KEY"; + static final String APP_TYPE_KEY = "APP_TYPE_KEY"; + + @NonNull + protected static PersistableBundle toPersistableBundle(@NonNull EapUiccConfig config) { + final PersistableBundle result = EapMethodConfigUtils.toPersistableBundle(config); + result.putInt(SUB_ID_KEY, config.getSubId()); + result.putInt(APP_TYPE_KEY, config.getAppType()); + + return result; + } + } + + private static final class EapSimConfigUtils extends EapUiccConfigUtils { + @NonNull + public static PersistableBundle toPersistableBundle(EapSimConfig config) { + return EapUiccConfigUtils.toPersistableBundle(config); + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle in, @NonNull EapSessionConfig.Builder builder) { + Objects.requireNonNull(in, "PersistableBundle was null"); + builder.setEapSimConfig(in.getInt(SUB_ID_KEY), in.getInt(APP_TYPE_KEY)); + } + } + + private static class EapAkaConfigUtils extends EapUiccConfigUtils { + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull EapAkaConfig config) { + return EapUiccConfigUtils.toPersistableBundle(config); + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle in, @NonNull EapSessionConfig.Builder builder) { + Objects.requireNonNull(in, "PersistableBundle was null"); + builder.setEapAkaConfig(in.getInt(SUB_ID_KEY), in.getInt(APP_TYPE_KEY)); + } + } + + private static final class EapAkaPrimeConfigUtils extends EapAkaConfigUtils { + private static final String NETWORK_NAME_KEY = "NETWORK_NAME_KEY"; + private static final String ALL_MISMATCHED_NETWORK_KEY = "ALL_MISMATCHED_NETWORK_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull EapAkaPrimeConfig config) { + final PersistableBundle result = EapUiccConfigUtils.toPersistableBundle(config); + result.putString(NETWORK_NAME_KEY, config.getNetworkName()); + result.putBoolean(ALL_MISMATCHED_NETWORK_KEY, config.allowsMismatchedNetworkNames()); + + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle in, @NonNull EapSessionConfig.Builder builder) { + Objects.requireNonNull(in, "PersistableBundle was null"); + builder.setEapAkaPrimeConfig( + in.getInt(SUB_ID_KEY), + in.getInt(APP_TYPE_KEY), + in.getString(NETWORK_NAME_KEY), + in.getBoolean(ALL_MISMATCHED_NETWORK_KEY)); + } + } + + private static final class EapMsChapV2ConfigUtils extends EapMethodConfigUtils { + private static final String USERNAME_KEY = "USERNAME_KEY"; + private static final String PASSWORD_KEY = "PASSWORD_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull EapMsChapV2Config config) { + final PersistableBundle result = EapMethodConfigUtils.toPersistableBundle(config); + result.putString(USERNAME_KEY, config.getUsername()); + result.putString(PASSWORD_KEY, config.getPassword()); + + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle in, @NonNull EapSessionConfig.Builder builder) { + Objects.requireNonNull(in, "PersistableBundle was null"); + builder.setEapMsChapV2Config(in.getString(USERNAME_KEY), in.getString(PASSWORD_KEY)); + } + } + + private static final class EapTtlsConfigUtils extends EapMethodConfigUtils { + private static final String TRUST_CERT_KEY = "TRUST_CERT_KEY"; + private static final String EAP_SESSION_CONFIG_KEY = "EAP_SESSION_CONFIG_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull EapTtlsConfig config) { + final PersistableBundle result = EapMethodConfigUtils.toPersistableBundle(config); + try { + if (config.getServerCaCert() != null) { + final PersistableBundle caBundle = + PersistableBundleUtils.fromByteArray( + config.getServerCaCert().getEncoded()); + result.putPersistableBundle(TRUST_CERT_KEY, caBundle); + } + } catch (CertificateEncodingException e) { + throw new IllegalStateException("Fail to encode the certificate"); + } + + result.putPersistableBundle( + EAP_SESSION_CONFIG_KEY, + EapSessionConfigUtils.toPersistableBundle(config.getInnerEapSessionConfig())); + + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle in, @NonNull EapSessionConfig.Builder builder) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + final PersistableBundle caBundle = in.getPersistableBundle(TRUST_CERT_KEY); + X509Certificate caCert = null; + if (caBundle != null) { + caCert = + CertUtils.certificateFromByteArray( + PersistableBundleUtils.toByteArray(caBundle)); + } + + final PersistableBundle eapSessionConfigBundle = + in.getPersistableBundle(EAP_SESSION_CONFIG_KEY); + Objects.requireNonNull(eapSessionConfigBundle, "Inner EAP Session Config was null"); + final EapSessionConfig eapSessionConfig = + EapSessionConfigUtils.fromPersistableBundle(eapSessionConfigBundle); + + builder.setEapTtlsConfig(caCert, eapSessionConfig); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeIdentificationUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeIdentificationUtils.java new file mode 100644 index 0000000000..6e8616fc9c --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeIdentificationUtils.java @@ -0,0 +1,143 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.net.InetAddresses; +import android.net.ipsec.ike.IkeDerAsn1DnIdentification; +import android.net.ipsec.ike.IkeFqdnIdentification; +import android.net.ipsec.ike.IkeIdentification; +import android.net.ipsec.ike.IkeIpv4AddrIdentification; +import android.net.ipsec.ike.IkeIpv6AddrIdentification; +import android.net.ipsec.ike.IkeKeyIdIdentification; +import android.net.ipsec.ike.IkeRfc822AddrIdentification; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.util.Objects; + +import javax.security.auth.x500.X500Principal; + +/** + * Abstract utility class to convert IkeIdentification to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class IkeIdentificationUtils { + private static final String ID_TYPE_KEY = "ID_TYPE_KEY"; + + private static final String DER_ASN1_DN_KEY = "DER_ASN1_DN_KEY"; + private static final String FQDN_KEY = "FQDN_KEY"; + private static final String KEY_ID_KEY = "KEY_ID_KEY"; + private static final String IP4_ADDRESS_KEY = "IP4_ADDRESS_KEY"; + private static final String IP6_ADDRESS_KEY = "IP6_ADDRESS_KEY"; + private static final String RFC822_ADDRESS_KEY = "RFC822_ADDRESS_KEY"; + + private static final int ID_TYPE_DER_ASN1_DN = 1; + private static final int ID_TYPE_FQDN = 2; + private static final int ID_TYPE_IPV4_ADDR = 3; + private static final int ID_TYPE_IPV6_ADDR = 4; + private static final int ID_TYPE_KEY_ID = 5; + private static final int ID_TYPE_RFC822_ADDR = 6; + + /** Serializes an IkeIdentification to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull IkeIdentification ikeId) { + if (ikeId instanceof IkeDerAsn1DnIdentification) { + final PersistableBundle result = createPersistableBundle(ID_TYPE_DER_ASN1_DN); + IkeDerAsn1DnIdentification id = (IkeDerAsn1DnIdentification) ikeId; + result.putPersistableBundle( + DER_ASN1_DN_KEY, + PersistableBundleUtils.fromByteArray(id.derAsn1Dn.getEncoded())); + return result; + } else if (ikeId instanceof IkeFqdnIdentification) { + final PersistableBundle result = createPersistableBundle(ID_TYPE_FQDN); + IkeFqdnIdentification id = (IkeFqdnIdentification) ikeId; + result.putString(FQDN_KEY, id.fqdn); + return result; + } else if (ikeId instanceof IkeIpv4AddrIdentification) { + final PersistableBundle result = createPersistableBundle(ID_TYPE_IPV4_ADDR); + IkeIpv4AddrIdentification id = (IkeIpv4AddrIdentification) ikeId; + result.putString(IP4_ADDRESS_KEY, id.ipv4Address.getHostAddress()); + return result; + } else if (ikeId instanceof IkeIpv6AddrIdentification) { + final PersistableBundle result = createPersistableBundle(ID_TYPE_IPV6_ADDR); + IkeIpv6AddrIdentification id = (IkeIpv6AddrIdentification) ikeId; + result.putString(IP6_ADDRESS_KEY, id.ipv6Address.getHostAddress()); + return result; + } else if (ikeId instanceof IkeKeyIdIdentification) { + final PersistableBundle result = createPersistableBundle(ID_TYPE_KEY_ID); + IkeKeyIdIdentification id = (IkeKeyIdIdentification) ikeId; + result.putPersistableBundle(KEY_ID_KEY, PersistableBundleUtils.fromByteArray(id.keyId)); + return result; + } else if (ikeId instanceof IkeRfc822AddrIdentification) { + final PersistableBundle result = createPersistableBundle(ID_TYPE_RFC822_ADDR); + IkeRfc822AddrIdentification id = (IkeRfc822AddrIdentification) ikeId; + result.putString(RFC822_ADDRESS_KEY, id.rfc822Name); + return result; + } else { + throw new IllegalStateException("Unrecognized IkeIdentification subclass"); + } + } + + private static PersistableBundle createPersistableBundle(int idType) { + final PersistableBundle result = new PersistableBundle(); + result.putInt(ID_TYPE_KEY, idType); + return result; + } + + /** Constructs an IkeIdentification by deserializing a PersistableBundle. */ + @NonNull + public static IkeIdentification fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + int idType = in.getInt(ID_TYPE_KEY); + switch (idType) { + case ID_TYPE_DER_ASN1_DN: + final PersistableBundle dnBundle = in.getPersistableBundle(DER_ASN1_DN_KEY); + Objects.requireNonNull(dnBundle, "ASN1 DN was null"); + return new IkeDerAsn1DnIdentification( + new X500Principal(PersistableBundleUtils.toByteArray(dnBundle))); + case ID_TYPE_FQDN: + return new IkeFqdnIdentification(in.getString(FQDN_KEY)); + case ID_TYPE_IPV4_ADDR: + final String v4AddressStr = in.getString(IP4_ADDRESS_KEY); + Objects.requireNonNull(v4AddressStr, "IPv4 address was null"); + return new IkeIpv4AddrIdentification( + (Inet4Address) InetAddresses.parseNumericAddress(v4AddressStr)); + case ID_TYPE_IPV6_ADDR: + final String v6AddressStr = in.getString(IP6_ADDRESS_KEY); + Objects.requireNonNull(v6AddressStr, "IPv6 address was null"); + return new IkeIpv6AddrIdentification( + (Inet6Address) InetAddresses.parseNumericAddress(v6AddressStr)); + case ID_TYPE_KEY_ID: + final PersistableBundle keyIdBundle = in.getPersistableBundle(KEY_ID_KEY); + Objects.requireNonNull(in, "Key ID was null"); + return new IkeKeyIdIdentification(PersistableBundleUtils.toByteArray(keyIdBundle)); + case ID_TYPE_RFC822_ADDR: + return new IkeRfc822AddrIdentification(in.getString(RFC822_ADDRESS_KEY)); + default: + throw new IllegalStateException("Unrecognized IKE ID type: " + idType); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeSaProposalUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeSaProposalUtils.java new file mode 100644 index 0000000000..b590148de5 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeSaProposalUtils.java @@ -0,0 +1,87 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.net.ipsec.ike.IkeSaProposal; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.List; +import java.util.Objects; + +/** + * Provides utility methods to convert IkeSaProposal to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class IkeSaProposalUtils extends SaProposalUtilsBase { + private static final String PRF_KEY = "PRF_KEY"; + + /** Serializes an IkeSaProposal to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(IkeSaProposal proposal) { + final PersistableBundle result = SaProposalUtilsBase.toPersistableBundle(proposal); + + final int[] prfArray = + proposal.getPseudorandomFunctions().stream().mapToInt(i -> i).toArray(); + result.putIntArray(PRF_KEY, prfArray); + + return result; + } + + /** Constructs an IkeSaProposal by deserializing a PersistableBundle. */ + @NonNull + public static IkeSaProposal fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + final IkeSaProposal.Builder builder = new IkeSaProposal.Builder(); + + final PersistableBundle encryptionBundle = in.getPersistableBundle(ENCRYPT_ALGO_KEY); + Objects.requireNonNull(encryptionBundle, "Encryption algo bundle was null"); + final List encryptList = + PersistableBundleUtils.toList(encryptionBundle, EncryptionAlgoKeyLenPair::new); + for (EncryptionAlgoKeyLenPair t : encryptList) { + builder.addEncryptionAlgorithm(t.encryptionAlgo, t.keyLen); + } + + final int[] integrityAlgoIdArray = in.getIntArray(INTEGRITY_ALGO_KEY); + Objects.requireNonNull(integrityAlgoIdArray, "Integrity algo array was null"); + for (int algo : integrityAlgoIdArray) { + builder.addIntegrityAlgorithm(algo); + } + + final int[] dhGroupArray = in.getIntArray(DH_GROUP_KEY); + Objects.requireNonNull(dhGroupArray, "DH Group array was null"); + for (int dh : dhGroupArray) { + builder.addDhGroup(dh); + } + + final int[] prfArray = in.getIntArray(PRF_KEY); + Objects.requireNonNull(prfArray, "PRF array was null"); + for (int prf : prfArray) { + builder.addPseudorandomFunction(prf); + } + + return builder.build(); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java new file mode 100644 index 0000000000..aefac2e89a --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java @@ -0,0 +1,568 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.InetAddresses; +import android.net.eap.EapSessionConfig; +import android.net.ipsec.ike.IkeSaProposal; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeSessionParams.ConfigRequestIpv4PcscfServer; +import android.net.ipsec.ike.IkeSessionParams.ConfigRequestIpv6PcscfServer; +import android.net.ipsec.ike.IkeSessionParams.IkeAuthConfig; +import android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignLocalConfig; +import android.net.ipsec.ike.IkeSessionParams.IkeAuthDigitalSignRemoteConfig; +import android.net.ipsec.ike.IkeSessionParams.IkeAuthEapConfig; +import android.net.ipsec.ike.IkeSessionParams.IkeAuthPskConfig; +import android.net.ipsec.ike.IkeSessionParams.IkeConfigRequest; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.net.InetAddress; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Abstract utility class to convert IkeSessionParams to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class IkeSessionParamsUtils { + private static final String TAG = IkeSessionParamsUtils.class.getSimpleName(); + + private static final String SERVER_HOST_NAME_KEY = "SERVER_HOST_NAME_KEY"; + private static final String SA_PROPOSALS_KEY = "SA_PROPOSALS_KEY"; + private static final String LOCAL_ID_KEY = "LOCAL_ID_KEY"; + private static final String REMOTE_ID_KEY = "REMOTE_ID_KEY"; + private static final String LOCAL_AUTH_KEY = "LOCAL_AUTH_KEY"; + private static final String REMOTE_AUTH_KEY = "REMOTE_AUTH_KEY"; + private static final String CONFIG_REQUESTS_KEY = "CONFIG_REQUESTS_KEY"; + private static final String RETRANS_TIMEOUTS_KEY = "RETRANS_TIMEOUTS_KEY"; + private static final String HARD_LIFETIME_SEC_KEY = "HARD_LIFETIME_SEC_KEY"; + private static final String SOFT_LIFETIME_SEC_KEY = "SOFT_LIFETIME_SEC_KEY"; + private static final String DPD_DELAY_SEC_KEY = "DPD_DELAY_SEC_KEY"; + private static final String NATT_KEEPALIVE_DELAY_SEC_KEY = "NATT_KEEPALIVE_DELAY_SEC_KEY"; + private static final String IKE_OPTIONS_KEY = "IKE_OPTIONS_KEY"; + private static final String IP_VERSION_KEY = "IP_VERSION_KEY"; + private static final String ENCAP_TYPE_KEY = "ENCAP_TYPE_KEY"; + // TODO: add DSCP_KEY and IS_IKE_FRAGMENT_SUPPORTED_KEY. + + // TODO: b/243181760 Use the IKE API when they are exposed + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static final int IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION = 6; + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static final int IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES = 7; + + private static final Set IKE_OPTIONS = new ArraySet<>(); + + static { + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_ACCEPT_ANY_REMOTE_ID); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_EAP_ONLY_AUTH); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_MOBIKE); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_FORCE_PORT_4500); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_INITIAL_CONTACT); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_REKEY_MOBILITY); + IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_ADDRESS_FAMILY_SELECTION); + IKE_OPTIONS.add(IKE_OPTION_AUTOMATIC_NATT_KEEPALIVES); + IKE_OPTIONS.add(IkeSessionParams.IKE_OPTION_AUTOMATIC_KEEPALIVE_ON_OFF); + } + + /** + * Check if an IKE option is supported in the IPsec module installed on the device + * + *

This method ensures caller to safely access options that are added between dessert + * releases. + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static boolean isIkeOptionValid(int option) { + try { + new IkeSessionParams.Builder().addIkeOption(option); + return true; + } catch (IllegalArgumentException e) { + Log.d(TAG, "Option not supported; discarding: " + option); + return false; + } + } + + /** Serializes an IkeSessionParams to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull IkeSessionParams params) { + if (params.getNetwork() != null || params.getIke3gppExtension() != null) { + throw new IllegalStateException( + "Cannot convert a IkeSessionParams with a caller configured network or with" + + " 3GPP extension enabled"); + } + + final PersistableBundle result = new PersistableBundle(); + + result.putString(SERVER_HOST_NAME_KEY, params.getServerHostname()); + + final PersistableBundle saProposalBundle = + PersistableBundleUtils.fromList( + params.getSaProposals(), IkeSaProposalUtils::toPersistableBundle); + result.putPersistableBundle(SA_PROPOSALS_KEY, saProposalBundle); + + result.putPersistableBundle( + LOCAL_ID_KEY, + IkeIdentificationUtils.toPersistableBundle(params.getLocalIdentification())); + result.putPersistableBundle( + REMOTE_ID_KEY, + IkeIdentificationUtils.toPersistableBundle(params.getRemoteIdentification())); + + result.putPersistableBundle( + LOCAL_AUTH_KEY, AuthConfigUtils.toPersistableBundle(params.getLocalAuthConfig())); + result.putPersistableBundle( + REMOTE_AUTH_KEY, AuthConfigUtils.toPersistableBundle(params.getRemoteAuthConfig())); + + final List reqList = new ArrayList<>(); + for (IkeConfigRequest req : params.getConfigurationRequests()) { + reqList.add(new ConfigRequest(req)); + } + final PersistableBundle configReqListBundle = + PersistableBundleUtils.fromList(reqList, ConfigRequest::toPersistableBundle); + result.putPersistableBundle(CONFIG_REQUESTS_KEY, configReqListBundle); + + result.putIntArray(RETRANS_TIMEOUTS_KEY, params.getRetransmissionTimeoutsMillis()); + result.putInt(HARD_LIFETIME_SEC_KEY, params.getHardLifetimeSeconds()); + result.putInt(SOFT_LIFETIME_SEC_KEY, params.getSoftLifetimeSeconds()); + result.putInt(DPD_DELAY_SEC_KEY, params.getDpdDelaySeconds()); + result.putInt(NATT_KEEPALIVE_DELAY_SEC_KEY, params.getNattKeepAliveDelaySeconds()); + result.putInt(IP_VERSION_KEY, params.getIpVersion()); + result.putInt(ENCAP_TYPE_KEY, params.getEncapType()); + + final List enabledIkeOptions = new ArrayList<>(); + + try { + // TODO: b/328844044: Ideally this code should gate the behavior by checking the + // com.android.ipsec.flags.enabled_ike_options_api flag but that flag is not accessible + // right now. We should either update the code when the flag is accessible or remove the + // legacy behavior after VIC SDK finalization + enabledIkeOptions.addAll(params.getIkeOptions()); + } catch (Exception e) { + // getIkeOptions throws. It means the API is not available + enabledIkeOptions.clear(); + for (int option : IKE_OPTIONS) { + if (isIkeOptionValid(option) && params.hasIkeOption(option)) { + enabledIkeOptions.add(option); + } + } + } + + final int[] optionArray = enabledIkeOptions.stream().mapToInt(i -> i).toArray(); + result.putIntArray(IKE_OPTIONS_KEY, optionArray); + + return result; + } + + /** Constructs an IkeSessionParams by deserializing a PersistableBundle. */ + @NonNull + public static IkeSessionParams fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle is null"); + + final IkeSessionParams.Builder builder = new IkeSessionParams.Builder(); + + builder.setServerHostname(in.getString(SERVER_HOST_NAME_KEY)); + + PersistableBundle proposalBundle = in.getPersistableBundle(SA_PROPOSALS_KEY); + Objects.requireNonNull(in, "SA Proposals was null"); + List saProposals = + PersistableBundleUtils.toList( + proposalBundle, IkeSaProposalUtils::fromPersistableBundle); + for (IkeSaProposal proposal : saProposals) { + builder.addSaProposal(proposal); + } + + builder.setLocalIdentification( + IkeIdentificationUtils.fromPersistableBundle( + in.getPersistableBundle(LOCAL_ID_KEY))); + builder.setRemoteIdentification( + IkeIdentificationUtils.fromPersistableBundle( + in.getPersistableBundle(REMOTE_ID_KEY))); + + AuthConfigUtils.setBuilderByReadingPersistableBundle( + in.getPersistableBundle(LOCAL_AUTH_KEY), + in.getPersistableBundle(REMOTE_AUTH_KEY), + builder); + + builder.setRetransmissionTimeoutsMillis(in.getIntArray(RETRANS_TIMEOUTS_KEY)); + builder.setLifetimeSeconds( + in.getInt(HARD_LIFETIME_SEC_KEY), in.getInt(SOFT_LIFETIME_SEC_KEY)); + builder.setDpdDelaySeconds(in.getInt(DPD_DELAY_SEC_KEY)); + builder.setNattKeepAliveDelaySeconds(in.getInt(NATT_KEEPALIVE_DELAY_SEC_KEY)); + builder.setIpVersion(in.getInt(IP_VERSION_KEY)); + builder.setEncapType(in.getInt(ENCAP_TYPE_KEY)); + + final PersistableBundle configReqListBundle = in.getPersistableBundle(CONFIG_REQUESTS_KEY); + Objects.requireNonNull(configReqListBundle, "Config request list was null"); + final List reqList = + PersistableBundleUtils.toList(configReqListBundle, ConfigRequest::new); + for (ConfigRequest req : reqList) { + switch (req.type) { + case ConfigRequest.IPV4_P_CSCF_ADDRESS: + if (req.address == null) { + builder.addPcscfServerRequest(AF_INET); + } else { + builder.addPcscfServerRequest(req.address); + } + break; + case ConfigRequest.IPV6_P_CSCF_ADDRESS: + if (req.address == null) { + builder.addPcscfServerRequest(AF_INET6); + } else { + builder.addPcscfServerRequest(req.address); + } + break; + default: + throw new IllegalArgumentException( + "Unrecognized config request type: " + req.type); + } + } + + // Clear IKE Options that are by default enabled + for (int option : IKE_OPTIONS) { + if (isIkeOptionValid(option)) { + builder.removeIkeOption(option); + } + } + + final int[] optionArray = in.getIntArray(IKE_OPTIONS_KEY); + for (int option : optionArray) { + if (isIkeOptionValid(option)) { + builder.addIkeOption(option); + } + } + + return builder.build(); + } + + private static final class AuthConfigUtils { + private static final int IKE_AUTH_METHOD_PSK = 1; + private static final int IKE_AUTH_METHOD_PUB_KEY_SIGNATURE = 2; + private static final int IKE_AUTH_METHOD_EAP = 3; + + private static final String AUTH_METHOD_KEY = "AUTH_METHOD_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull IkeAuthConfig authConfig) { + if (authConfig instanceof IkeAuthPskConfig) { + IkeAuthPskConfig config = (IkeAuthPskConfig) authConfig; + return IkeAuthPskConfigUtils.toPersistableBundle( + config, createPersistableBundle(IKE_AUTH_METHOD_PSK)); + } else if (authConfig instanceof IkeAuthDigitalSignLocalConfig) { + IkeAuthDigitalSignLocalConfig config = (IkeAuthDigitalSignLocalConfig) authConfig; + return IkeAuthDigitalSignConfigUtils.toPersistableBundle( + config, createPersistableBundle(IKE_AUTH_METHOD_PUB_KEY_SIGNATURE)); + } else if (authConfig instanceof IkeAuthDigitalSignRemoteConfig) { + IkeAuthDigitalSignRemoteConfig config = (IkeAuthDigitalSignRemoteConfig) authConfig; + return IkeAuthDigitalSignConfigUtils.toPersistableBundle( + config, createPersistableBundle(IKE_AUTH_METHOD_PUB_KEY_SIGNATURE)); + } else if (authConfig instanceof IkeAuthEapConfig) { + IkeAuthEapConfig config = (IkeAuthEapConfig) authConfig; + return IkeAuthEapConfigUtils.toPersistableBundle( + config, createPersistableBundle(IKE_AUTH_METHOD_EAP)); + } else { + throw new IllegalStateException("Invalid IkeAuthConfig subclass"); + } + } + + private static PersistableBundle createPersistableBundle(int type) { + final PersistableBundle result = new PersistableBundle(); + result.putInt(AUTH_METHOD_KEY, type); + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle localAuthBundle, + @NonNull PersistableBundle remoteAuthBundle, + @NonNull IkeSessionParams.Builder builder) { + Objects.requireNonNull(localAuthBundle, "localAuthBundle was null"); + Objects.requireNonNull(remoteAuthBundle, "remoteAuthBundle was null"); + + final int localMethodType = localAuthBundle.getInt(AUTH_METHOD_KEY); + final int remoteMethodType = remoteAuthBundle.getInt(AUTH_METHOD_KEY); + switch (localMethodType) { + case IKE_AUTH_METHOD_PSK: + if (remoteMethodType != IKE_AUTH_METHOD_PSK) { + throw new IllegalArgumentException( + "Expect remote auth method to be PSK based, but was " + + remoteMethodType); + } + IkeAuthPskConfigUtils.setBuilderByReadingPersistableBundle( + localAuthBundle, remoteAuthBundle, builder); + return; + case IKE_AUTH_METHOD_PUB_KEY_SIGNATURE: + if (remoteMethodType != IKE_AUTH_METHOD_PUB_KEY_SIGNATURE) { + throw new IllegalArgumentException( + "Expect remote auth method to be digital signature based, but was " + + remoteMethodType); + } + IkeAuthDigitalSignConfigUtils.setBuilderByReadingPersistableBundle( + localAuthBundle, remoteAuthBundle, builder); + return; + case IKE_AUTH_METHOD_EAP: + if (remoteMethodType != IKE_AUTH_METHOD_PUB_KEY_SIGNATURE) { + throw new IllegalArgumentException( + "When using EAP for local authentication, expect remote auth" + + " method to be digital signature based, but was " + + remoteMethodType); + } + IkeAuthEapConfigUtils.setBuilderByReadingPersistableBundle( + localAuthBundle, remoteAuthBundle, builder); + return; + default: + throw new IllegalArgumentException( + "Invalid EAP method type " + localMethodType); + } + } + } + + private static final class IkeAuthPskConfigUtils { + private static final String PSK_KEY = "PSK_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle( + @NonNull IkeAuthPskConfig config, @NonNull PersistableBundle result) { + result.putPersistableBundle( + PSK_KEY, PersistableBundleUtils.fromByteArray(config.getPsk())); + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle localAuthBundle, + @NonNull PersistableBundle remoteAuthBundle, + @NonNull IkeSessionParams.Builder builder) { + Objects.requireNonNull(localAuthBundle, "localAuthBundle was null"); + Objects.requireNonNull(remoteAuthBundle, "remoteAuthBundle was null"); + + final PersistableBundle localPskBundle = localAuthBundle.getPersistableBundle(PSK_KEY); + final PersistableBundle remotePskBundle = + remoteAuthBundle.getPersistableBundle(PSK_KEY); + Objects.requireNonNull(localAuthBundle, "Local PSK was null"); + Objects.requireNonNull(remoteAuthBundle, "Remote PSK was null"); + + final byte[] localPsk = PersistableBundleUtils.toByteArray(localPskBundle); + final byte[] remotePsk = PersistableBundleUtils.toByteArray(remotePskBundle); + if (!Arrays.equals(localPsk, remotePsk)) { + throw new IllegalArgumentException("Local PSK and remote PSK are different"); + } + builder.setAuthPsk(localPsk); + } + } + + private static class IkeAuthDigitalSignConfigUtils { + private static final String END_CERT_KEY = "END_CERT_KEY"; + private static final String INTERMEDIATE_CERTS_KEY = "INTERMEDIATE_CERTS_KEY"; + private static final String PRIVATE_KEY_KEY = "PRIVATE_KEY_KEY"; + private static final String TRUST_CERT_KEY = "TRUST_CERT_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle( + @NonNull IkeAuthDigitalSignLocalConfig config, @NonNull PersistableBundle result) { + try { + result.putPersistableBundle( + END_CERT_KEY, + PersistableBundleUtils.fromByteArray( + config.getClientEndCertificate().getEncoded())); + + final List certList = config.getIntermediateCertificates(); + final List encodedCertList = new ArrayList<>(certList.size()); + for (X509Certificate cert : certList) { + encodedCertList.add(cert.getEncoded()); + } + + final PersistableBundle certsBundle = + PersistableBundleUtils.fromList( + encodedCertList, PersistableBundleUtils::fromByteArray); + result.putPersistableBundle(INTERMEDIATE_CERTS_KEY, certsBundle); + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Fail to encode certificate"); + } + + // TODO: b/170670506 Consider putting PrivateKey in Android KeyStore + result.putPersistableBundle( + PRIVATE_KEY_KEY, + PersistableBundleUtils.fromByteArray(config.getPrivateKey().getEncoded())); + return result; + } + + @NonNull + public static PersistableBundle toPersistableBundle( + @NonNull IkeAuthDigitalSignRemoteConfig config, @NonNull PersistableBundle result) { + try { + X509Certificate caCert = config.getRemoteCaCert(); + if (caCert != null) { + result.putPersistableBundle( + TRUST_CERT_KEY, + PersistableBundleUtils.fromByteArray(caCert.getEncoded())); + } + } catch (CertificateEncodingException e) { + throw new IllegalArgumentException("Fail to encode the certificate"); + } + + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle localAuthBundle, + @NonNull PersistableBundle remoteAuthBundle, + @NonNull IkeSessionParams.Builder builder) { + Objects.requireNonNull(localAuthBundle, "localAuthBundle was null"); + Objects.requireNonNull(remoteAuthBundle, "remoteAuthBundle was null"); + + // Deserialize localAuth + final PersistableBundle endCertBundle = + localAuthBundle.getPersistableBundle(END_CERT_KEY); + Objects.requireNonNull(endCertBundle, "End cert was null"); + final byte[] encodedCert = PersistableBundleUtils.toByteArray(endCertBundle); + final X509Certificate endCert = CertUtils.certificateFromByteArray(encodedCert); + + final PersistableBundle certsBundle = + localAuthBundle.getPersistableBundle(INTERMEDIATE_CERTS_KEY); + Objects.requireNonNull(certsBundle, "Intermediate certs was null"); + final List encodedCertList = + PersistableBundleUtils.toList(certsBundle, PersistableBundleUtils::toByteArray); + final List certList = new ArrayList<>(encodedCertList.size()); + for (byte[] encoded : encodedCertList) { + certList.add(CertUtils.certificateFromByteArray(encoded)); + } + + final PersistableBundle privateKeyBundle = + localAuthBundle.getPersistableBundle(PRIVATE_KEY_KEY); + Objects.requireNonNull(privateKeyBundle, "PrivateKey bundle was null"); + final PrivateKey privateKey = + CertUtils.privateKeyFromByteArray( + PersistableBundleUtils.toByteArray(privateKeyBundle)); + + // Deserialize remoteAuth + final PersistableBundle trustCertBundle = + remoteAuthBundle.getPersistableBundle(TRUST_CERT_KEY); + + X509Certificate caCert = null; + if (trustCertBundle != null) { + final byte[] encodedCaCert = PersistableBundleUtils.toByteArray(trustCertBundle); + caCert = CertUtils.certificateFromByteArray(encodedCaCert); + } + + builder.setAuthDigitalSignature(caCert, endCert, certList, privateKey); + } + } + + private static final class IkeAuthEapConfigUtils { + private static final String EAP_CONFIG_KEY = "EAP_CONFIG_KEY"; + + @NonNull + public static PersistableBundle toPersistableBundle( + @NonNull IkeAuthEapConfig config, @NonNull PersistableBundle result) { + result.putPersistableBundle( + EAP_CONFIG_KEY, + EapSessionConfigUtils.toPersistableBundle(config.getEapConfig())); + return result; + } + + public static void setBuilderByReadingPersistableBundle( + @NonNull PersistableBundle localAuthBundle, + @NonNull PersistableBundle remoteAuthBundle, + @NonNull IkeSessionParams.Builder builder) { + // Deserialize localAuth + final PersistableBundle eapBundle = + localAuthBundle.getPersistableBundle(EAP_CONFIG_KEY); + Objects.requireNonNull(eapBundle, "EAP Config was null"); + final EapSessionConfig eapConfig = + EapSessionConfigUtils.fromPersistableBundle(eapBundle); + + // Deserialize remoteAuth + final PersistableBundle trustCertBundle = + remoteAuthBundle.getPersistableBundle( + IkeAuthDigitalSignConfigUtils.TRUST_CERT_KEY); + + X509Certificate serverCaCert = null; + if (trustCertBundle != null) { + final byte[] encodedCaCert = PersistableBundleUtils.toByteArray(trustCertBundle); + serverCaCert = CertUtils.certificateFromByteArray(encodedCaCert); + } + builder.setAuthEap(serverCaCert, eapConfig); + } + } + + private static final class ConfigRequest { + private static final int IPV4_P_CSCF_ADDRESS = 1; + private static final int IPV6_P_CSCF_ADDRESS = 2; + + private static final String TYPE_KEY = "type"; + private static final String ADDRESS_KEY = "address"; + + public final int type; + + // Null when it is an empty request + @Nullable public final InetAddress address; + + ConfigRequest(IkeConfigRequest config) { + if (config instanceof ConfigRequestIpv4PcscfServer) { + type = IPV4_P_CSCF_ADDRESS; + address = ((ConfigRequestIpv4PcscfServer) config).getAddress(); + } else if (config instanceof ConfigRequestIpv6PcscfServer) { + type = IPV6_P_CSCF_ADDRESS; + address = ((ConfigRequestIpv6PcscfServer) config).getAddress(); + } else { + throw new IllegalStateException("Unknown TunnelModeChildConfigRequest"); + } + } + + ConfigRequest(PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + type = in.getInt(TYPE_KEY); + + String addressStr = in.getString(ADDRESS_KEY); + if (addressStr == null) { + address = null; + } else { + address = InetAddresses.parseNumericAddress(addressStr); + } + } + + @NonNull + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(TYPE_KEY, type); + if (address != null) { + result.putString(ADDRESS_KEY, address.getHostAddress()); + } + + return result; + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtils.java new file mode 100644 index 0000000000..6bbc6b1e82 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/IkeTrafficSelectorUtils.java @@ -0,0 +1,74 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.net.InetAddresses; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.os.PersistableBundle; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; + +/** + * Provides utility methods to convert IkeTrafficSelector to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class IkeTrafficSelectorUtils { + private static final String START_PORT_KEY = "START_PORT_KEY"; + private static final String END_PORT_KEY = "END_PORT_KEY"; + private static final String START_ADDRESS_KEY = "START_ADDRESS_KEY"; + private static final String END_ADDRESS_KEY = "END_ADDRESS_KEY"; + + /** Constructs an IkeTrafficSelector by deserializing a PersistableBundle. */ + @NonNull + public static IkeTrafficSelector fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + final int startPort = in.getInt(START_PORT_KEY); + final int endPort = in.getInt(END_PORT_KEY); + + final String startingAddress = in.getString(START_ADDRESS_KEY); + final String endingAddress = in.getString(END_ADDRESS_KEY); + Objects.requireNonNull(startingAddress, "startAddress was null"); + Objects.requireNonNull(startingAddress, "endAddress was null"); + + return new IkeTrafficSelector( + startPort, + endPort, + InetAddresses.parseNumericAddress(startingAddress), + InetAddresses.parseNumericAddress(endingAddress)); + } + + /** Serializes an IkeTrafficSelector to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull IkeTrafficSelector ts) { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(START_PORT_KEY, ts.startPort); + result.putInt(END_PORT_KEY, ts.endPort); + result.putString(START_ADDRESS_KEY, ts.startingAddress.getHostAddress()); + result.putString(END_ADDRESS_KEY, ts.endingAddress.getHostAddress()); + + return result; + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/SaProposalUtilsBase.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/SaProposalUtilsBase.java new file mode 100644 index 0000000000..469966a484 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/SaProposalUtilsBase.java @@ -0,0 +1,95 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import android.annotation.NonNull; +import android.net.ipsec.ike.SaProposal; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Abstract utility class to convert SaProposal to/from PersistableBundle. + * + * @hide + */ +abstract class SaProposalUtilsBase { + static final String ENCRYPT_ALGO_KEY = "ENCRYPT_ALGO_KEY"; + static final String INTEGRITY_ALGO_KEY = "INTEGRITY_ALGO_KEY"; + static final String DH_GROUP_KEY = "DH_GROUP_KEY"; + + static class EncryptionAlgoKeyLenPair { + private static final String ALGO_KEY = "ALGO_KEY"; + private static final String KEY_LEN_KEY = "KEY_LEN_KEY"; + + public final int encryptionAlgo; + public final int keyLen; + + EncryptionAlgoKeyLenPair(int encryptionAlgo, int keyLen) { + this.encryptionAlgo = encryptionAlgo; + this.keyLen = keyLen; + } + + EncryptionAlgoKeyLenPair(PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + this.encryptionAlgo = in.getInt(ALGO_KEY); + this.keyLen = in.getInt(KEY_LEN_KEY); + } + + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(ALGO_KEY, encryptionAlgo); + result.putInt(KEY_LEN_KEY, keyLen); + + return result; + } + } + + /** + * Serializes common info of a SaProposal to a PersistableBundle. + * + * @hide + */ + @NonNull + static PersistableBundle toPersistableBundle(SaProposal proposal) { + final PersistableBundle result = new PersistableBundle(); + + final List encryptAlgoKeyLenPairs = new ArrayList<>(); + for (Pair pair : proposal.getEncryptionAlgorithms()) { + encryptAlgoKeyLenPairs.add(new EncryptionAlgoKeyLenPair(pair.first, pair.second)); + } + final PersistableBundle encryptionBundle = + PersistableBundleUtils.fromList( + encryptAlgoKeyLenPairs, EncryptionAlgoKeyLenPair::toPersistableBundle); + result.putPersistableBundle(ENCRYPT_ALGO_KEY, encryptionBundle); + + final int[] integrityAlgoIdArray = + proposal.getIntegrityAlgorithms().stream().mapToInt(i -> i).toArray(); + result.putIntArray(INTEGRITY_ALGO_KEY, integrityAlgoIdArray); + + final int[] dhGroupArray = proposal.getDhGroups().stream().mapToInt(i -> i).toArray(); + result.putIntArray(DH_GROUP_KEY, dhGroupArray); + + return result; + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java new file mode 100644 index 0000000000..0427742f9c --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/TunnelConnectionParamsUtils.java @@ -0,0 +1,103 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import android.annotation.NonNull; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTunnelConnectionParams; +import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.os.PersistableBundle; + +import java.util.Objects; + +/** + * Utility class to convert Tunnel Connection Params to/from PersistableBundle + * + * @hide + */ +public final class TunnelConnectionParamsUtils { + private static final int EXPECTED_BUNDLE_KEY_CNT = 1; + + private static final String PARAMS_TYPE_IKE = "IKE"; + + /** Serializes an IkeTunnelConnectionParams to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle(@NonNull IkeTunnelConnectionParams params) { + final PersistableBundle result = new PersistableBundle(); + + result.putPersistableBundle( + PARAMS_TYPE_IKE, + IkeTunnelConnectionParamsUtils.serializeIkeParams( + (IkeTunnelConnectionParams) params)); + return result; + } + + /** Constructs an IkeTunnelConnectionParams by deserializing a PersistableBundle. */ + @NonNull + public static IkeTunnelConnectionParams fromPersistableBundle(@NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + if (in.keySet().size() != EXPECTED_BUNDLE_KEY_CNT) { + throw new IllegalArgumentException( + String.format( + "Expect PersistableBundle to have %d element but found: %s", + EXPECTED_BUNDLE_KEY_CNT, in.keySet())); + } + + if (in.get(PARAMS_TYPE_IKE) != null) { + return IkeTunnelConnectionParamsUtils.deserializeIkeParams( + in.getPersistableBundle(PARAMS_TYPE_IKE)); + } + + throw new IllegalArgumentException( + "Invalid Tunnel Connection Params type " + in.keySet().iterator().next()); + } + + private static final class IkeTunnelConnectionParamsUtils { + private static final String IKE_PARAMS_KEY = "IKE_PARAMS_KEY"; + private static final String CHILD_PARAMS_KEY = "CHILD_PARAMS_KEY"; + + @NonNull + public static PersistableBundle serializeIkeParams( + @NonNull IkeTunnelConnectionParams ikeParams) { + final PersistableBundle result = new PersistableBundle(); + + result.putPersistableBundle( + IKE_PARAMS_KEY, + IkeSessionParamsUtils.toPersistableBundle(ikeParams.getIkeSessionParams())); + result.putPersistableBundle( + CHILD_PARAMS_KEY, + TunnelModeChildSessionParamsUtils.toPersistableBundle( + ikeParams.getTunnelModeChildSessionParams())); + return result; + } + + @NonNull + public static IkeTunnelConnectionParams deserializeIkeParams( + @NonNull PersistableBundle in) { + final PersistableBundle ikeBundle = in.getPersistableBundle(IKE_PARAMS_KEY); + final PersistableBundle childBundle = in.getPersistableBundle(CHILD_PARAMS_KEY); + Objects.requireNonNull(ikeBundle, "IkeSessionParams was null"); + Objects.requireNonNull(ikeBundle, "TunnelModeChildSessionParams was null"); + + final IkeSessionParams ikeParams = + IkeSessionParamsUtils.fromPersistableBundle(ikeBundle); + final TunnelModeChildSessionParams childParams = + TunnelModeChildSessionParamsUtils.fromPersistableBundle(childBundle); + return new IkeTunnelConnectionParams(ikeParams, childParams); + } + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtils.java b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtils.java new file mode 100644 index 0000000000..3f4ba345a1 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/persistablebundleutils/TunnelModeChildSessionParamsUtils.java @@ -0,0 +1,285 @@ +/* + * 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 android.net.vcn.persistablebundleutils; + +import static android.system.OsConstants.AF_INET; +import static android.system.OsConstants.AF_INET6; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.InetAddresses; +import android.net.ipsec.ike.ChildSaProposal; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.TunnelModeChildSessionParams; +import android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequestIpv4Address; +import android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequestIpv4DhcpServer; +import android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequestIpv4DnsServer; +import android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequestIpv4Netmask; +import android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequestIpv6Address; +import android.net.ipsec.ike.TunnelModeChildSessionParams.ConfigRequestIpv6DnsServer; +import android.net.ipsec.ike.TunnelModeChildSessionParams.TunnelModeChildConfigRequest; +import android.net.vcn.util.PersistableBundleUtils; +import android.os.PersistableBundle; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Provides utility methods to convert TunnelModeChildSessionParams to/from PersistableBundle. + * + * @hide + */ +@VisibleForTesting(visibility = Visibility.PRIVATE) +public final class TunnelModeChildSessionParamsUtils { + private static final String TAG = TunnelModeChildSessionParamsUtils.class.getSimpleName(); + + private static final String INBOUND_TS_KEY = "INBOUND_TS_KEY"; + private static final String OUTBOUND_TS_KEY = "OUTBOUND_TS_KEY"; + private static final String SA_PROPOSALS_KEY = "SA_PROPOSALS_KEY"; + private static final String HARD_LIFETIME_SEC_KEY = "HARD_LIFETIME_SEC_KEY"; + private static final String SOFT_LIFETIME_SEC_KEY = "SOFT_LIFETIME_SEC_KEY"; + private static final String CONFIG_REQUESTS_KEY = "CONFIG_REQUESTS_KEY"; + + private static class ConfigRequest { + private static final int TYPE_IPV4_ADDRESS = 1; + private static final int TYPE_IPV6_ADDRESS = 2; + private static final int TYPE_IPV4_DNS = 3; + private static final int TYPE_IPV6_DNS = 4; + private static final int TYPE_IPV4_DHCP = 5; + private static final int TYPE_IPV4_NETMASK = 6; + + private static final String TYPE_KEY = "type"; + private static final String VALUE_KEY = "address"; + private static final String IP6_PREFIX_LEN = "ip6PrefixLen"; + + private static final int PREFIX_LEN_UNUSED = -1; + + public final int type; + public final int ip6PrefixLen; + + // Null when it is an empty request + @Nullable public final InetAddress address; + + ConfigRequest(TunnelModeChildConfigRequest config) { + int prefixLen = PREFIX_LEN_UNUSED; + + if (config instanceof ConfigRequestIpv4Address) { + type = TYPE_IPV4_ADDRESS; + address = ((ConfigRequestIpv4Address) config).getAddress(); + } else if (config instanceof ConfigRequestIpv6Address) { + type = TYPE_IPV6_ADDRESS; + address = ((ConfigRequestIpv6Address) config).getAddress(); + if (address != null) { + prefixLen = ((ConfigRequestIpv6Address) config).getPrefixLength(); + } + } else if (config instanceof ConfigRequestIpv4DnsServer) { + type = TYPE_IPV4_DNS; + address = null; + } else if (config instanceof ConfigRequestIpv6DnsServer) { + type = TYPE_IPV6_DNS; + address = null; + } else if (config instanceof ConfigRequestIpv4DhcpServer) { + type = TYPE_IPV4_DHCP; + address = null; + } else if (config instanceof ConfigRequestIpv4Netmask) { + type = TYPE_IPV4_NETMASK; + address = null; + } else { + throw new IllegalStateException("Unknown TunnelModeChildConfigRequest"); + } + + ip6PrefixLen = prefixLen; + } + + ConfigRequest(PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + type = in.getInt(TYPE_KEY); + ip6PrefixLen = in.getInt(IP6_PREFIX_LEN); + + String addressStr = in.getString(VALUE_KEY); + if (addressStr == null) { + address = null; + } else { + address = InetAddresses.parseNumericAddress(addressStr); + } + } + + @NonNull + public PersistableBundle toPersistableBundle() { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(TYPE_KEY, type); + result.putInt(IP6_PREFIX_LEN, ip6PrefixLen); + + if (address != null) { + result.putString(VALUE_KEY, address.getHostAddress()); + } + + return result; + } + } + + /** Serializes a TunnelModeChildSessionParams to a PersistableBundle. */ + @NonNull + public static PersistableBundle toPersistableBundle( + @NonNull TunnelModeChildSessionParams params) { + final PersistableBundle result = new PersistableBundle(); + + final PersistableBundle saProposalBundle = + PersistableBundleUtils.fromList( + params.getSaProposals(), ChildSaProposalUtils::toPersistableBundle); + result.putPersistableBundle(SA_PROPOSALS_KEY, saProposalBundle); + + final PersistableBundle inTsBundle = + PersistableBundleUtils.fromList( + params.getInboundTrafficSelectors(), + IkeTrafficSelectorUtils::toPersistableBundle); + result.putPersistableBundle(INBOUND_TS_KEY, inTsBundle); + + final PersistableBundle outTsBundle = + PersistableBundleUtils.fromList( + params.getOutboundTrafficSelectors(), + IkeTrafficSelectorUtils::toPersistableBundle); + result.putPersistableBundle(OUTBOUND_TS_KEY, outTsBundle); + + result.putInt(HARD_LIFETIME_SEC_KEY, params.getHardLifetimeSeconds()); + result.putInt(SOFT_LIFETIME_SEC_KEY, params.getSoftLifetimeSeconds()); + + final List reqList = new ArrayList<>(); + for (TunnelModeChildConfigRequest req : params.getConfigurationRequests()) { + reqList.add(new ConfigRequest(req)); + } + final PersistableBundle configReqListBundle = + PersistableBundleUtils.fromList(reqList, ConfigRequest::toPersistableBundle); + result.putPersistableBundle(CONFIG_REQUESTS_KEY, configReqListBundle); + + return result; + } + + private static List getTsFromPersistableBundle( + PersistableBundle in, String key) { + PersistableBundle tsBundle = in.getPersistableBundle(key); + Objects.requireNonNull(tsBundle, "Value for key " + key + " was null"); + return PersistableBundleUtils.toList( + tsBundle, IkeTrafficSelectorUtils::fromPersistableBundle); + } + + /** Constructs a TunnelModeChildSessionParams by deserializing a PersistableBundle. */ + @NonNull + public static TunnelModeChildSessionParams fromPersistableBundle( + @NonNull PersistableBundle in) { + Objects.requireNonNull(in, "PersistableBundle was null"); + + final TunnelModeChildSessionParams.Builder builder = + new TunnelModeChildSessionParams.Builder(); + + final PersistableBundle proposalBundle = in.getPersistableBundle(SA_PROPOSALS_KEY); + Objects.requireNonNull(proposalBundle, "SA proposal was null"); + final List proposals = + PersistableBundleUtils.toList( + proposalBundle, ChildSaProposalUtils::fromPersistableBundle); + for (ChildSaProposal p : proposals) { + builder.addSaProposal(p); + } + + for (IkeTrafficSelector ts : getTsFromPersistableBundle(in, INBOUND_TS_KEY)) { + builder.addInboundTrafficSelectors(ts); + } + + for (IkeTrafficSelector ts : getTsFromPersistableBundle(in, OUTBOUND_TS_KEY)) { + builder.addOutboundTrafficSelectors(ts); + } + + builder.setLifetimeSeconds( + in.getInt(HARD_LIFETIME_SEC_KEY), in.getInt(SOFT_LIFETIME_SEC_KEY)); + final PersistableBundle configReqListBundle = in.getPersistableBundle(CONFIG_REQUESTS_KEY); + Objects.requireNonNull(configReqListBundle, "Config request list was null"); + final List reqList = + PersistableBundleUtils.toList(configReqListBundle, ConfigRequest::new); + + boolean hasIpv4AddressReq = false; + boolean hasIpv4NetmaskReq = false; + for (ConfigRequest req : reqList) { + switch (req.type) { + case ConfigRequest.TYPE_IPV4_ADDRESS: + hasIpv4AddressReq = true; + if (req.address == null) { + builder.addInternalAddressRequest(AF_INET); + } else { + builder.addInternalAddressRequest((Inet4Address) req.address); + } + break; + case ConfigRequest.TYPE_IPV6_ADDRESS: + if (req.address == null) { + builder.addInternalAddressRequest(AF_INET6); + } else { + builder.addInternalAddressRequest( + (Inet6Address) req.address, req.ip6PrefixLen); + } + break; + case ConfigRequest.TYPE_IPV4_NETMASK: + // Do not need to set netmask because it will be automatically set by the + // builder when an IPv4 internal address request is set. + hasIpv4NetmaskReq = true; + break; + case ConfigRequest.TYPE_IPV4_DNS: + if (req.address != null) { + Log.w(TAG, "Requesting a specific IPv4 DNS server is unsupported"); + } + builder.addInternalDnsServerRequest(AF_INET); + break; + case ConfigRequest.TYPE_IPV6_DNS: + if (req.address != null) { + Log.w(TAG, "Requesting a specific IPv6 DNS server is unsupported"); + } + builder.addInternalDnsServerRequest(AF_INET6); + break; + case ConfigRequest.TYPE_IPV4_DHCP: + if (req.address != null) { + Log.w(TAG, "Requesting a specific IPv4 DHCP server is unsupported"); + } + builder.addInternalDhcpServerRequest(AF_INET); + break; + default: + throw new IllegalArgumentException( + "Unrecognized config request type: " + req.type); + } + } + + if (hasIpv4AddressReq != hasIpv4NetmaskReq) { + Log.w( + TAG, + String.format( + "Expect IPv4 address request and IPv4 netmask request either both" + + " exist or both absent, but found hasIpv4AddressReq exists? %b," + + " hasIpv4AddressReq exists? %b, ", + hasIpv4AddressReq, hasIpv4NetmaskReq)); + } + + return builder.build(); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/util/LogUtils.java b/Vcn/framework-b/src/android/net/vcn/util/LogUtils.java new file mode 100644 index 0000000000..742aa761e6 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/util/LogUtils.java @@ -0,0 +1,39 @@ +/* + * 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 android.net.vcn.util; + +import android.annotation.Nullable; +import android.os.ParcelUuid; + +import com.android.net.module.util.HexDump; + +/** @hide */ +public class LogUtils { + /** + * Returns the hash of the subscription group in hexadecimal format. + * + * @return the hexadecimal encoded string if uuid was non-null, else {@code null} + */ + @Nullable + public static String getHashedSubscriptionGroup(@Nullable ParcelUuid uuid) { + if (uuid == null) { + return null; + } + + return HexDump.toHexString(uuid.hashCode()); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/util/MtuUtils.java b/Vcn/framework-b/src/android/net/vcn/util/MtuUtils.java new file mode 100644 index 0000000000..c3123bcecf --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/util/MtuUtils.java @@ -0,0 +1,171 @@ +/* + * 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 android.net.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 Length + Next + * Header). Note: Payload data, Pad Length and Next Header will need to be padded to be multiple + * of the block size of a cipher, and at the same time be aligned on a 4-byte boundary. + */ + private static final int GENERIC_ESP_OVERHEAD_MAX_V4 = 78; + + /** + * Max ESP overhead possible + * + *

40 (Outer IPv6) + 4 (SPI) + 4 (Seq) + 2 (Pad Length + Next Header). Note: Payload data, + * Pad Length and Next Header will need to be padded to be multiple of the block size of a + * cipher, and at the same time be aligned on a 4-byte boundary. + */ + private static final int GENERIC_ESP_OVERHEAD_MAX_V6 = 50; + + /** 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: + * + *

    + *
  • The MTU of the outer interface, minus the greatest ESP overhead (based on proposed + * algorithms). + *
  • The maximum MTU as provided in the arguments. + *
+ */ + public static int getMtu( + @NonNull List childProposals, + int maxMtu, + int underlyingMtu, + boolean isIpv4) { + if (underlyingMtu <= 0) { + return IPV6_MIN_MTU; + } + + 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; + } + } + + final int genericEspOverheadMax = + isIpv4 ? GENERIC_ESP_OVERHEAD_MAX_V4 : GENERIC_ESP_OVERHEAD_MAX_V6; + + // Return minimum of maxMtu, and the adjusted MTUs based on algorithms. + final int combinedModeMtu = underlyingMtu - maxAuthCryptOverhead - genericEspOverheadMax; + final int normalModeMtu = + underlyingMtu - maxCryptOverhead - maxAuthOverhead - genericEspOverheadMax; + return Math.min(Math.min(maxMtu, combinedModeMtu), normalModeMtu); + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/util/OneWayBoolean.java b/Vcn/framework-b/src/android/net/vcn/util/OneWayBoolean.java new file mode 100644 index 0000000000..a7ef67b187 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/util/OneWayBoolean.java @@ -0,0 +1,39 @@ +/* + * 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 android.net.vcn.util; + +/** + * OneWayBoolean is an abstraction for a boolean that MUST only ever be flipped from false to true + * + *

This class allows the providing of a guarantee that a flag will never be flipped back after + * being set. + * + * @hide + */ +public class OneWayBoolean { + private boolean mValue = false; + + /** Get boolean value. */ + public boolean getValue() { + return mValue; + } + + /** Sets the value to true. */ + public void setTrue() { + mValue = true; + } +} diff --git a/Vcn/framework-b/src/android/net/vcn/util/PersistableBundleUtils.java b/Vcn/framework-b/src/android/net/vcn/util/PersistableBundleUtils.java new file mode 100644 index 0000000000..8a687d8c59 --- /dev/null +++ b/Vcn/framework-b/src/android/net/vcn/util/PersistableBundleUtils.java @@ -0,0 +1,582 @@ +/* + * 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.util; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.ParcelUuid; +import android.os.PersistableBundle; + +import com.android.net.module.util.HexDump; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TreeSet; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** @hide */ +public class PersistableBundleUtils { + private static final String LIST_KEY_FORMAT = "LIST_ITEM_%d"; + private static final String COLLECTION_SIZE_KEY = "COLLECTION_LENGTH"; + private static final String MAP_KEY_FORMAT = "MAP_KEY_%d"; + private static final String MAP_VALUE_FORMAT = "MAP_VALUE_%d"; + + private static final String PARCEL_UUID_KEY = "PARCEL_UUID"; + private static final String BYTE_ARRAY_KEY = "BYTE_ARRAY_KEY"; + private static final String INTEGER_KEY = "INTEGER_KEY"; + private static final String STRING_KEY = "STRING_KEY"; + + /** + * Functional interface to convert an object of the specified type to a PersistableBundle. + * + * @param the type of the source object + */ + public interface Serializer { + /** + * Converts this object to a PersistableBundle. + * + * @return the PersistableBundle representation of this object + */ + PersistableBundle toPersistableBundle(T obj); + } + + /** + * Functional interface used to create an object of the specified type from a PersistableBundle. + * + * @param the type of the resultant object + */ + public interface Deserializer { + /** + * Creates an instance of specified type from a PersistableBundle representation. + * + * @param in the PersistableBundle representation + * @return an instance of the specified type + */ + T fromPersistableBundle(PersistableBundle in); + } + + /** Serializer to convert an integer to a PersistableBundle. */ + public static final Serializer INTEGER_SERIALIZER = + (i) -> { + final PersistableBundle result = new PersistableBundle(); + result.putInt(INTEGER_KEY, i); + return result; + }; + + /** Deserializer to convert a PersistableBundle to an integer. */ + public static final Deserializer INTEGER_DESERIALIZER = + (bundle) -> { + Objects.requireNonNull(bundle, "PersistableBundle is null"); + return bundle.getInt(INTEGER_KEY); + }; + + /** Serializer to convert s String to a PersistableBundle. */ + public static final Serializer STRING_SERIALIZER = + (i) -> { + final PersistableBundle result = new PersistableBundle(); + result.putString(STRING_KEY, i); + return result; + }; + + /** Deserializer to convert a PersistableBundle to a String. */ + public static final Deserializer STRING_DESERIALIZER = + (bundle) -> { + Objects.requireNonNull(bundle, "PersistableBundle is null"); + return bundle.getString(STRING_KEY); + }; + + /** + * Converts a ParcelUuid to a PersistableBundle. + * + *

To avoid key collisions, NO additional key/value pairs should be added to the returned + * PersistableBundle object. + * + * @param uuid a ParcelUuid instance to persist + * @return the PersistableBundle instance + */ + public static PersistableBundle fromParcelUuid(ParcelUuid uuid) { + final PersistableBundle result = new PersistableBundle(); + + result.putString(PARCEL_UUID_KEY, uuid.toString()); + + return result; + } + + /** + * Converts from a PersistableBundle to a ParcelUuid. + * + * @param bundle the PersistableBundle containing the ParcelUuid + * @return the ParcelUuid instance + */ + public static ParcelUuid toParcelUuid(PersistableBundle bundle) { + return ParcelUuid.fromString(bundle.getString(PARCEL_UUID_KEY)); + } + + /** + * Converts from a list of Persistable objects to a single PersistableBundle. + * + *

To avoid key collisions, NO additional key/value pairs should be added to the returned + * PersistableBundle object. + * + * @param the type of the objects to convert to the PersistableBundle + * @param in the list of objects to be serialized into a PersistableBundle + * @param serializer an implementation of the {@link Serializer} functional interface that + * converts an object of type T to a PersistableBundle + */ + @NonNull + public static PersistableBundle fromList( + @NonNull List in, @NonNull Serializer serializer) { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(COLLECTION_SIZE_KEY, in.size()); + for (int i = 0; i < in.size(); i++) { + final String key = String.format(LIST_KEY_FORMAT, i); + result.putPersistableBundle(key, serializer.toPersistableBundle(in.get(i))); + } + return result; + } + + /** + * Converts from a PersistableBundle to a list of objects. + * + * @param the type of the objects to convert from a PersistableBundle + * @param in the PersistableBundle containing the persisted list + * @param deserializer an implementation of the {@link Deserializer} functional interface that + * builds the relevant type of objects. + */ + @NonNull + public static List toList( + @NonNull PersistableBundle in, @NonNull Deserializer deserializer) { + final int listLength = in.getInt(COLLECTION_SIZE_KEY); + final ArrayList result = new ArrayList<>(listLength); + + for (int i = 0; i < listLength; i++) { + final String key = String.format(LIST_KEY_FORMAT, i); + final PersistableBundle item = in.getPersistableBundle(key); + + result.add(deserializer.fromPersistableBundle(item)); + } + return result; + } + + // TODO: b/170513329 Delete #fromByteArray and #toByteArray once BaseBundle#putByteArray and + // BaseBundle#getByteArray are exposed. + + /** + * Converts a byte array to a PersistableBundle. + * + *

To avoid key collisions, NO additional key/value pairs should be added to the returned + * PersistableBundle object. + * + * @param array a byte array instance to persist + * @return the PersistableBundle instance + */ + public static PersistableBundle fromByteArray(byte[] array) { + final PersistableBundle result = new PersistableBundle(); + + result.putString(BYTE_ARRAY_KEY, HexDump.toHexString(array)); + + return result; + } + + /** + * Converts from a PersistableBundle to a byte array. + * + * @param bundle the PersistableBundle containing the byte array + * @return the byte array instance + */ + public static byte[] toByteArray(PersistableBundle bundle) { + Objects.requireNonNull(bundle, "PersistableBundle is null"); + + String hex = bundle.getString(BYTE_ARRAY_KEY); + if (hex == null || hex.length() % 2 != 0) { + throw new IllegalArgumentException("PersistableBundle contains invalid byte array"); + } + + return HexDump.hexStringToByteArray(hex); + } + + /** + * Converts from a Map of Persistable objects to a single PersistableBundle. + * + *

To avoid key collisions, NO additional key/value pairs should be added to the returned + * PersistableBundle object. + * + * @param the type of the map-key to convert to the PersistableBundle + * @param the type of the map-value to convert to the PersistableBundle + * @param in the Map of objects implementing the {@link Persistable} interface + * @param keySerializer an implementation of the {@link Serializer} functional interface that + * converts a map-key of type T to a PersistableBundle + * @param valueSerializer an implementation of the {@link Serializer} functional interface that + * converts a map-value of type E to a PersistableBundle + */ + @NonNull + public static PersistableBundle fromMap( + @NonNull Map in, + @NonNull Serializer keySerializer, + @NonNull Serializer valueSerializer) { + final PersistableBundle result = new PersistableBundle(); + + result.putInt(COLLECTION_SIZE_KEY, in.size()); + int i = 0; + for (Entry entry : in.entrySet()) { + final String keyKey = String.format(MAP_KEY_FORMAT, i); + final String valueKey = String.format(MAP_VALUE_FORMAT, i); + result.putPersistableBundle(keyKey, keySerializer.toPersistableBundle(entry.getKey())); + result.putPersistableBundle( + valueKey, valueSerializer.toPersistableBundle(entry.getValue())); + + i++; + } + + return result; + } + + /** + * Converts from a PersistableBundle to a Map of objects. + * + *

In an attempt to preserve ordering, the returned map will be a LinkedHashMap. However, the + * guarantees on the ordering can only ever be as strong as the map that was serialized in + * {@link fromMap()}. If the initial map that was serialized had no ordering guarantees, the + * deserialized map similarly may be of a non-deterministic order. + * + * @param the type of the map-key to convert from a PersistableBundle + * @param the type of the map-value to convert from a PersistableBundle + * @param in the PersistableBundle containing the persisted Map + * @param keyDeserializer an implementation of the {@link Deserializer} functional interface + * that builds the relevant type of map-key. + * @param valueDeserializer an implementation of the {@link Deserializer} functional interface + * that builds the relevant type of map-value. + * @return An instance of the parsed map as a LinkedHashMap (in an attempt to preserve + * ordering). + */ + @NonNull + public static LinkedHashMap toMap( + @NonNull PersistableBundle in, + @NonNull Deserializer keyDeserializer, + @NonNull Deserializer valueDeserializer) { + final int mapSize = in.getInt(COLLECTION_SIZE_KEY); + final LinkedHashMap result = new LinkedHashMap<>(mapSize); + + for (int i = 0; i < mapSize; i++) { + final String keyKey = String.format(MAP_KEY_FORMAT, i); + final String valueKey = String.format(MAP_VALUE_FORMAT, i); + final PersistableBundle keyBundle = in.getPersistableBundle(keyKey); + final PersistableBundle valueBundle = in.getPersistableBundle(valueKey); + + final K key = keyDeserializer.fromPersistableBundle(keyBundle); + final V value = valueDeserializer.fromPersistableBundle(valueBundle); + result.put(key, value); + } + return result; + } + + /** + * Converts a PersistableBundle into a disk-stable byte array format + * + * @param bundle the PersistableBundle to be converted to a disk-stable format + * @return the byte array representation of the PersistableBundle + */ + @Nullable + public static byte[] toDiskStableBytes(@NonNull PersistableBundle bundle) throws IOException { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + bundle.writeToStream(outputStream); + return outputStream.toByteArray(); + } + + /** + * Converts from a disk-stable byte array format to a PersistableBundle + * + * @param bytes the disk-stable byte array + * @return the PersistableBundle parsed from this byte array. + */ + public static PersistableBundle fromDiskStableBytes(@NonNull byte[] bytes) throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + return PersistableBundle.readFromStream(inputStream); + } + + /** + * Ensures safe reading and writing of {@link PersistableBundle}s to and from disk. + * + *

This class will enforce exclusion between reads and writes using the standard semantics of + * a ReadWriteLock. Specifically, concurrent readers ARE allowed, but reads/writes from/to the + * file are mutually exclusive. In other words, for an unbounded number n, the acceptable states + * are n readers, OR 1 writer (but not both). + */ + public static class LockingReadWriteHelper { + private final ReadWriteLock mDiskLock = new ReentrantReadWriteLock(); + private final String mPath; + + public LockingReadWriteHelper(@NonNull String path) { + mPath = Objects.requireNonNull(path, "fileName was null"); + } + + /** + * Reads the {@link PersistableBundle} from the disk. + * + * @return the PersistableBundle, if the file existed, or null otherwise + */ + @Nullable + public PersistableBundle readFromDisk() throws IOException { + try { + mDiskLock.readLock().lock(); + final File file = new File(mPath); + if (!file.exists()) { + return null; + } + + try (FileInputStream fis = new FileInputStream(file)) { + return PersistableBundle.readFromStream(fis); + } + } finally { + mDiskLock.readLock().unlock(); + } + } + + /** + * Writes a {@link PersistableBundle} to disk. + * + * @param bundle the {@link PersistableBundle} to write to disk + */ + public void writeToDisk(@NonNull PersistableBundle bundle) throws IOException { + Objects.requireNonNull(bundle, "bundle was null"); + + try { + mDiskLock.writeLock().lock(); + final File file = new File(mPath); + if (!file.exists()) { + file.getParentFile().mkdirs(); + } + + try (FileOutputStream fos = new FileOutputStream(file)) { + bundle.writeToStream(fos); + } + } finally { + mDiskLock.writeLock().unlock(); + } + } + } + + /** + * Returns a copy of the persistable bundle with only the specified keys + * + *

This allows for holding minimized copies for memory-saving purposes. + */ + @NonNull + public static PersistableBundle minimizeBundle( + @NonNull PersistableBundle bundle, String... keys) { + final PersistableBundle minimized = new PersistableBundle(); + + if (bundle == null) { + return minimized; + } + + for (String key : keys) { + if (bundle.containsKey(key)) { + final Object value = bundle.get(key); + if (value == null) { + continue; + } + + if (value instanceof Boolean) { + minimized.putBoolean(key, (Boolean) value); + } else if (value instanceof boolean[]) { + minimized.putBooleanArray(key, (boolean[]) value); + } else if (value instanceof Double) { + minimized.putDouble(key, (Double) value); + } else if (value instanceof double[]) { + minimized.putDoubleArray(key, (double[]) value); + } else if (value instanceof Integer) { + minimized.putInt(key, (Integer) value); + } else if (value instanceof int[]) { + minimized.putIntArray(key, (int[]) value); + } else if (value instanceof Long) { + minimized.putLong(key, (Long) value); + } else if (value instanceof long[]) { + minimized.putLongArray(key, (long[]) value); + } else if (value instanceof String) { + minimized.putString(key, (String) value); + } else if (value instanceof String[]) { + minimized.putStringArray(key, (String[]) value); + } else if (value instanceof PersistableBundle) { + minimized.putPersistableBundle(key, (PersistableBundle) value); + } else { + continue; + } + } + } + + return minimized; + } + + /** Builds a stable hashcode */ + public static int getHashCode(@Nullable PersistableBundle bundle) { + if (bundle == null) { + return -1; + } + + int iterativeHashcode = 0; + TreeSet treeSet = new TreeSet<>(bundle.keySet()); + for (String key : treeSet) { + Object val = bundle.get(key); + if (val instanceof PersistableBundle) { + iterativeHashcode = + Objects.hash(iterativeHashcode, key, getHashCode((PersistableBundle) val)); + } else { + iterativeHashcode = Objects.hash(iterativeHashcode, key, val); + } + } + + return iterativeHashcode; + } + + /** Checks for persistable bundle equality */ + public static boolean isEqual( + @Nullable PersistableBundle left, @Nullable PersistableBundle right) { + // Check for pointer equality & null equality + if (Objects.equals(left, right)) { + return true; + } + + // If only one of the two is null, but not the other, not equal by definition. + if (Objects.isNull(left) != Objects.isNull(right)) { + return false; + } + + if (!left.keySet().equals(right.keySet())) { + return false; + } + + for (String key : left.keySet()) { + Object leftVal = left.get(key); + Object rightVal = right.get(key); + + // Check for equality + if (Objects.equals(leftVal, rightVal)) { + continue; + } else if (Objects.isNull(leftVal) != Objects.isNull(rightVal)) { + // If only one of the two is null, but not the other, not equal by definition. + return false; + } else if (!Objects.equals(leftVal.getClass(), rightVal.getClass())) { + // If classes are different, not equal by definition. + return false; + } + if (leftVal instanceof PersistableBundle) { + if (!isEqual((PersistableBundle) leftVal, (PersistableBundle) rightVal)) { + return false; + } + } else if (leftVal.getClass().isArray()) { + if (leftVal instanceof boolean[]) { + if (!Arrays.equals((boolean[]) leftVal, (boolean[]) rightVal)) { + return false; + } + } else if (leftVal instanceof double[]) { + if (!Arrays.equals((double[]) leftVal, (double[]) rightVal)) { + return false; + } + } else if (leftVal instanceof int[]) { + if (!Arrays.equals((int[]) leftVal, (int[]) rightVal)) { + return false; + } + } else if (leftVal instanceof long[]) { + if (!Arrays.equals((long[]) leftVal, (long[]) rightVal)) { + return false; + } + } else if (!Arrays.equals((Object[]) leftVal, (Object[]) rightVal)) { + return false; + } + } else { + if (!Objects.equals(leftVal, rightVal)) { + return false; + } + } + } + + return true; + } + + /** + * Wrapper class around PersistableBundles to allow equality comparisons + * + *

This class exposes the minimal getters to retrieve values. + */ + public static class PersistableBundleWrapper { + @NonNull private final PersistableBundle mBundle; + + public PersistableBundleWrapper(@NonNull PersistableBundle bundle) { + mBundle = Objects.requireNonNull(bundle, "Bundle was null"); + } + + /** + * Retrieves the integer associated with the provided key. + * + * @param key the string key to query + * @param defaultValue the value to return if key does not exist + * @return the int value, or the default + */ + public int getInt(String key, int defaultValue) { + return mBundle.getInt(key, defaultValue); + } + + /** + * Returns the value associated with the given key, or null if no mapping of the desired + * type exists for the given key or a null value is explicitly associated with the key. + * + * @param key a String, or null + * @param defaultValue the value to return if key does not exist + * @return an int[] value, or null + */ + @Nullable + public int[] getIntArray(@Nullable String key, @Nullable int[] defaultValue) { + final int[] value = mBundle.getIntArray(key); + return value == null ? defaultValue : value; + } + + @Override + public int hashCode() { + return getHashCode(mBundle); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PersistableBundleWrapper)) { + return false; + } + + final PersistableBundleWrapper other = (PersistableBundleWrapper) obj; + + return isEqual(mBundle, other.mBundle); + } + + @Override + public String toString() { + return mBundle.toString(); + } + } +} diff --git a/Vcn/service-b/Android.bp b/Vcn/service-b/Android.bp new file mode 100644 index 0000000000..e9882ae030 --- /dev/null +++ b/Vcn/service-b/Android.bp @@ -0,0 +1,127 @@ +// +// Copyright (C) 2024 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 { + default_team: "trendy_team_enigma", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +filegroup { + name: "vcn-location-sources", + srcs: select(release_flag("RELEASE_MOVE_VCN_TO_MAINLINE"), { + true: [ + "vcn-location-flag/module/com/android/server/vcn/VcnLocation.java", + ], + default: [ + "vcn-location-flag/platform/com/android/server/vcn/VcnLocation.java", + ], + }), + visibility: [ + "//frameworks/base/services/core", + "//packages/modules/Connectivity/service-t", + ], +} + +// TODO: b/374174952 This library is only used in "service-connectivity-b-platform" +// After VCN moves to Connectivity/ and the modularization is done, this lib and +// "service-connectivity-b-platform" can both be removed +java_library { + name: "connectivity-utils-service-vcn-internal", + sdk_version: "module_current", + min_sdk_version: "30", + srcs: ["service-utils/**/*.java"], + libs: [ + "framework-annotations-lib", + "unsupportedappusage", + ], + visibility: ["//visibility:private"], +} + +filegroup { + name: "service-vcn-sources", + srcs: [ + "src/**/*.java", + ":statslog-vcn-java-gen", + ], + visibility: [ + "//packages/modules/Connectivity/service-b", + ], +} + +// This java_defaults will be used for "service-connectivity-b-platform" and +// "service-connectivity-b-pre-jarjar" +java_defaults { + name: "service-connectivity-b-pre-jarjar-defaults", + defaults: ["framework-system-server-module-defaults"], // This is a system server jar + + srcs: [ + ":service-vcn-sources", + ], + + libs: [ + "android.net.ipsec.ike.stubs.module_lib", + "framework-annotations-lib", + "framework-connectivity-pre-jarjar", + "framework-connectivity-t-pre-jarjar", + "framework-connectivity-b-pre-jarjar", + "framework-statsd.stubs.module_lib", + "framework-wifi.stubs.module_lib", + "keepanno-annotations", + "modules-utils-statemachine", + "unsupportedappusage", + ], + + // TODO: b/374174952 Dynamically include these libs when VCN + // modularization is released + static_libs: [ + "net-utils-service-vcn", + "modules-utils-handlerexecutor", + ], + + defaults_visibility: [ + "//packages/modules/Connectivity/service-b", + ], +} + +// This library is only used to be included into services.jar when the build system +// flag RELEASE_MOVE_VCN_TO_MAINLINE is disabled. When the flag is enabled, a module +// version of this library will be included in Tethering module +java_library { + name: "service-connectivity-b-platform", + defaults: ["service-connectivity-b-pre-jarjar-defaults"], + static_libs: ["connectivity-utils-service-vcn-internal"], + + sdk_version: "system_server_current", + min_sdk_version: "35", + + // TODO (b/374174952 ): This file is for jarjaring files in + // "connectivity-utils-service-vcn-internal". + jarjar_rules: "service-vcn-platform-jarjar-rules.txt", + + visibility: [ + "//frameworks/base/services", + ], + apex_available: [ + "//apex_available:platform", + ], +} + +genrule { + name: "statslog-vcn-java-gen", + tools: ["stats-log-api-gen"], + cmd: "$(location stats-log-api-gen) --java $(out) --module vcn --javaPackage com.android.server.vcn.metrics --javaClass VcnStatsLog", + out: ["com/android/server/vcn/metrics/VcnStatsLog.java"], +} diff --git a/Vcn/service-b/service-utils/android/util/LocalLog.java b/Vcn/service-b/service-utils/android/util/LocalLog.java new file mode 100644 index 0000000000..5955d930aa --- /dev/null +++ b/Vcn/service-b/service-utils/android/util/LocalLog.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2025 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.util; + +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Build; +import android.os.SystemClock; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +/** + * @hide + */ +// Exported to Mainline modules; cannot use annotations +// @android.ravenwood.annotation.RavenwoodKeepWholeClass +// TODO: b/374174952 This is an exact copy of frameworks/base/core/java/android/util/LocalLog.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public final class LocalLog { + + private final Deque mLog; + private final int mMaxLines; + + /** + * {@code true} to use log timestamps expressed in local date/time, {@code false} to use log + * timestamped expressed with the elapsed realtime clock and UTC system clock. {@code false} is + * useful when logging behavior that modifies device time zone or system clock. + */ + private final boolean mUseLocalTimestamps; + + @UnsupportedAppUsage + public LocalLog(int maxLines) { + this(maxLines, true /* useLocalTimestamps */); + } + + public LocalLog(int maxLines, boolean useLocalTimestamps) { + mMaxLines = Math.max(0, maxLines); + mLog = new ArrayDeque<>(mMaxLines); + mUseLocalTimestamps = useLocalTimestamps; + } + + @UnsupportedAppUsage + public void log(String msg) { + if (mMaxLines <= 0) { + return; + } + final String logLine; + if (mUseLocalTimestamps) { + logLine = LocalDateTime.now() + " - " + msg; + } else { + logLine = Duration.ofMillis(SystemClock.elapsedRealtime()) + + " / " + Instant.now() + " - " + msg; + } + append(logLine); + } + + private synchronized void append(String logLine) { + while (mLog.size() >= mMaxLines) { + mLog.remove(); + } + mLog.add(logLine); + } + + @UnsupportedAppUsage + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + dump(pw); + } + + public synchronized void dump(PrintWriter pw) { + dump("", pw); + } + + /** + * Dumps the content of local log to print writer with each log entry predeced with indent + * + * @param indent indent that precedes each log entry + * @param pw printer writer to write into + */ + public synchronized void dump(String indent, PrintWriter pw) { + Iterator itr = mLog.iterator(); + while (itr.hasNext()) { + pw.printf("%s%s\n", indent, itr.next()); + } + } + + public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + reverseDump(pw); + } + + public synchronized void reverseDump(PrintWriter pw) { + Iterator itr = mLog.descendingIterator(); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } + + // @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public synchronized void clear() { + mLog.clear(); + } + + public static class ReadOnlyLocalLog { + private final LocalLog mLog; + ReadOnlyLocalLog(LocalLog log) { + mLog = log; + } + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.dump(pw); + } + public void dump(PrintWriter pw) { + mLog.dump(pw); + } + public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.reverseDump(pw); + } + public void reverseDump(PrintWriter pw) { + mLog.reverseDump(pw); + } + } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + public ReadOnlyLocalLog readOnlyLocalLog() { + return new ReadOnlyLocalLog(this); + } +} \ No newline at end of file diff --git a/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java b/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java new file mode 100644 index 0000000000..7db62f8e9f --- /dev/null +++ b/Vcn/service-b/service-utils/com/android/internal/util/WakeupMessage.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 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.internal.util; + +import android.app.AlarmManager; +import android.content.Context; +import android.os.Handler; +import android.os.Message; + +import com.android.internal.annotations.VisibleForTesting; + + /** + * An AlarmListener that sends the specified message to a Handler and keeps the system awake until + * the message is processed. + * + * This is useful when using the AlarmManager direct callback interface to wake up the system and + * request that an object whose API consists of messages (such as a StateMachine) perform some + * action. + * + * In this situation, using AlarmManager.onAlarmListener by itself will wake up the system to send + * the message, but does not guarantee that the system will be awake until the target object has + * processed it. This is because as soon as the onAlarmListener sends the message and returns, the + * AlarmManager releases its wakelock and the system is free to go to sleep again. + */ +// TODO: b/374174952 This is an exact copy of +// frameworks/base/core/java/com/android/internal/util/WakeupMessage.java. +// This file is only used in "service-connectivity-b-platform" before the VCN modularization flag +// is fully ramped. When the flag is fully ramped and the development is finalized, this file can +// be removed. +public class WakeupMessage implements AlarmManager.OnAlarmListener { + private final AlarmManager mAlarmManager; + + @VisibleForTesting + protected final Handler mHandler; + @VisibleForTesting + protected final String mCmdName; + @VisibleForTesting + protected final int mCmd, mArg1, mArg2; + @VisibleForTesting + protected final Object mObj; + private final Runnable mRunnable; + private boolean mScheduled; + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2, Object obj) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = cmd; + mArg1 = arg1; + mArg2 = arg2; + mObj = obj; + mRunnable = null; + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) { + this(context, handler, cmdName, cmd, arg1, 0, null); + } + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2) { + this(context, handler, cmdName, cmd, arg1, arg2, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) { + this(context, handler, cmdName, cmd, 0, 0, null); + } + + public WakeupMessage(Context context, Handler handler, String cmdName, Runnable runnable) { + mAlarmManager = getAlarmManager(context); + mHandler = handler; + mCmdName = cmdName; + mCmd = 0; + mArg1 = 0; + mArg2 = 0; + mObj = null; + mRunnable = runnable; + } + + private static AlarmManager getAlarmManager(Context context) { + return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + + /** + * Schedule the message to be delivered at the time in milliseconds of the + * {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()} clock and wakeup + * the device when it goes off. If schedule is called multiple times without the message being + * dispatched then the alarm is rescheduled to the new time. + */ + public synchronized void schedule(long when) { + mAlarmManager.setExact( + AlarmManager.ELAPSED_REALTIME_WAKEUP, when, mCmdName, this, mHandler); + mScheduled = true; + } + + /** + * Cancel all pending messages. This includes alarms that may have been fired, but have not been + * run on the handler yet. + */ + public synchronized void cancel() { + if (mScheduled) { + mAlarmManager.cancel(this); + mScheduled = false; + } + } + + @Override + public void onAlarm() { + // Once this method is called the alarm has already been fired and removed from + // AlarmManager (it is still partially tracked, but only for statistics). The alarm can now + // be marked as unscheduled so that it can be rescheduled in the message handler. + final boolean stillScheduled; + synchronized (this) { + stillScheduled = mScheduled; + mScheduled = false; + } + if (stillScheduled) { + Message msg; + if (mRunnable == null) { + msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj); + } else { + msg = Message.obtain(mHandler, mRunnable); + } + mHandler.dispatchMessage(msg); + msg.recycle(); + } + } +} \ No newline at end of file diff --git a/Vcn/service-b/service-vcn-platform-jarjar-rules.txt b/Vcn/service-b/service-vcn-platform-jarjar-rules.txt new file mode 100644 index 0000000000..6ec39d9532 --- /dev/null +++ b/Vcn/service-b/service-vcn-platform-jarjar-rules.txt @@ -0,0 +1,2 @@ +rule android.util.LocalLog android.net.vcn.module.repackaged.android.util.LocalLog +rule com.android.internal.util.WakeupMessage android.net.vcn.module.repackaged.com.android.internal.util.WakeupMessage \ No newline at end of file diff --git a/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java b/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java new file mode 100644 index 0000000000..aac217b3cc --- /dev/null +++ b/Vcn/service-b/src/com/android/server/ConnectivityServiceInitializerB.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 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; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import com.android.tools.r8.keepanno.annotations.KeepItemKind; +import com.android.tools.r8.keepanno.annotations.UsedByReflection; + +/** + * Service initializer for VCN. This is called by system server to create a new instance of + * VcnManagementService. + */ +// This class is reflectively invoked from SystemServer and ConnectivityServiceInitializer. +// Without this annotation, this class will be treated as unused class and be removed during build +// time. +@UsedByReflection(kind = KeepItemKind.CLASS_AND_METHODS) +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public final class ConnectivityServiceInitializerB extends SystemService { + private static final String TAG = ConnectivityServiceInitializerB.class.getSimpleName(); + private final VcnManagementService mVcnManagementService; + + // STOPSHIP: b/385203616 This static flag is for handling a temporary case when the mainline + // module prebuilt has updated to register the VCN but the platform change to remove + // registration is not merged. After mainline prebuilt is updated, we should merge the platform + // ASAP and remove this static check. This check is safe because both mainline and platform + // registration are triggered from the same method on the same thread. + private static boolean sIsRegistered = false; + + public ConnectivityServiceInitializerB(Context context) { + super(context); + + if (!sIsRegistered) { + mVcnManagementService = VcnManagementService.create(context); + sIsRegistered = true; + } else { + mVcnManagementService = null; + Log.e( + TAG, + "Ignore this registration since VCN is already registered. It will happen when" + + " the mainline module prebuilt has updated to register the VCN but the" + + " platform change to remove registration is not merged."); + } + } + + @Override + public void onStart() { + if (mVcnManagementService != null) { + Log.i(TAG, "Registering " + Context.VCN_MANAGEMENT_SERVICE); + publishBinderService( + Context.VCN_MANAGEMENT_SERVICE, + mVcnManagementService, + /* allowIsolated= */ false); + } + } + + @Override + public void onBootPhase(int phase) { + if (mVcnManagementService != null && phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { + Log.i(TAG, "Starting " + Context.VCN_MANAGEMENT_SERVICE); + mVcnManagementService.systemReady(); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/VcnManagementService.java b/Vcn/service-b/src/com/android/server/VcnManagementService.java new file mode 100644 index 0000000000..8edd63dc34 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/VcnManagementService.java @@ -0,0 +1,1551 @@ +/* + * 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; + +import static android.Manifest.permission.DUMP; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.vcn.VcnGatewayConnectionConfig.ALLOWED_CAPABILITIES; +import static android.net.vcn.VcnManager.VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_NOT_CONFIGURED; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE; +import static android.telephony.SubscriptionManager.isValidSubscriptionId; + +import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import static com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionTrackerCallback; + +import static java.util.Objects.requireNonNull; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.AppOpsManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.vcn.IVcnManagementService; +import android.net.vcn.IVcnStatusCallback; +import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; +import android.net.vcn.VcnConfig; +import android.net.vcn.VcnManager.VcnErrorCode; +import android.net.vcn.VcnManager.VcnStatusCode; +import android.net.vcn.VcnUnderlyingNetworkPolicy; +import android.net.vcn.util.PersistableBundleUtils; +import android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; +import android.net.wifi.WifiInfo; +import android.os.Binder; +import android.os.Build; +import android.os.Environment; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.ParcelUuid; +import android.os.PersistableBundle; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.os.UserHandle; +import android.os.UserManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.LocalLog; +import android.util.Log; +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.net.module.util.BinderUtils; +import com.android.net.module.util.HandlerUtils; +import com.android.net.module.util.LocationPermissionChecker; +import com.android.net.module.util.PermissionUtils; +import com.android.server.vcn.TelephonySubscriptionTracker; +import com.android.server.vcn.Vcn; +import com.android.server.vcn.VcnContext; +import com.android.server.vcn.VcnNetworkProvider; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * VcnManagementService manages Virtual Carrier Network profiles and lifecycles. + * + *

The internal structure of the VCN Management subsystem is as follows:
+ *
+ * +-------------------------+ 1:1                                +--------------------------------+
+ * |  VcnManagementService   | ------------ Creates ------------> |  TelephonySubscriptionManager  |
+ * |                         |                                    |                                |
+ * |   Manages configs and   |                                    | Tracks subscriptions, carrier  |
+ * | Vcn instance lifecycles | <--- Notifies of subscription & -- | privilege changes, caches maps |
+ * +-------------------------+      carrier privilege changes     +--------------------------------+
+ *      | 1:N          ^
+ *      |              |
+ *      |              +-------------------------------+
+ *      +---------------+                              |
+ *                      |                              |
+ *         Creates when config present,                |
+ *        subscription group active, and               |
+ *      providing app is carrier privileged     Notifies of safe
+ *                      |                      mode state changes
+ *                      v                              |
+ * +-----------------------------------------------------------------------+
+ * |                                  Vcn                                  |
+ * |                                                                       |
+ * |       Manages GatewayConnection lifecycles based on fulfillable       |
+ * |                NetworkRequest(s) and overall safe-mode                |
+ * +-----------------------------------------------------------------------+
+ *                      | 1:N                          ^
+ *              Creates to fulfill                     |
+ *           NetworkRequest(s), tears   Notifies of VcnGatewayConnection
+ *          down when no longer needed   teardown (e.g. Network reaped)
+ *                      |                 and safe-mode timer changes
+ *                      v                              |
+ * +-----------------------------------------------------------------------+
+ * |                          VcnGatewayConnection                         |
+ * |                                                                       |
+ * |       Manages a single (IKEv2) tunnel session and NetworkAgent,       |
+ * |  handles mobility events, (IPsec) Tunnel setup and safe-mode timers   |
+ * +-----------------------------------------------------------------------+
+ *                      | 1:1                          ^
+ *                      |                              |
+ *          Creates upon instantiation      Notifies of changes in
+ *                      |                 selected underlying network
+ *                      |                     or its properties
+ *                      v                              |
+ * +-----------------------------------------------------------------------+
+ * |                       UnderlyingNetworkController                     |
+ * |                                                                       |
+ * | Manages lifecycle of underlying physical networks, filing requests to |
+ * | bring them up, and releasing them as they become no longer necessary  |
+ * +-----------------------------------------------------------------------+
+ * 
+ * + * @hide + */ +// TODO(b/180451994): ensure all incoming + outgoing calls have a cleared calling identity +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class VcnManagementService extends IVcnManagementService.Stub { + @NonNull private static final String TAG = VcnManagementService.class.getSimpleName(); + @NonNull private static final String CONTEXT_ATTRIBUTION_TAG = "VCN"; + + private static final long DUMP_TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(5); + private static final int LOCAL_LOG_LINE_COUNT = 512; + + private static final Set RESTRICTED_TRANSPORTS_DEFAULT = + Collections.singleton(TRANSPORT_WIFI); + + // Public for use in all other VCN classes + @NonNull public static final LocalLog LOCAL_LOG = new LocalLog(LOCAL_LOG_LINE_COUNT); + + public static final boolean VDBG = false; // STOPSHIP: if true + + // The system path is copied from Environment.getDataSystemDirectory + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final String VCN_CONFIG_FILE = + new File(Environment.getDataDirectory(), "system/vcn/configs.xml").getPath(); + + // TODO(b/176956496): Directly use CarrierServiceBindHelper.UNBIND_DELAY_MILLIS + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final long CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS = TimeUnit.SECONDS.toMillis(30); + + /* 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; + @NonNull private final TelephonySubscriptionTrackerCallback mTelephonySubscriptionTrackerCb; + @NonNull private final TelephonySubscriptionTracker mTelephonySubscriptionTracker; + @NonNull private final BroadcastReceiver mVcnBroadcastReceiver; + + @NonNull + private final TrackingNetworkCallback mTrackingNetworkCallback = new TrackingNetworkCallback(); + + @GuardedBy("mLock") + @NonNull + private final Map mConfigs = new ArrayMap<>(); + + @GuardedBy("mLock") + @NonNull + private final Map mVcns = new ArrayMap<>(); + + @GuardedBy("mLock") + @NonNull + private TelephonySubscriptionSnapshot mLastSnapshot = + TelephonySubscriptionSnapshot.EMPTY_SNAPSHOT; + + @NonNull private final Object mLock = new Object(); + + @NonNull private final PersistableBundleUtils.LockingReadWriteHelper mConfigDiskRwHelper; + + @GuardedBy("mLock") + @NonNull + private final Map mRegisteredPolicyListeners = + new ArrayMap<>(); + + @GuardedBy("mLock") + @NonNull + private final Map mRegisteredStatusCallbacks = new ArrayMap<>(); + + @VisibleForTesting(visibility = Visibility.PRIVATE) + VcnManagementService(@NonNull Context context, @NonNull Dependencies deps) { + mContext = + requireNonNull(context, "Missing context") + .createAttributionContext(CONTEXT_ATTRIBUTION_TAG); + mDeps = requireNonNull(deps, "Missing dependencies"); + + mLooper = mDeps.getLooper(); + mHandler = new Handler(mLooper); + mNetworkProvider = new VcnNetworkProvider(mContext, mLooper); + mTelephonySubscriptionTrackerCb = new VcnSubscriptionTrackerCallback(); + mTelephonySubscriptionTracker = mDeps.newTelephonySubscriptionTracker( + mContext, mLooper, mTelephonySubscriptionTrackerCb); + + mConfigDiskRwHelper = mDeps.newPersistableBundleLockingReadWriteHelper(VCN_CONFIG_FILE); + + mVcnBroadcastReceiver = new VcnBroadcastReceiver(); + + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REPLACED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED); + intentFilter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + intentFilter.addDataScheme("package"); + mContext.registerReceiver( + mVcnBroadcastReceiver, intentFilter, null /* broadcastPermission */, mHandler); + + // 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) { + logErr("Failed to read configs from disk; retrying", e1); + + // Retry immediately. The IOException may have been transient. + try { + configBundle = mConfigDiskRwHelper.readFromDisk(); + } catch (IOException e2) { + logWtf("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()); + } + } + + // Re-evaluate subscriptions, and start/stop VCNs. This starts with an empty + // snapshot, and therefore safe even before telephony subscriptions are loaded. + mTelephonySubscriptionTrackerCb.onNewSnapshot(mLastSnapshot); + } + } + }); + } + + /** Called by ConnectivityServiceInitializerB to create instances. */ + // VcnManagementService will be jarjared but ConnectivityServiceInitializerB will not. Thus this + // method needs to be public for ConnectivityServiceInitializerB to access + public static VcnManagementService create(@NonNull Context context) { + return new VcnManagementService(context, new Dependencies()); + } + + /** External dependencies used by VcnManagementService, for injection in tests */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + private HandlerThread mHandlerThread; + + /** Retrieves a looper for the VcnManagementService */ + public Looper getLooper() { + if (mHandlerThread == null) { + synchronized (this) { + if (mHandlerThread == null) { + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + } + } + } + return mHandlerThread.getLooper(); + } + + /** Creates a new VcnInstance using the provided configuration */ + public TelephonySubscriptionTracker newTelephonySubscriptionTracker( + @NonNull Context context, + @NonNull Looper looper, + @NonNull TelephonySubscriptionTrackerCallback callback) { + return new TelephonySubscriptionTracker(context, new Handler(looper), callback); + } + + /** + * Retrieves the caller's UID + * + *

This call MUST be made before calling {@link Binder#clearCallingIdentity}, otherwise + * this will not work properly. + * + * @return + */ + 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); + } + + /** Creates a new VcnContext */ + public VcnContext newVcnContext( + @NonNull Context context, + @NonNull Looper looper, + @NonNull VcnNetworkProvider vcnNetworkProvider, + boolean isInTestMode) { + return new VcnContext(context, looper, vcnNetworkProvider, isInTestMode); + } + + /** Creates a new Vcn instance using the provided configuration */ + public Vcn newVcn( + @NonNull VcnContext vcnContext, + @NonNull ParcelUuid subscriptionGroup, + @NonNull VcnConfig config, + @NonNull TelephonySubscriptionSnapshot snapshot, + @NonNull VcnCallback vcnCallback) { + return new Vcn(vcnContext, subscriptionGroup, config, snapshot, vcnCallback); + } + + /** Gets the subId indicated by the given {@link WifiInfo}. */ + public int getSubIdForWifiInfo(@NonNull WifiInfo wifiInfo) { + return wifiInfo.getSubscriptionId(); + } + + /** Creates a new LocationPermissionChecker for the provided Context. */ + public LocationPermissionChecker newLocationPermissionChecker(@NonNull Context context) { + return new LocationPermissionChecker(context); + } + + /** Gets transports that need to be marked as restricted by the VCN from CarrierConfig */ + // TODO: b/262269892 This method was created to perform experiments before the relevant API + // was exposed. Now it is obsolete and should be removed. + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Set getRestrictedTransportsFromCarrierConfig( + ParcelUuid subGrp, TelephonySubscriptionSnapshot lastSnapshot) { + if (!Build.isDebuggable()) { + return RESTRICTED_TRANSPORTS_DEFAULT; + } + + final PersistableBundleWrapper carrierConfig = + lastSnapshot.getCarrierConfigForSubGrp(subGrp); + if (carrierConfig == null) { + return RESTRICTED_TRANSPORTS_DEFAULT; + } + + final int[] defaultValue = + RESTRICTED_TRANSPORTS_DEFAULT.stream().mapToInt(i -> i).toArray(); + final int[] restrictedTransportsArray = + carrierConfig.getIntArray( + VCN_RESTRICTED_TRANSPORTS_INT_ARRAY_KEY, + defaultValue); + + // Convert to a boxed set + final Set restrictedTransports = new ArraySet<>(); + for (int transport : restrictedTransportsArray) { + restrictedTransports.add(transport); + } + return restrictedTransports; + } + + /** Gets the transports that need to be marked as restricted by the VCN */ + public Set getRestrictedTransports( + ParcelUuid subGrp, + TelephonySubscriptionSnapshot lastSnapshot, + VcnConfig vcnConfig) { + final Set restrictedTransports = new ArraySet<>(); + restrictedTransports.addAll(vcnConfig.getRestrictedUnderlyingNetworkTransports()); + + // TODO: b/262269892 Remove the ability to configure restricted transports + // via CarrierConfig + restrictedTransports.addAll( + getRestrictedTransportsFromCarrierConfig(subGrp, lastSnapshot)); + + return restrictedTransports; + } + } + + /** Notifies the VcnManagementService that external dependencies can be set up. */ + public void systemReady() { + mNetworkProvider.register(); + mContext.getSystemService(ConnectivityManager.class) + .registerNetworkCallback( + new NetworkRequest.Builder().clearCapabilities().build(), + mTrackingNetworkCallback); + mTelephonySubscriptionTracker.register(); + } + + // The system server automatically has the required permissions for #getMainUser() + @SuppressLint("AndroidFrameworkRequiresPermission") + private void enforcePrimaryUser() { + final int uid = mDeps.getBinderCallingUid(); + if (uid == Process.SYSTEM_UID) { + throw new IllegalStateException( + "Calling identity was System Server. Was Binder calling identity cleared?"); + } + + final UserHandle userHandle = UserHandle.getUserHandleForUid(uid); + final UserManager userManager = mContext.getSystemService(UserManager.class); + + BinderUtils.withCleanCallingIdentity( + () -> { + if (!Objects.equals(userManager.getMainUser(), userHandle)) { + throw new SecurityException( + "VcnManagementService can only be used by callers running as" + + " the main user"); + } + }); + } + + private void enforceCallingUserAndCarrierPrivilege( + ParcelUuid subscriptionGroup, String pkgName) { + // Only apps running in the primary (system) user are allowed to configure the VCN. This is + // in line with Telephony's behavior with regards to binding to a Carrier App provided + // CarrierConfigService. + enforcePrimaryUser(); + + // TODO (b/172619301): Check based on events propagated from CarrierPrivilegesTracker + final SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class); + final List subscriptionInfos = new ArrayList<>(); + BinderUtils.withCleanCallingIdentity( + () -> { + List subsInGroup = + subMgr.getSubscriptionsInGroup(subscriptionGroup); + if (subsInGroup == null) { + logWtf("Received null from getSubscriptionsInGroup"); + subsInGroup = Collections.emptyList(); + } + subscriptionInfos.addAll(subsInGroup); + }); + + for (SubscriptionInfo info : subscriptionInfos) { + final TelephonyManager telMgr = mContext.getSystemService(TelephonyManager.class) + .createForSubscriptionId(info.getSubscriptionId()); + + // Check subscription is active first; much cheaper/faster check, and an app (currently) + // cannot be carrier privileged for inactive subscriptions. + final int simSlotIndex = info.getSimSlotIndex(); + final boolean isValidSlotIndex = + simSlotIndex >= 0 && simSlotIndex < telMgr.getActiveModemCount(); + if (isValidSlotIndex + && telMgr.checkCarrierPrivilegesForPackage(pkgName) + == TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS) { + // TODO (b/173717728): Allow configuration for inactive, but manageable + // subscriptions. + // TODO (b/173718661): Check for whole subscription groups at a time. + return; + } + } + + throw new SecurityException( + "Carrier privilege required for subscription group to set VCN Config"); + } + + private void enforceManageTestNetworksForTestMode(@NonNull VcnConfig vcnConfig) { + if (vcnConfig.isTestModeProfile()) { + mContext.enforceCallingPermission( + android.Manifest.permission.MANAGE_TEST_NETWORKS, + "Test-mode require the MANAGE_TEST_NETWORKS permission"); + } + } + + private boolean isActiveSubGroup( + @NonNull ParcelUuid subGrp, @NonNull TelephonySubscriptionSnapshot snapshot) { + if (subGrp == null || snapshot == null) { + return false; + } + + return Objects.equals(subGrp, snapshot.getActiveDataSubscriptionGroup()); + } + + private class VcnBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action == null) { + return; + } + + switch (action) { + case Intent.ACTION_PACKAGE_ADDED: // Fallthrough + case Intent.ACTION_PACKAGE_REPLACED: // Fallthrough + case Intent.ACTION_PACKAGE_REMOVED: + // Reevaluate subscriptions + mTelephonySubscriptionTracker.handleSubscriptionsChanged(); + + break; + case Intent.ACTION_PACKAGE_FULLY_REMOVED: + case Intent.ACTION_PACKAGE_DATA_CLEARED: + final String pkgName = intent.getData().getSchemeSpecificPart(); + + if (pkgName == null || pkgName.isEmpty()) { + logWtf("Package name was empty or null for intent with action" + action); + return; + } + + // Clear configs for the packages that had data cleared, or removed. + synchronized (mLock) { + final List toRemove = new ArrayList<>(); + for (Entry entry : mConfigs.entrySet()) { + if (pkgName.equals(entry.getValue().getProvisioningPackageName())) { + toRemove.add(entry.getKey()); + } + } + + for (ParcelUuid subGrp : toRemove) { + stopAndClearVcnConfigInternalLocked(subGrp); + } + + if (!toRemove.isEmpty()) { + writeConfigsToDiskLocked(); + } + } + + break; + default: + Slog.wtf(TAG, "received unexpected intent: " + action); + } + } + } + + private class VcnSubscriptionTrackerCallback implements TelephonySubscriptionTrackerCallback { + /** + * Handles subscription group changes, as notified by {@link TelephonySubscriptionTracker} + * + *

Start any unstarted VCN instances + * + * @hide + */ + public void onNewSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) { + // Startup VCN instances + synchronized (mLock) { + final TelephonySubscriptionSnapshot oldSnapshot = mLastSnapshot; + mLastSnapshot = snapshot; + logInfo("new snapshot: " + mLastSnapshot); + + // Start any VCN instances as necessary + for (Entry entry : mConfigs.entrySet()) { + final ParcelUuid subGrp = entry.getKey(); + + // TODO(b/193687515): Support multiple VCNs active at the same time + if (snapshot.packageHasPermissionsForSubscriptionGroup( + subGrp, entry.getValue().getProvisioningPackageName()) + && isActiveSubGroup(subGrp, snapshot)) { + if (!mVcns.containsKey(subGrp)) { + startVcnLocked(subGrp, entry.getValue()); + } + + // Cancel any scheduled teardowns for active subscriptions + mHandler.removeCallbacksAndMessages(mVcns.get(subGrp)); + } + } + + boolean needNotifyAllPolicyListeners = false; + // Schedule teardown of any VCN instances that have lost carrier privileges (after a + // delay) + for (Entry entry : mVcns.entrySet()) { + final ParcelUuid subGrp = entry.getKey(); + final VcnConfig config = mConfigs.get(subGrp); + + final boolean isActiveSubGrp = isActiveSubGroup(subGrp, snapshot); + final boolean isValidActiveDataSubIdNotInVcnSubGrp = + isValidSubscriptionId(snapshot.getActiveDataSubscriptionId()) + && !isActiveSubGroup(subGrp, snapshot); + + // TODO(b/193687515): Support multiple VCNs active at the same time + if (config == null + || !snapshot.packageHasPermissionsForSubscriptionGroup( + subGrp, config.getProvisioningPackageName()) + || !isActiveSubGrp) { + final ParcelUuid uuidToTeardown = subGrp; + final Vcn instanceToTeardown = entry.getValue(); + + // TODO(b/193687515): Support multiple VCNs active at the same time + // If directly switching to a subscription not in the current group, + // teardown immediately to prevent other subscription's network from being + // outscored by the VCN. Otherwise, teardown after a delay to ensure that + // SIM profile switches do not trigger the VCN to cycle. + final long teardownDelayMs = + isValidActiveDataSubIdNotInVcnSubGrp + ? 0 + : CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS; + mHandler.postDelayed(() -> { + synchronized (mLock) { + // Guard against case where this is run after a old instance was + // torn down, and a new instance was started. Verify to ensure + // correct instance is torn down. This could happen as a result of a + // Carrier App manually removing/adding a VcnConfig. + if (mVcns.get(uuidToTeardown) == instanceToTeardown) { + stopVcnLocked(uuidToTeardown); + + // TODO(b/181789060): invoke asynchronously after Vcn notifies + // through VcnCallback + notifyAllPermissionedStatusCallbacksLocked( + uuidToTeardown, VCN_STATUS_CODE_INACTIVE); + } + } + }, instanceToTeardown, teardownDelayMs); + } else { + // If this VCN's status has not changed, update it with the new snapshot + entry.getValue().updateSubscriptionSnapshot(mLastSnapshot); + needNotifyAllPolicyListeners |= + !Objects.equals( + oldSnapshot.getCarrierConfigForSubGrp(subGrp), + mLastSnapshot.getCarrierConfigForSubGrp(subGrp)); + } + } + + final Map> oldSubGrpMappings = + getSubGroupToSubIdMappings(oldSnapshot); + final Map> currSubGrpMappings = + getSubGroupToSubIdMappings(mLastSnapshot); + if (!currSubGrpMappings.equals(oldSubGrpMappings)) { + garbageCollectAndWriteVcnConfigsLocked(); + needNotifyAllPolicyListeners = true; + } + + if (needNotifyAllPolicyListeners) { + notifyAllPolicyListenersLocked(); + } + } + } + } + + @GuardedBy("mLock") + private Map> getSubGroupToSubIdMappings( + @NonNull TelephonySubscriptionSnapshot snapshot) { + final Map> subGrpMappings = new ArrayMap<>(); + for (ParcelUuid subGrp : mVcns.keySet()) { + subGrpMappings.put(subGrp, snapshot.getAllSubIdsInGroup(subGrp)); + } + return subGrpMappings; + } + + @GuardedBy("mLock") + private void stopVcnLocked(@NonNull ParcelUuid uuidToTeardown) { + logInfo("Stopping VCN config for subGrp: " + uuidToTeardown); + + // Remove in 2 steps. Make sure teardownAsync is triggered before removing from the map. + final Vcn vcnToTeardown = mVcns.get(uuidToTeardown); + if (vcnToTeardown == null) { + return; + } + + vcnToTeardown.teardownAsynchronously(); + mVcns.remove(uuidToTeardown); + + // Now that the VCN is removed, notify all registered listeners to refresh their + // UnderlyingNetworkPolicy. + notifyAllPolicyListenersLocked(); + } + + @GuardedBy("mLock") + private void notifyAllPolicyListenersLocked() { + for (final PolicyListenerBinderDeath policyListener : mRegisteredPolicyListeners.values()) { + BinderUtils.withCleanCallingIdentity(() -> { + try { + policyListener.mListener.onPolicyChanged(); + } catch (RemoteException e) { + logDbg("VcnStatusCallback threw on VCN status change", e); + } + }); + } + } + + @GuardedBy("mLock") + private void notifyAllPermissionedStatusCallbacksLocked( + @NonNull ParcelUuid subGroup, @VcnStatusCode int statusCode) { + for (final VcnStatusCallbackInfo cbInfo : mRegisteredStatusCallbacks.values()) { + if (isCallbackPermissioned(cbInfo, subGroup)) { + BinderUtils.withCleanCallingIdentity(() -> { + try { + cbInfo.mCallback.onVcnStatusChanged(statusCode); + } catch (RemoteException e) { + logDbg("VcnStatusCallback threw on VCN status change", e); + } + }); + } + } + } + + @GuardedBy("mLock") + private void startVcnLocked(@NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + logInfo("Starting VCN config for subGrp: " + subscriptionGroup); + + // TODO(b/193687515): Support multiple VCNs active at the same time + if (!mVcns.isEmpty()) { + // Only one VCN supported at a time; teardown all others before starting new one + for (ParcelUuid uuidToTeardown : mVcns.keySet()) { + stopVcnLocked(uuidToTeardown); + } + } + + final VcnCallbackImpl vcnCallback = new VcnCallbackImpl(subscriptionGroup); + + final VcnContext vcnContext = + mDeps.newVcnContext( + mContext, mLooper, mNetworkProvider, config.isTestModeProfile()); + final Vcn newInstance = + mDeps.newVcn(vcnContext, subscriptionGroup, config, mLastSnapshot, vcnCallback); + mVcns.put(subscriptionGroup, newInstance); + + // Now that a new VCN has started, notify all registered listeners to refresh their + // UnderlyingNetworkPolicy. + notifyAllPolicyListenersLocked(); + + // TODO(b/181789060): invoke asynchronously after Vcn notifies through VcnCallback + notifyAllPermissionedStatusCallbacksLocked(subscriptionGroup, VCN_STATUS_CODE_ACTIVE); + } + + @GuardedBy("mLock") + private void startOrUpdateVcnLocked( + @NonNull ParcelUuid subscriptionGroup, @NonNull VcnConfig config) { + logDbg("Starting or updating VCN config for subGrp: " + subscriptionGroup); + + if (mVcns.containsKey(subscriptionGroup)) { + final Vcn vcn = mVcns.get(subscriptionGroup); + vcn.updateConfig(config); + notifyAllPolicyListenersLocked(); + } else { + // TODO(b/193687515): Support multiple VCNs active at the same time + if (isActiveSubGroup(subscriptionGroup, mLastSnapshot)) { + startVcnLocked(subscriptionGroup, config); + } + } + } + + /** + * Sets a VCN config for a given subscription group. + * + *

Implements the IVcnManagementService Binder interface. + */ + @Override + public void setVcnConfig( + @NonNull ParcelUuid subscriptionGroup, + @NonNull VcnConfig config, + @NonNull String opPkgName) { + requireNonNull(subscriptionGroup, "subscriptionGroup was null"); + requireNonNull(config, "config was null"); + requireNonNull(opPkgName, "opPkgName was null"); + if (!config.getProvisioningPackageName().equals(opPkgName)) { + throw new IllegalArgumentException("Mismatched caller and VcnConfig creator"); + } + logInfo("VCN config updated for subGrp: " + subscriptionGroup); + + mContext.getSystemService(AppOpsManager.class) + .checkPackage(mDeps.getBinderCallingUid(), config.getProvisioningPackageName()); + enforceManageTestNetworksForTestMode(config); + enforceCallingUserAndCarrierPrivilege(subscriptionGroup, opPkgName); + + BinderUtils.withCleanCallingIdentity(() -> { + synchronized (mLock) { + mConfigs.put(subscriptionGroup, config); + startOrUpdateVcnLocked(subscriptionGroup, config); + + writeConfigsToDiskLocked(); + } + }); + } + + private void enforceCarrierPrivilegeOrProvisioningPackage( + @NonNull ParcelUuid subscriptionGroup, @NonNull String pkg) { + // Only apps running in the primary (system) user are allowed to configure the VCN. This is + // in line with Telephony's behavior with regards to binding to a Carrier App provided + // CarrierConfigService. + enforcePrimaryUser(); + + if (isProvisioningPackageForConfig(subscriptionGroup, pkg)) { + return; + } + + // Must NOT be called from cleared binder identity, since this checks user calling identity + enforceCallingUserAndCarrierPrivilege(subscriptionGroup, pkg); + } + + private boolean isProvisioningPackageForConfig( + @NonNull ParcelUuid subscriptionGroup, @NonNull String pkg) { + // Try-finally to return early if matching owned subscription found. + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + final VcnConfig config = mConfigs.get(subscriptionGroup); + if (config != null && pkg.equals(config.getProvisioningPackageName())) { + return true; + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + + return false; + } + + /** + * Clears the VcnManagementService for a given subscription group. + * + *

Implements the IVcnManagementService Binder interface. + */ + @Override + public void clearVcnConfig(@NonNull ParcelUuid subscriptionGroup, @NonNull String opPkgName) { + requireNonNull(subscriptionGroup, "subscriptionGroup was null"); + requireNonNull(opPkgName, "opPkgName was null"); + logInfo("VCN config cleared for subGrp: " + subscriptionGroup); + + mContext.getSystemService(AppOpsManager.class) + .checkPackage(mDeps.getBinderCallingUid(), opPkgName); + enforceCarrierPrivilegeOrProvisioningPackage(subscriptionGroup, opPkgName); + + BinderUtils.withCleanCallingIdentity(() -> { + synchronized (mLock) { + stopAndClearVcnConfigInternalLocked(subscriptionGroup); + writeConfigsToDiskLocked(); + } + }); + } + + private void stopAndClearVcnConfigInternalLocked(@NonNull ParcelUuid subscriptionGroup) { + mConfigs.remove(subscriptionGroup); + final boolean vcnExists = mVcns.containsKey(subscriptionGroup); + + stopVcnLocked(subscriptionGroup); + + if (vcnExists) { + // TODO(b/181789060): invoke asynchronously after Vcn notifies through + // VcnCallback + notifyAllPermissionedStatusCallbacksLocked( + subscriptionGroup, VCN_STATUS_CODE_NOT_CONFIGURED); + } + } + + private void garbageCollectAndWriteVcnConfigsLocked() { + final SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class); + final Set subGroups = mLastSnapshot.getAllSubscriptionGroups(); + + boolean shouldWrite = false; + + final Iterator configsIterator = mConfigs.keySet().iterator(); + while (configsIterator.hasNext()) { + final ParcelUuid subGrp = configsIterator.next(); + + if (!subGroups.contains(subGrp)) { + // Trim subGrps with no more subscriptions; must have moved to another subGrp + logDbg("Garbage collect VcnConfig for group=" + subGrp); + configsIterator.remove(); + shouldWrite = true; + } + } + + if (shouldWrite) { + writeConfigsToDiskLocked(); + } + } + + /** + * Retrieves the list of subscription groups with configured VcnConfigs + * + *

Limited to subscription groups for which the caller had configured. + * + *

Implements the IVcnManagementService Binder interface. + */ + @Override + @NonNull + public List getConfiguredSubscriptionGroups(@NonNull String opPkgName) { + requireNonNull(opPkgName, "opPkgName was null"); + + mContext.getSystemService(AppOpsManager.class) + .checkPackage(mDeps.getBinderCallingUid(), opPkgName); + enforcePrimaryUser(); + + final List result = new ArrayList<>(); + synchronized (mLock) { + for (ParcelUuid subGrp : mConfigs.keySet()) { + if (mLastSnapshot.packageHasPermissionsForSubscriptionGroup(subGrp, opPkgName) + || isProvisioningPackageForConfig(subGrp, opPkgName)) { + result.add(subGrp); + } + } + } + + return result; + } + + @GuardedBy("mLock") + private void writeConfigsToDiskLocked() { + try { + PersistableBundle bundle = + PersistableBundleUtils.fromMap( + mConfigs, + PersistableBundleUtils::fromParcelUuid, + VcnConfig::toPersistableBundle); + mConfigDiskRwHelper.writeToDisk(bundle); + } catch (IOException e) { + logErr("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); + } + } + + /** Get current VCNs for testing purposes */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Map getAllVcns() { + synchronized (mLock) { + return Collections.unmodifiableMap(mVcns); + } + } + + /** Get current VcnStatusCallbacks for testing purposes. */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Map getAllStatusCallbacks() { + synchronized (mLock) { + return Collections.unmodifiableMap(mRegisteredStatusCallbacks); + } + } + + /** Binder death recipient used to remove a registered policy listener. */ + private class PolicyListenerBinderDeath implements Binder.DeathRecipient { + @NonNull private final IVcnUnderlyingNetworkPolicyListener mListener; + + PolicyListenerBinderDeath(@NonNull IVcnUnderlyingNetworkPolicyListener listener) { + mListener = listener; + } + + @Override + public void binderDied() { + Log.e(TAG, "app died without removing VcnUnderlyingNetworkPolicyListener"); + removeVcnUnderlyingNetworkPolicyListener(mListener); + } + } + + /** Adds the provided listener for receiving VcnUnderlyingNetworkPolicy updates. */ + @Override + public void addVcnUnderlyingNetworkPolicyListener( + @NonNull IVcnUnderlyingNetworkPolicyListener listener) { + requireNonNull(listener, "listener was null"); + + PermissionUtils.enforceAnyPermissionOf( + mContext, + android.Manifest.permission.NETWORK_FACTORY, + android.Manifest.permission.MANAGE_TEST_NETWORKS); + + BinderUtils.withCleanCallingIdentity(() -> { + PolicyListenerBinderDeath listenerBinderDeath = new PolicyListenerBinderDeath(listener); + + synchronized (mLock) { + mRegisteredPolicyListeners.put(listener.asBinder(), listenerBinderDeath); + + try { + listener.asBinder().linkToDeath(listenerBinderDeath, 0 /* flags */); + } catch (RemoteException e) { + // Remote binder already died - cleanup registered Listener + listenerBinderDeath.binderDied(); + } + } + }); + } + + /** Removes the provided listener from receiving VcnUnderlyingNetworkPolicy updates. */ + @Override + public void removeVcnUnderlyingNetworkPolicyListener( + @NonNull IVcnUnderlyingNetworkPolicyListener listener) { + requireNonNull(listener, "listener was null"); + + PermissionUtils.enforceAnyPermissionOf( + mContext, + android.Manifest.permission.NETWORK_FACTORY, + android.Manifest.permission.MANAGE_TEST_NETWORKS); + + BinderUtils.withCleanCallingIdentity(() -> { + synchronized (mLock) { + PolicyListenerBinderDeath listenerBinderDeath = + mRegisteredPolicyListeners.remove(listener.asBinder()); + + if (listenerBinderDeath != null) { + listener.asBinder().unlinkToDeath(listenerBinderDeath, 0 /* flags */); + } + } + }); + } + + private ParcelUuid getSubGroupForNetworkCapabilities( + @NonNull NetworkCapabilities networkCapabilities) { + ParcelUuid subGrp = null; + final TelephonySubscriptionSnapshot snapshot; + + // Always access mLastSnapshot under lock. Technically this can be treated as a volatile + // but for consistency and safety, always access under lock. + synchronized (mLock) { + snapshot = mLastSnapshot; + } + + // If multiple subscription IDs exist, they MUST all point to the same subscription + // group. Otherwise undefined behavior may occur. + for (int subId : networkCapabilities.getSubscriptionIds()) { + // Verify that all subscriptions point to the same group + if (subGrp != null && !subGrp.equals(snapshot.getGroupForSubId(subId))) { + logWtf("Got multiple subscription groups for a single network"); + } + + subGrp = snapshot.getGroupForSubId(subId); + } + + return subGrp; + } + + /** + * Gets the UnderlyingNetworkPolicy as determined by the provided NetworkCapabilities and + * LinkProperties. + */ + @NonNull + @Override + public VcnUnderlyingNetworkPolicy getUnderlyingNetworkPolicy( + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties) { + requireNonNull(networkCapabilities, "networkCapabilities was null"); + requireNonNull(linkProperties, "linkProperties was null"); + + PermissionUtils.enforceAnyPermissionOf( + mContext, + android.Manifest.permission.NETWORK_FACTORY, + android.Manifest.permission.MANAGE_TEST_NETWORKS); + + final boolean isUsingManageTestNetworks = + mContext.checkCallingOrSelfPermission(android.Manifest.permission.NETWORK_FACTORY) + != PackageManager.PERMISSION_GRANTED; + + if (isUsingManageTestNetworks && !networkCapabilities.hasTransport(TRANSPORT_TEST)) { + throw new IllegalStateException( + "NetworkCapabilities must be for Test Network if using permission" + + " MANAGE_TEST_NETWORKS"); + } + + return BinderUtils.withCleanCallingIdentity(() -> { + // Defensive copy in case this call is in-process and the given NetworkCapabilities + // mutates + final NetworkCapabilities ncCopy = new NetworkCapabilities(networkCapabilities); + + final ParcelUuid subGrp = getSubGroupForNetworkCapabilities(ncCopy); + boolean isVcnManagedNetwork = false; + boolean isRestricted = false; + synchronized (mLock) { + final Vcn vcn = mVcns.get(subGrp); + final VcnConfig vcnConfig = mConfigs.get(subGrp); + if (vcn != null && vcnConfig != null) { + if (vcn.getStatus() == VCN_STATUS_CODE_ACTIVE) { + isVcnManagedNetwork = true; + } + + final Set restrictedTransports = mDeps.getRestrictedTransports( + subGrp, mLastSnapshot, vcnConfig); + for (int restrictedTransport : restrictedTransports) { + if (ncCopy.hasTransport(restrictedTransport)) { + if (restrictedTransport == TRANSPORT_CELLULAR + || restrictedTransport == TRANSPORT_TEST) { + // For cell or test network, only mark it as restricted when + // the VCN is in active mode. + isRestricted |= (vcn.getStatus() == VCN_STATUS_CODE_ACTIVE); + } else { + isRestricted = true; + break; + } + } + } + } else if (vcn != null && vcnConfig == null) { + logWtf("Vcn instance exists but VcnConfig does not for " + subGrp); + } + } + + final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder(ncCopy); + + if (isVcnManagedNetwork) { + ncBuilder.removeCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } else { + ncBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } + + if (isRestricted) { + ncBuilder.removeCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + } + + final NetworkCapabilities result = ncBuilder.build(); + final VcnUnderlyingNetworkPolicy policy = new VcnUnderlyingNetworkPolicy( + mTrackingNetworkCallback + .requiresRestartForImmutableCapabilityChanges(result, linkProperties), + result); + + logVdbg("getUnderlyingNetworkPolicy() called for caps: " + networkCapabilities + + "; and lp: " + linkProperties + "; result = " + policy); + return policy; + }); + } + + /** Binder death recipient used to remove registered VcnStatusCallbacks. */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + class VcnStatusCallbackInfo implements Binder.DeathRecipient { + @NonNull final ParcelUuid mSubGroup; + @NonNull final IVcnStatusCallback mCallback; + @NonNull final String mPkgName; + final int mUid; + + private VcnStatusCallbackInfo( + @NonNull ParcelUuid subGroup, + @NonNull IVcnStatusCallback callback, + @NonNull String pkgName, + int uid) { + mSubGroup = subGroup; + mCallback = callback; + mPkgName = pkgName; + mUid = uid; + } + + @Override + public void binderDied() { + Log.e(TAG, "app died without unregistering VcnStatusCallback"); + unregisterVcnStatusCallback(mCallback); + } + } + + private boolean isCallbackPermissioned( + @NonNull VcnStatusCallbackInfo cbInfo, @NonNull ParcelUuid subgroup) { + if (!subgroup.equals(cbInfo.mSubGroup)) { + return false; + } + + if (!mLastSnapshot.packageHasPermissionsForSubscriptionGroup(subgroup, cbInfo.mPkgName)) { + return false; + } + + return true; + } + + /** Registers the provided callback for receiving VCN status updates. */ + @Override + public void registerVcnStatusCallback( + @NonNull ParcelUuid subGroup, + @NonNull IVcnStatusCallback callback, + @NonNull String opPkgName) { + final int callingUid = mDeps.getBinderCallingUid(); + final long identity = Binder.clearCallingIdentity(); + try { + requireNonNull(subGroup, "subGroup must not be null"); + requireNonNull(callback, "callback must not be null"); + requireNonNull(opPkgName, "opPkgName must not be null"); + + mContext.getSystemService(AppOpsManager.class).checkPackage(callingUid, opPkgName); + + final IBinder cbBinder = callback.asBinder(); + final VcnStatusCallbackInfo cbInfo = + new VcnStatusCallbackInfo(subGroup, callback, opPkgName, callingUid); + + try { + cbBinder.linkToDeath(cbInfo, 0 /* flags */); + } catch (RemoteException e) { + // Remote binder already died - don't add to mRegisteredStatusCallbacks and exit + return; + } + + synchronized (mLock) { + if (mRegisteredStatusCallbacks.containsKey(cbBinder)) { + throw new IllegalStateException( + "Attempting to register a callback that is already in use"); + } + + mRegisteredStatusCallbacks.put(cbBinder, cbInfo); + + // now that callback is registered, send it the VCN's current status + final VcnConfig vcnConfig = mConfigs.get(subGroup); + final Vcn vcn = mVcns.get(subGroup); + final int vcnStatus = + vcn == null ? VCN_STATUS_CODE_NOT_CONFIGURED : vcn.getStatus(); + final int resultStatus; + if (vcnConfig == null || !isCallbackPermissioned(cbInfo, subGroup)) { + resultStatus = VCN_STATUS_CODE_NOT_CONFIGURED; + } else if (vcn == null) { + resultStatus = VCN_STATUS_CODE_INACTIVE; + } else if (vcnStatus == VCN_STATUS_CODE_ACTIVE + || vcnStatus == VCN_STATUS_CODE_SAFE_MODE) { + resultStatus = vcnStatus; + } else { + logWtf("Unknown VCN status: " + vcnStatus); + resultStatus = VCN_STATUS_CODE_NOT_CONFIGURED; + } + + try { + cbInfo.mCallback.onVcnStatusChanged(resultStatus); + } catch (RemoteException e) { + logDbg("VcnStatusCallback threw on VCN status change", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + /** Unregisters the provided callback from receiving future VCN status updates. */ + @Override + public void unregisterVcnStatusCallback(@NonNull IVcnStatusCallback callback) { + final long identity = Binder.clearCallingIdentity(); + try { + requireNonNull(callback, "callback must not be null"); + + final IBinder cbBinder = callback.asBinder(); + synchronized (mLock) { + VcnStatusCallbackInfo cbInfo = mRegisteredStatusCallbacks.remove(cbBinder); + + if (cbInfo != null) { + cbBinder.unlinkToDeath(cbInfo, 0 /* flags */); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + void setLastSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) { + mLastSnapshot = Objects.requireNonNull(snapshot); + } + + private void logVdbg(String msg) { + if (VDBG) { + Slog.v(TAG, msg); + } + } + + private void logDbg(String msg) { + Slog.d(TAG, msg); + } + + private void logDbg(String msg, Throwable tr) { + Slog.d(TAG, msg, tr); + } + + private void logInfo(String msg) { + Slog.i(TAG, msg); + LOCAL_LOG.log("[INFO] [" + TAG + "] " + msg); + } + + private void logInfo(String msg, Throwable tr) { + Slog.i(TAG, msg, tr); + LOCAL_LOG.log("[INFO] [" + TAG + "] " + msg + tr); + } + + private void logErr(String msg) { + Slog.e(TAG, msg); + LOCAL_LOG.log("[ERR] [" + TAG + "] " + msg); + } + + private void logErr(String msg, Throwable tr) { + Slog.e(TAG, msg, tr); + LOCAL_LOG.log("[ERR ] [" + TAG + "] " + msg + tr); + } + + private void logWtf(String msg) { + Slog.wtf(TAG, msg); + LOCAL_LOG.log("[WTF] [" + TAG + "] " + msg); + } + + private void logWtf(String msg, Throwable tr) { + Slog.wtf(TAG, msg, tr); + LOCAL_LOG.log("[WTF ] [" + TAG + "] " + msg + tr); + } + + /** + * 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, "| "); + + // Post to handler thread to prevent ConcurrentModificationExceptions, and avoid lock-hell. + HandlerUtils.runWithScissorsForDump( + mHandler, + () -> { + mNetworkProvider.dump(pw); + pw.println(); + + mTrackingNetworkCallback.dump(pw); + pw.println(); + + synchronized (mLock) { + mLastSnapshot.dump(pw); + pw.println(); + + pw.println("mConfigs:"); + pw.increaseIndent(); + for (Entry entry : mConfigs.entrySet()) { + pw.println( + entry.getKey() + + ": " + + entry.getValue().getProvisioningPackageName()); + } + pw.decreaseIndent(); + pw.println(); + + pw.println("mVcns:"); + pw.increaseIndent(); + for (Vcn vcn : mVcns.values()) { + vcn.dump(pw); + } + pw.decreaseIndent(); + pw.println(); + } + + pw.println("Local log:"); + pw.increaseIndent(); + LOCAL_LOG.dump(pw); + pw.decreaseIndent(); + pw.println(); + }, + DUMP_TIMEOUT_MILLIS); + } + + // TODO(b/180452282): Make name more generic and implement directly with VcnManagementService + /** Callback for Vcn signals sent up to VcnManagementService. */ + public interface VcnCallback { + /** Called by a Vcn to signal that its safe mode status has changed. */ + void onSafeModeStatusChanged(boolean isInSafeMode); + + /** Called by a Vcn to signal that an error occurred. */ + void onGatewayConnectionError( + @NonNull String gatewayConnectionName, + @VcnErrorCode int errorCode, + @Nullable String exceptionClass, + @Nullable String exceptionMessage); + } + + /** + * TrackingNetworkCallback tracks all active networks + * + *

This is used to ensure that no underlying networks have immutable capabilities changed + * without requiring a Network restart. + */ + private class TrackingNetworkCallback extends ConnectivityManager.NetworkCallback { + private final Object mLockObject = new Object(); + private final Map mCaps = new ArrayMap<>(); + private final Map mLinkProperties = new ArrayMap<>(); + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities caps) { + synchronized (mLockObject) { + mCaps.put(network, caps); + } + } + + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties lp) { + synchronized (mLockObject) { + mLinkProperties.put(network, lp); + } + } + + @Override + public void onLost(Network network) { + synchronized (mLockObject) { + mCaps.remove(network); + mLinkProperties.remove(network); + } + } + + private Set getNonTestTransportTypes(NetworkCapabilities caps) { + final Set transportTypes = new ArraySet<>(); + for (int t : caps.getTransportTypes()) { + transportTypes.add(t); + } + return transportTypes; + } + + private boolean hasSameTransportsAndCapabilities( + NetworkCapabilities caps, NetworkCapabilities capsOther) { + if (!Objects.equals( + getNonTestTransportTypes(caps), getNonTestTransportTypes(capsOther))) { + return false; + } + + for (int capability : ALLOWED_CAPABILITIES) { + if (caps.hasCapability(capability) != capsOther.hasCapability(capability)) { + return false; + } + } + return true; + } + + private boolean requiresRestartForImmutableCapabilityChanges( + NetworkCapabilities caps, LinkProperties lp) { + if (caps.getSubscriptionIds() == null) { + return false; + } + + synchronized (mLockObject) { + // Search for an existing network (using interfce names) + // TODO: Get network from NetworkFactory (if exists) for this match. + for (Entry lpEntry : mLinkProperties.entrySet()) { + if (lp.getInterfaceName() != null + && !lp.getInterfaceName().isEmpty() + && Objects.equals( + lp.getInterfaceName(), lpEntry.getValue().getInterfaceName())) { + return mCaps.get(lpEntry.getKey()) + .hasCapability(NET_CAPABILITY_NOT_RESTRICTED) + != caps.hasCapability(NET_CAPABILITY_NOT_RESTRICTED); + } + } + } + + // If no network found, by definition does not need restart. + return false; + } + + /** Dumps the state of this snapshot for logging and debugging purposes. */ + public void dump(IndentingPrintWriter pw) { + pw.println("TrackingNetworkCallback:"); + pw.increaseIndent(); + + pw.println("mCaps:"); + pw.increaseIndent(); + synchronized (mCaps) { + for (Entry entry : mCaps.entrySet()) { + pw.println(entry.getKey() + ": " + entry.getValue()); + } + } + pw.decreaseIndent(); + pw.println(); + + pw.decreaseIndent(); + } + } + + /** VcnCallbackImpl for Vcn signals sent up to VcnManagementService. */ + private class VcnCallbackImpl implements VcnCallback { + @NonNull private final ParcelUuid mSubGroup; + + private VcnCallbackImpl(@NonNull final ParcelUuid subGroup) { + mSubGroup = Objects.requireNonNull(subGroup, "Missing subGroup"); + } + + @Override + public void onSafeModeStatusChanged(boolean isInSafeMode) { + synchronized (mLock) { + // Ignore if this subscription group doesn't exist anymore + if (!mVcns.containsKey(mSubGroup)) { + return; + } + + final int status = + isInSafeMode ? VCN_STATUS_CODE_SAFE_MODE : VCN_STATUS_CODE_ACTIVE; + + notifyAllPolicyListenersLocked(); + notifyAllPermissionedStatusCallbacksLocked(mSubGroup, status); + } + } + + @Override + public void onGatewayConnectionError( + @NonNull String gatewayConnectionName, + @VcnErrorCode int errorCode, + @Nullable String exceptionClass, + @Nullable String exceptionMessage) { + synchronized (mLock) { + // Ignore if this subscription group doesn't exist anymore + if (!mVcns.containsKey(mSubGroup)) { + return; + } + + // Notify all registered StatusCallbacks for this subGroup + for (VcnStatusCallbackInfo cbInfo : mRegisteredStatusCallbacks.values()) { + if (isCallbackPermissioned(cbInfo, mSubGroup)) { + BinderUtils.withCleanCallingIdentity(() -> { + try { + cbInfo.mCallback.onGatewayConnectionError( + gatewayConnectionName, + errorCode, + exceptionClass, + exceptionMessage); + } catch (RemoteException e) { + logDbg("VcnStatusCallback threw on VCN status change", e); + } + }); + } + } + } + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/Placeholder.java b/Vcn/service-b/src/com/android/server/vcn/Placeholder.java new file mode 100644 index 0000000000..e79914531c --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/Placeholder.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 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; + +/** + * Placeholder class so new service-vcn isn't empty + * + * @hide + */ +// This class will be removed once source code is migrated +public class Placeholder {} diff --git a/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java b/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java new file mode 100644 index 0000000000..cedb2d1680 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/TelephonySubscriptionTracker.java @@ -0,0 +1,602 @@ +/* + * 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.telephony.SubscriptionManager.INVALID_SIM_SLOT_INDEX; +import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; +import static android.telephony.TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.vcn.VcnManager; +import android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; +import android.os.Build; +import android.os.Handler; +import android.os.ParcelUuid; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.telephony.TelephonyManager.CarrierPrivilegesCallback; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.modules.utils.HandlerExecutor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +/** + * TelephonySubscriptionTracker provides a caching layer for tracking active subscription groups. + * + *

This class performs two roles: + * + *

    + *
  1. De-noises subscription changes by ensuring that only changes in active and ready + * subscription groups are acted upon + *
  2. Caches mapping between subIds and subscription groups + *
+ * + *

An subscription group is active and ready if any of its contained subIds has had BOTH the + * {@link CarrierConfigManager#isConfigForIdentifiedCarrier()} return true, AND the subscription is + * listed as active per SubscriptionManager#getAllSubscriptionInfoList(). + * + *

Note that due to the asynchronous nature of callbacks and broadcasts, the output of this class + * is (only) eventually consistent. + * + * @hide + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class TelephonySubscriptionTracker extends BroadcastReceiver { + @NonNull private static final String TAG = TelephonySubscriptionTracker.class.getSimpleName(); + private static final boolean LOG_DBG = false; // STOPSHIP if true + + @NonNull private final Context mContext; + @NonNull private final Handler mHandler; + @NonNull private final TelephonySubscriptionTrackerCallback mCallback; + @NonNull private final Dependencies mDeps; + + @NonNull private final TelephonyManager mTelephonyManager; + @NonNull private final SubscriptionManager mSubscriptionManager; + @Nullable private final CarrierConfigManager mCarrierConfigManager; + + @NonNull private final ActiveDataSubscriptionIdListener mActiveDataSubIdListener; + + // TODO (Android T+): Add ability to handle multiple subIds per slot. + @NonNull private final Map mReadySubIdsBySlotId = new HashMap<>(); + + @NonNull + private final Map mSubIdToCarrierConfigMap = new HashMap<>(); + + @NonNull private final OnSubscriptionsChangedListener mSubscriptionChangedListener; + + @NonNull + private final List mCarrierPrivilegesCallbacks = new ArrayList<>(); + + @NonNull private TelephonySubscriptionSnapshot mCurrentSnapshot; + + @NonNull + private final CarrierConfigManager.CarrierConfigChangeListener mCarrierConfigChangeListener = + (int logicalSlotIndex, int subscriptionId, int carrierId, int specificCarrierId) -> + handleActionCarrierConfigChanged(logicalSlotIndex, subscriptionId); + + + public TelephonySubscriptionTracker( + @NonNull Context context, + @NonNull Handler handler, + @NonNull TelephonySubscriptionTrackerCallback callback) { + this(context, handler, callback, new Dependencies()); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + TelephonySubscriptionTracker( + @NonNull Context context, + @NonNull Handler handler, + @NonNull TelephonySubscriptionTrackerCallback callback, + @NonNull Dependencies deps) { + mContext = Objects.requireNonNull(context, "Missing context"); + mHandler = Objects.requireNonNull(handler, "Missing handler"); + mCallback = Objects.requireNonNull(callback, "Missing callback"); + mDeps = Objects.requireNonNull(deps, "Missing deps"); + + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class); + mActiveDataSubIdListener = new ActiveDataSubscriptionIdListener(); + + mSubscriptionChangedListener = + new OnSubscriptionsChangedListener() { + @Override + public void onSubscriptionsChanged() { + handleSubscriptionsChanged(); + } + }; + } + + /** + * Registers the receivers, and starts tracking subscriptions. + * + *

Must always be run on the VcnManagementService thread. + */ + public void register() { + final HandlerExecutor executor = new HandlerExecutor(mHandler); + final IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_MULTI_SIM_CONFIG_CHANGED); + + mContext.registerReceiver(this, filter, null, mHandler); + mSubscriptionManager.addOnSubscriptionsChangedListener( + executor, mSubscriptionChangedListener); + mTelephonyManager.registerTelephonyCallback(executor, mActiveDataSubIdListener); + if (mCarrierConfigManager != null) { + mCarrierConfigManager.registerCarrierConfigChangeListener(executor, + mCarrierConfigChangeListener); + } + + registerCarrierPrivilegesCallbacks(); + } + + private void registerCarrierPrivilegesCallbacks() { + final HandlerExecutor executor = new HandlerExecutor(mHandler); + final int modemCount = mTelephonyManager.getActiveModemCount(); + try { + for (int i = 0; i < modemCount; i++) { + CarrierPrivilegesCallback carrierPrivilegesCallback = + new CarrierPrivilegesCallback() { + @Override + public void onCarrierPrivilegesChanged( + @NonNull Set privilegedPackageNames, + @NonNull Set privilegedUids) { + // Re-trigger the synchronous check (which is also very cheap due + // to caching in CarrierPrivilegesTracker). This allows consistency + // with the onSubscriptionsChangedListener and broadcasts. + handleSubscriptionsChanged(); + } + }; + + mTelephonyManager.registerCarrierPrivilegesCallback( + i, executor, carrierPrivilegesCallback); + mCarrierPrivilegesCallbacks.add(carrierPrivilegesCallback); + } + } catch (IllegalArgumentException e) { + Slog.wtf(TAG, "Encounted exception registering carrier privileges listeners", e); + } + } + + /** + * Unregisters the receivers, and stops tracking subscriptions. + * + *

Must always be run on the VcnManagementService thread. + */ + public void unregister() { + mContext.unregisterReceiver(this); + mSubscriptionManager.removeOnSubscriptionsChangedListener(mSubscriptionChangedListener); + mTelephonyManager.unregisterTelephonyCallback(mActiveDataSubIdListener); + if (mCarrierConfigManager != null) { + mCarrierConfigManager.unregisterCarrierConfigChangeListener( + mCarrierConfigChangeListener); + } + + unregisterCarrierPrivilegesCallbacks(); + } + + private void unregisterCarrierPrivilegesCallbacks() { + for (CarrierPrivilegesCallback carrierPrivilegesCallback : + mCarrierPrivilegesCallbacks) { + mTelephonyManager.unregisterCarrierPrivilegesCallback(carrierPrivilegesCallback); + } + mCarrierPrivilegesCallbacks.clear(); + } + + /** + * Handles subscription changes, correlating available subscriptions and loaded carrier configs + * + *

The subscription change listener is registered with a HandlerExecutor backed by mHandler, + * so callbacks & broadcasts are all serialized on mHandler, avoiding the need for locking. + */ + public void handleSubscriptionsChanged() { + final Map> privilegedPackages = new HashMap<>(); + final Map newSubIdToInfoMap = new HashMap<>(); + + final List allSubs = mSubscriptionManager.getAllSubscriptionInfoList(); + if (allSubs == null) { + return; // Telephony crashed; no way to verify subscriptions. + } + + // If allSubs is empty, no subscriptions exist. Cache will be cleared by virtue of no active + // subscriptions + for (SubscriptionInfo subInfo : allSubs) { + if (subInfo.getGroupUuid() == null) { + continue; + } + + // Build subId -> subGrp cache + newSubIdToInfoMap.put(subInfo.getSubscriptionId(), subInfo); + + // Update subscription groups that are both ready, and active. For a group to be + // considered active, both of the following must be true: + // + // 1. A final CARRIER_CONFIG_CHANGED (where config is for an identified carrier) + // broadcast must have been received for the subId + // 2. A active subscription (is loaded into a SIM slot) must be part of the subscription + // group. + if (subInfo.getSimSlotIndex() != INVALID_SIM_SLOT_INDEX + && mReadySubIdsBySlotId.values().contains(subInfo.getSubscriptionId())) { + final TelephonyManager subIdSpecificTelephonyManager = + mTelephonyManager.createForSubscriptionId(subInfo.getSubscriptionId()); + + final ParcelUuid subGroup = subInfo.getGroupUuid(); + final Set pkgs = + privilegedPackages.getOrDefault(subGroup, new ArraySet<>()); + pkgs.addAll(subIdSpecificTelephonyManager.getPackagesWithCarrierPrivileges()); + + privilegedPackages.put(subGroup, pkgs); + } + } + + final TelephonySubscriptionSnapshot newSnapshot = + new TelephonySubscriptionSnapshot( + mDeps.getActiveDataSubscriptionId(), + newSubIdToInfoMap, + mSubIdToCarrierConfigMap, + privilegedPackages); + + // If snapshot was meaningfully updated, fire the callback + if (!newSnapshot.equals(mCurrentSnapshot)) { + mCurrentSnapshot = newSnapshot; + mHandler.post( + () -> { + mCallback.onNewSnapshot(newSnapshot); + }); + } + } + + /** + * Broadcast receiver for ACTION_MULTI_SIM_CONFIG_CHANGED + * + *

The broadcast receiver is registered with mHandler, so callbacks & broadcasts are all + * serialized on mHandler, avoiding the need for locking. + */ + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case ACTION_MULTI_SIM_CONFIG_CHANGED: + handleActionMultiSimConfigChanged(context, intent); + break; + default: + Slog.v(TAG, "Unknown intent received with action: " + intent.getAction()); + } + } + + private void handleActionMultiSimConfigChanged(Context context, Intent intent) { + unregisterCarrierPrivilegesCallbacks(); + + // Clear invalid slotIds from the mReadySubIdsBySlotId map. + final int modemCount = mTelephonyManager.getActiveModemCount(); + final Iterator slotIdIterator = mReadySubIdsBySlotId.keySet().iterator(); + while (slotIdIterator.hasNext()) { + final int slotId = slotIdIterator.next(); + + if (slotId >= modemCount) { + slotIdIterator.remove(); + } + } + + registerCarrierPrivilegesCallbacks(); + handleSubscriptionsChanged(); + } + + private void handleActionCarrierConfigChanged(int slotId, int subId) { + if (slotId == INVALID_SIM_SLOT_INDEX) { + return; + } + + if (SubscriptionManager.isValidSubscriptionId(subId)) { + // Get only configs as needed to save memory. + PersistableBundle carrierConfig = new PersistableBundle(); + try { + carrierConfig = + mCarrierConfigManager.getConfigForSubId( + subId, VcnManager.VCN_RELATED_CARRIER_CONFIG_KEYS); + + } catch (RuntimeException exception) { + Slog.w(TAG, "CarrierConfigLoader is not available."); + } + + if (mDeps.isConfigForIdentifiedCarrier(carrierConfig)) { + mReadySubIdsBySlotId.put(slotId, subId); + + if (!carrierConfig.isEmpty()) { + mSubIdToCarrierConfigMap.put(subId, + new PersistableBundleWrapper(carrierConfig)); + } + handleSubscriptionsChanged(); + } + } else { + final Integer oldSubid = mReadySubIdsBySlotId.remove(slotId); + if (oldSubid != null) { + mSubIdToCarrierConfigMap.remove(oldSubid); + } + handleSubscriptionsChanged(); + } + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + void setReadySubIdsBySlotId(Map readySubIdsBySlotId) { + mReadySubIdsBySlotId.clear(); + mReadySubIdsBySlotId.putAll(readySubIdsBySlotId); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + void setSubIdToCarrierConfigMap( + Map subIdToCarrierConfigMap) { + mSubIdToCarrierConfigMap.clear(); + mSubIdToCarrierConfigMap.putAll(subIdToCarrierConfigMap); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + Map getReadySubIdsBySlotId() { + return Collections.unmodifiableMap(mReadySubIdsBySlotId); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + Map getSubIdToCarrierConfigMap() { + return Collections.unmodifiableMap(mSubIdToCarrierConfigMap); + } + + /** TelephonySubscriptionSnapshot is a class containing info about active subscriptions */ + public static class TelephonySubscriptionSnapshot { + private final int mActiveDataSubId; + private final Map mSubIdToInfoMap; + private final Map mSubIdToCarrierConfigMap; + private final Map> mPrivilegedPackages; + + public static final TelephonySubscriptionSnapshot EMPTY_SNAPSHOT = + new TelephonySubscriptionSnapshot( + INVALID_SUBSCRIPTION_ID, + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap()); + + @VisibleForTesting(visibility = Visibility.PRIVATE) + TelephonySubscriptionSnapshot( + int activeDataSubId, + @NonNull Map subIdToInfoMap, + @NonNull Map subIdToCarrierConfigMap, + @NonNull Map> privilegedPackages) { + mActiveDataSubId = activeDataSubId; + Objects.requireNonNull(subIdToInfoMap, "subIdToInfoMap was null"); + Objects.requireNonNull(privilegedPackages, "privilegedPackages was null"); + Objects.requireNonNull(subIdToCarrierConfigMap, "subIdToCarrierConfigMap was null"); + + mSubIdToInfoMap = + Collections.unmodifiableMap( + new HashMap(subIdToInfoMap)); + mSubIdToCarrierConfigMap = + Collections.unmodifiableMap( + new HashMap( + subIdToCarrierConfigMap)); + + final Map> unmodifiableInnerSets = new ArrayMap<>(); + for (Entry> entry : privilegedPackages.entrySet()) { + unmodifiableInnerSets.put( + entry.getKey(), Collections.unmodifiableSet(entry.getValue())); + } + mPrivilegedPackages = Collections.unmodifiableMap(unmodifiableInnerSets); + } + + /** Returns the active subscription ID. May be INVALID_SUBSCRIPTION_ID */ + public int getActiveDataSubscriptionId() { + return mActiveDataSubId; + } + + /** Returns the active subscription group */ + @Nullable + public ParcelUuid getActiveDataSubscriptionGroup() { + final SubscriptionInfo info = mSubIdToInfoMap.get(getActiveDataSubscriptionId()); + if (info == null) { + return null; + } + + return info.getGroupUuid(); + } + + /** Returns the active subscription groups */ + @NonNull + public Set getActiveSubscriptionGroups() { + return mPrivilegedPackages.keySet(); + } + + /** Returns all subscription groups */ + @NonNull + public Set getAllSubscriptionGroups() { + final Set subGroups = new ArraySet<>(); + for (SubscriptionInfo subInfo : mSubIdToInfoMap.values()) { + subGroups.add(subInfo.getGroupUuid()); + } + + return subGroups; + } + + /** Checks if the provided package is carrier privileged for the specified sub group. */ + public boolean packageHasPermissionsForSubscriptionGroup( + @NonNull ParcelUuid subGrp, @NonNull String packageName) { + final Set privilegedPackages = mPrivilegedPackages.get(subGrp); + + return privilegedPackages != null && privilegedPackages.contains(packageName); + } + + /** Returns the Subscription Group for a given subId. */ + @Nullable + public ParcelUuid getGroupForSubId(int subId) { + return mSubIdToInfoMap.containsKey(subId) + ? mSubIdToInfoMap.get(subId).getGroupUuid() + : null; + } + + /** + * Returns all the subIds in a given group, including available, but inactive subscriptions. + */ + @NonNull + public Set getAllSubIdsInGroup(ParcelUuid subGrp) { + final Set subIds = new ArraySet<>(); + + for (Entry entry : mSubIdToInfoMap.entrySet()) { + if (subGrp.equals(entry.getValue().getGroupUuid())) { + subIds.add(entry.getKey()); + } + } + + return subIds; + } + + /** Checks if the requested subscription is opportunistic */ + @NonNull + public boolean isOpportunistic(int subId) { + return mSubIdToInfoMap.containsKey(subId) + ? mSubIdToInfoMap.get(subId).isOpportunistic() + : false; + } + + /** + * Retrieves a carrier config for a subscription in the provided group. + * + *

This method will prioritize non-opportunistic subscriptions, but will use the a + * carrier config for an opportunistic subscription if no other subscriptions are found. + */ + @Nullable + public PersistableBundleWrapper getCarrierConfigForSubGrp(@NonNull ParcelUuid subGrp) { + PersistableBundleWrapper result = null; + + for (int subId : getAllSubIdsInGroup(subGrp)) { + final PersistableBundleWrapper config = mSubIdToCarrierConfigMap.get(subId); + if (config != null) { + result = config; + + // Attempt to use (any) non-opportunistic subscription. If this subscription is + // opportunistic, continue and try to find a non-opportunistic subscription, + // using the opportunistic ones as a last resort. + if (!isOpportunistic(subId)) { + return config; + } + } + } + + return result; + } + + @Override + public int hashCode() { + return Objects.hash( + mActiveDataSubId, + mSubIdToInfoMap, + mSubIdToCarrierConfigMap, + mPrivilegedPackages); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof TelephonySubscriptionSnapshot)) { + return false; + } + + final TelephonySubscriptionSnapshot other = (TelephonySubscriptionSnapshot) obj; + + return mActiveDataSubId == other.mActiveDataSubId + && mSubIdToInfoMap.equals(other.mSubIdToInfoMap) + && mSubIdToCarrierConfigMap.equals(other.mSubIdToCarrierConfigMap) + && mPrivilegedPackages.equals(other.mPrivilegedPackages); + } + + /** Dumps the state of this snapshot for logging and debugging purposes. */ + public void dump(IndentingPrintWriter pw) { + pw.println("TelephonySubscriptionSnapshot:"); + pw.increaseIndent(); + + pw.println("mActiveDataSubId: " + mActiveDataSubId); + pw.println("mSubIdToInfoMap: " + mSubIdToInfoMap); + pw.println("mSubIdToCarrierConfigMap: " + mSubIdToCarrierConfigMap); + pw.println("mPrivilegedPackages: " + mPrivilegedPackages); + + pw.decreaseIndent(); + } + + @Override + public String toString() { + return "TelephonySubscriptionSnapshot{ " + + "mActiveDataSubId=" + mActiveDataSubId + + ", mSubIdToInfoMap=" + mSubIdToInfoMap + + ", mSubIdToCarrierConfigMap=" + mSubIdToCarrierConfigMap + + ", mPrivilegedPackages=" + mPrivilegedPackages + + " }"; + } + } + + /** + * Interface for listening to changes in subscriptions + * + * @see TelephonySubscriptionTracker + */ + public interface TelephonySubscriptionTrackerCallback { + /** + * Called when subscription information changes, and a new subscription snapshot was taken + * + * @param snapshot the snapshot of subscription information. + */ + void onNewSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot); + } + + private class ActiveDataSubscriptionIdListener extends TelephonyCallback + implements TelephonyCallback.ActiveDataSubscriptionIdListener { + @Override + public void onActiveDataSubscriptionIdChanged(int subId) { + handleSubscriptionsChanged(); + } + } + + /** External static dependencies for test injection */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + /** Checks if the given bundle is for an identified carrier */ + public boolean isConfigForIdentifiedCarrier(PersistableBundle bundle) { + return CarrierConfigManager.isConfigForIdentifiedCarrier(bundle); + } + + /** Gets the active Subscription ID */ + public int getActiveDataSubscriptionId() { + return SubscriptionManager.getActiveDataSubscriptionId(); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/Vcn.java b/Vcn/service-b/src/com/android/server/vcn/Vcn.java new file mode 100644 index 0000000000..fbf8ce3248 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/Vcn.java @@ -0,0 +1,791 @@ +/* + * 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_VCN_MANAGED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_ACTIVE; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_INACTIVE; +import static android.net.vcn.VcnManager.VCN_STATUS_CODE_SAFE_MODE; + +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.annotation.TargetApi; +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.Uri; +import android.net.vcn.VcnConfig; +import android.net.vcn.VcnGatewayConnectionConfig; +import android.net.vcn.VcnManager.VcnErrorCode; +import android.net.vcn.util.LogUtils; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.os.ParcelUuid; +import android.provider.Settings; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.modules.utils.HandlerExecutor; +import com.android.server.VcnManagementService.VcnCallback; +import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; + +/** + * Represents an single instance of a VCN. + * + *

Each Vcn instance manages all {@link VcnGatewayConnection}(s) for a given subscription group, + * including per-capability networks, network selection, and multi-homing. + * + * @hide + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class Vcn extends Handler { + private static final String TAG = Vcn.class.getSimpleName(); + + private static final int VCN_LEGACY_SCORE_INT = 52; + + private static final List CAPS_REQUIRING_MOBILE_DATA = + Arrays.asList(NET_CAPABILITY_INTERNET, NET_CAPABILITY_DUN); + + private static final int MSG_EVENT_BASE = 0; + private static final int MSG_CMD_BASE = 100; + + // Copied from Settings.Global.MOBILE_DATA + private static final String SETTINGS_GLOBAL_MOBILE_DATA_STRING = "mobile_data"; + + /** + * A carrier app updated the configuration. + * + *

Triggers update of config, re-evaluating all active and underlying networks. + * + * @param obj VcnConfig + */ + private static final int MSG_EVENT_CONFIG_UPDATED = MSG_EVENT_BASE; + + /** + * A NetworkRequest was added or updated. + * + *

Triggers an evaluation of all active networks, bringing up a new one if necessary. + * + * @param obj NetworkRequest + */ + private static final int MSG_EVENT_NETWORK_REQUESTED = MSG_EVENT_BASE + 1; + + /** + * The TelephonySubscriptionSnapshot tracked by VcnManagementService has changed. + * + *

This updated snapshot should be cached locally and passed to all VcnGatewayConnections. + * + * @param obj TelephonySubscriptionSnapshot + */ + private static final int MSG_EVENT_SUBSCRIPTIONS_CHANGED = MSG_EVENT_BASE + 2; + + /** + * A GatewayConnection owned by this VCN quit. + * + * @param obj VcnGatewayConnectionConfig + */ + private static final int MSG_EVENT_GATEWAY_CONNECTION_QUIT = MSG_EVENT_BASE + 3; + + /** + * Triggers reevaluation of safe mode conditions. + * + *

Upon entering safe mode, the VCN will only provide gateway connections opportunistically, + * leaving the underlying networks marked as NOT_VCN_MANAGED. + * + *

Any VcnGatewayConnection in safe mode will result in the entire Vcn instance being put + * into safe mode. Upon receiving this message, the Vcn MUST query all VcnGatewayConnections to + * determine if any are in safe mode. + */ + private static final int MSG_EVENT_SAFE_MODE_STATE_CHANGED = MSG_EVENT_BASE + 4; + + /** + * Triggers reevaluation of mobile data enabled conditions. + * + *

Upon this notification, the VCN will check if any of the underlying subIds have mobile + * data enabled. If not, the VCN will restart any GatewayConnections providing INTERNET or DUN + * with the current mobile data toggle status. + */ + private static final int MSG_EVENT_MOBILE_DATA_TOGGLED = MSG_EVENT_BASE + 5; + + /** Triggers an immediate teardown of the entire Vcn, including GatewayConnections. */ + private static final int MSG_CMD_TEARDOWN = MSG_CMD_BASE; + + @NonNull private final VcnContext mVcnContext; + @NonNull private final ParcelUuid mSubscriptionGroup; + @NonNull private final Dependencies mDeps; + @NonNull private final VcnNetworkRequestListener mRequestListener; + @NonNull private final VcnCallback mVcnCallback; + @NonNull private final VcnContentResolver mContentResolver; + @NonNull private final ContentObserver mMobileDataSettingsObserver; + + @NonNull + private final Map mMobileDataStateListeners = + new ArrayMap<>(); + + /** + * Map containing all VcnGatewayConnections and their VcnGatewayConnectionConfigs. + * + *

Due to potential for race conditions, VcnGatewayConnections MUST only be created and added + * to this map in {@link #handleNetworkRequested(NetworkRequest, int, int)}, when a VCN receives + * a NetworkRequest that matches a VcnGatewayConnectionConfig for this VCN's VcnConfig. + * + *

A VcnGatewayConnection instance MUST NEVER overwrite an existing instance - otherwise + * there is potential for a orphaned VcnGatewayConnection instance that does not get properly + * shut down. + * + *

Due to potential for race conditions, VcnGatewayConnections MUST only be removed from this + * map once they have finished tearing down, which is reported to this VCN via {@link + * VcnGatewayStatusCallback#onQuit()}. Once this is done, all NetworkRequests are retrieved from + * the NetworkProvider so that another VcnGatewayConnectionConfig can match the + * previously-matched request. + */ + // TODO(b/182533200): remove the invariant on VcnGatewayConnection lifecycles + @NonNull + private final Map mVcnGatewayConnections = + new HashMap<>(); + + @NonNull private VcnConfig mConfig; + @NonNull private TelephonySubscriptionSnapshot mLastSnapshot; + + /** + * The current status of this Vcn instance + * + *

The value will be {@link VCN_STATUS_CODE_ACTIVE} while all VcnGatewayConnections are in + * good standing, {@link VCN_STATUS_CODE_SAFE_MODE} if any VcnGatewayConnections are in safe + * mode, and {@link VCN_STATUS_CODE_INACTIVE} once a teardown has been commanded. + */ + // Accessed from different threads, but always under lock in VcnManagementService + private volatile int mCurrentStatus = VCN_STATUS_CODE_ACTIVE; + + private boolean mIsMobileDataEnabled = false; + + public Vcn( + @NonNull VcnContext vcnContext, + @NonNull ParcelUuid subscriptionGroup, + @NonNull VcnConfig config, + @NonNull TelephonySubscriptionSnapshot snapshot, + @NonNull VcnCallback vcnCallback) { + this(vcnContext, subscriptionGroup, config, snapshot, vcnCallback, new Dependencies()); + } + + // WARNING: This constructor executes on the binder thread. Thread safety MUST be ensured when + // accessing data within this constructor and any methods called from here. + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Vcn( + @NonNull VcnContext vcnContext, + @NonNull ParcelUuid subscriptionGroup, + @NonNull VcnConfig config, + @NonNull TelephonySubscriptionSnapshot snapshot, + @NonNull VcnCallback vcnCallback, + @NonNull Dependencies deps) { + super(Objects.requireNonNull(vcnContext, "Missing vcnContext").getLooper()); + mVcnContext = vcnContext; + mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); + mVcnCallback = Objects.requireNonNull(vcnCallback, "Missing vcnCallback"); + mDeps = Objects.requireNonNull(deps, "Missing deps"); + mRequestListener = new VcnNetworkRequestListener(); + mContentResolver = mDeps.newVcnContentResolver(mVcnContext); + mMobileDataSettingsObserver = new VcnMobileDataContentObserver(this /* handler */); + + // TODO: b/364740845: Replace it with DataEnabledListener + final Uri uri = Settings.Global.getUriFor(SETTINGS_GLOBAL_MOBILE_DATA_STRING); + mContentResolver.registerContentObserver( + uri, true /* notifyForDescendants */, mMobileDataSettingsObserver); + + mConfig = Objects.requireNonNull(config, "Missing config"); + mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot"); + + // Update mIsMobileDataEnabled before starting handling of NetworkRequests. + mIsMobileDataEnabled = getMobileDataStatus(); + + // Register mobile data state listeners. + updateMobileDataStateListeners(); + + // Register to receive cached and future NetworkRequests + mVcnContext.getVcnNetworkProvider().registerListener(mRequestListener); + } + + /** Asynchronously updates the configuration and triggers a re-evaluation of Networks */ + public void updateConfig(@NonNull VcnConfig config) { + Objects.requireNonNull(config, "Missing config"); + + sendMessage(obtainMessage(MSG_EVENT_CONFIG_UPDATED, config)); + } + + /** Asynchronously updates the Subscription snapshot for this VCN. */ + public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot snapshot) { + Objects.requireNonNull(snapshot, "Missing snapshot"); + + sendMessage(obtainMessage(MSG_EVENT_SUBSCRIPTIONS_CHANGED, snapshot)); + } + + /** Asynchronously tears down this Vcn instance, including VcnGatewayConnection(s) */ + public void teardownAsynchronously() { + sendMessageAtFrontOfQueue(obtainMessage(MSG_CMD_TEARDOWN)); + } + + /** Synchronously retrieves the current status code. */ + public int getStatus() { + return mCurrentStatus; + } + + /** Sets the status of this VCN */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public void setStatus(int status) { + mCurrentStatus = status; + } + + /** Get current Gateways for testing purposes */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Set getVcnGatewayConnections() { + return Collections.unmodifiableSet(new HashSet<>(mVcnGatewayConnections.values())); + } + + /** Get current Configs and Gateways for testing purposes */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public Map + getVcnGatewayConnectionConfigMap() { + return Collections.unmodifiableMap(new HashMap<>(mVcnGatewayConnections)); + } + + private class VcnNetworkRequestListener implements VcnNetworkProvider.NetworkRequestListener { + @Override + public void onNetworkRequested(@NonNull NetworkRequest request) { + Objects.requireNonNull(request, "Missing request"); + + sendMessage(obtainMessage(MSG_EVENT_NETWORK_REQUESTED, request)); + } + } + + @Override + public void handleMessage(@NonNull Message msg) { + if (mCurrentStatus != VCN_STATUS_CODE_ACTIVE + && mCurrentStatus != VCN_STATUS_CODE_SAFE_MODE) { + return; + } + + switch (msg.what) { + case MSG_EVENT_CONFIG_UPDATED: + handleConfigUpdated((VcnConfig) msg.obj); + break; + case MSG_EVENT_NETWORK_REQUESTED: + handleNetworkRequested((NetworkRequest) msg.obj); + break; + case MSG_EVENT_SUBSCRIPTIONS_CHANGED: + handleSubscriptionsChanged((TelephonySubscriptionSnapshot) msg.obj); + break; + case MSG_EVENT_GATEWAY_CONNECTION_QUIT: + handleGatewayConnectionQuit((VcnGatewayConnectionConfig) msg.obj); + break; + case MSG_EVENT_SAFE_MODE_STATE_CHANGED: + handleSafeModeStatusChanged(); + break; + case MSG_EVENT_MOBILE_DATA_TOGGLED: + handleMobileDataToggled(); + break; + case MSG_CMD_TEARDOWN: + handleTeardown(); + break; + default: + logWtf("Unknown msg.what: " + msg.what); + } + } + + private void handleConfigUpdated(@NonNull VcnConfig config) { + // TODO: Add a dump function in VcnConfig that omits PII. Until then, use hashCode() + logDbg("Config updated: old = " + mConfig.hashCode() + "; new = " + config.hashCode()); + + mConfig = config; + + // Teardown any GatewayConnections whose configs have been removed and get all current + // requests + for (final Entry entry : + mVcnGatewayConnections.entrySet()) { + final VcnGatewayConnectionConfig gatewayConnectionConfig = entry.getKey(); + final VcnGatewayConnection gatewayConnection = entry.getValue(); + + // GatewayConnectionConfigs must match exactly (otherwise authentication or + // connection details may have changed). + if (!mConfig.getGatewayConnectionConfigs().contains(gatewayConnectionConfig)) { + if (gatewayConnection == null) { + logWtf("Found gatewayConnectionConfig without GatewayConnection"); + } else { + logInfo( + "Config updated, restarting gateway " + + gatewayConnection.getLogPrefix()); + gatewayConnection.teardownAsynchronously(); + } + } + } + + // Trigger a re-evaluation of all NetworkRequests (to make sure any that can be + // satisfied start a new GatewayConnection) + mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener); + } + + private void handleTeardown() { + logDbg("Tearing down"); + mVcnContext.getVcnNetworkProvider().unregisterListener(mRequestListener); + + for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) { + gatewayConnection.teardownAsynchronously(); + } + + // Unregister MobileDataStateListeners + for (VcnUserMobileDataStateListener listener : mMobileDataStateListeners.values()) { + getTelephonyManager().unregisterTelephonyCallback(listener); + } + mMobileDataStateListeners.clear(); + + mCurrentStatus = VCN_STATUS_CODE_INACTIVE; + } + + private void handleSafeModeStatusChanged() { + logVdbg("VcnGatewayConnection safe mode status changed"); + boolean hasSafeModeGatewayConnection = false; + + // If any VcnGatewayConnection is in safe mode, mark the entire VCN as being in safe mode + for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) { + if (gatewayConnection.isInSafeMode()) { + hasSafeModeGatewayConnection = true; + break; + } + } + + final int oldStatus = mCurrentStatus; + mCurrentStatus = + hasSafeModeGatewayConnection ? VCN_STATUS_CODE_SAFE_MODE : VCN_STATUS_CODE_ACTIVE; + if (oldStatus != mCurrentStatus) { + mVcnCallback.onSafeModeStatusChanged(hasSafeModeGatewayConnection); + logInfo( + "Safe mode " + + (mCurrentStatus == VCN_STATUS_CODE_SAFE_MODE ? "entered" : "exited")); + } + } + + private void handleNetworkRequested(@NonNull NetworkRequest request) { + logVdbg("Received request " + request); + + // If preexisting VcnGatewayConnection(s) satisfy request, return + for (VcnGatewayConnectionConfig gatewayConnectionConfig : mVcnGatewayConnections.keySet()) { + if (isRequestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) { + logVdbg("Request already satisfied by existing VcnGatewayConnection: " + request); + return; + } + } + + // If any supported (but not running) VcnGatewayConnection(s) can satisfy request, bring it + // up + for (VcnGatewayConnectionConfig gatewayConnectionConfig : + mConfig.getGatewayConnectionConfigs()) { + if (isRequestSatisfiedByGatewayConnectionConfig(request, gatewayConnectionConfig)) { + if (getExposedCapabilitiesForMobileDataState(gatewayConnectionConfig).isEmpty()) { + // Skip; this network does not provide any services if mobile data is disabled. + continue; + } + + // This should never happen, by virtue of checking for the above check for + // pre-existing VcnGatewayConnections that satisfy a given request, but if state + // that affects the satsifying of requests changes, this is theoretically possible. + if (mVcnGatewayConnections.containsKey(gatewayConnectionConfig)) { + logWtf( + "Attempted to bring up VcnGatewayConnection for config " + + "with existing VcnGatewayConnection"); + return; + } + + logInfo("Bringing up new VcnGatewayConnection for request " + request); + final VcnGatewayConnection vcnGatewayConnection = + mDeps.newVcnGatewayConnection( + mVcnContext, + mSubscriptionGroup, + mLastSnapshot, + gatewayConnectionConfig, + new VcnGatewayStatusCallbackImpl(gatewayConnectionConfig), + mIsMobileDataEnabled); + mVcnGatewayConnections.put(gatewayConnectionConfig, vcnGatewayConnection); + + return; + } + } + + logVdbg("Request could not be fulfilled by VCN: " + request); + } + + private Set getExposedCapabilitiesForMobileDataState( + VcnGatewayConnectionConfig gatewayConnectionConfig) { + if (mIsMobileDataEnabled) { + return gatewayConnectionConfig.getAllExposedCapabilities(); + } + + final Set exposedCapsWithoutMobileData = + new ArraySet<>(gatewayConnectionConfig.getAllExposedCapabilities()); + exposedCapsWithoutMobileData.removeAll(CAPS_REQUIRING_MOBILE_DATA); + + return exposedCapsWithoutMobileData; + } + + private void handleGatewayConnectionQuit(VcnGatewayConnectionConfig config) { + logInfo("VcnGatewayConnection quit: " + config); + mVcnGatewayConnections.remove(config); + + // Trigger a re-evaluation of all NetworkRequests (to make sure any that can be satisfied + // start a new GatewayConnection). VCN is always alive here, courtesy of the liveness check + // in handleMessage() + mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener); + } + + private void handleSubscriptionsChanged(@NonNull TelephonySubscriptionSnapshot snapshot) { + mLastSnapshot = snapshot; + + for (VcnGatewayConnection gatewayConnection : mVcnGatewayConnections.values()) { + gatewayConnection.updateSubscriptionSnapshot(mLastSnapshot); + } + + updateMobileDataStateListeners(); + + // Update the mobile data state after updating the subscription snapshot as a change in + // subIds for a subGroup may affect the mobile data state. + handleMobileDataToggled(); + } + + private void updateMobileDataStateListeners() { + final Set subIdsInGroup = mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup); + final HandlerExecutor executor = new HandlerExecutor(this); + + // Register new callbacks + for (int subId : subIdsInGroup) { + if (!mMobileDataStateListeners.containsKey(subId)) { + final VcnUserMobileDataStateListener listener = + new VcnUserMobileDataStateListener(); + + getTelephonyManagerForSubid(subId).registerTelephonyCallback(executor, listener); + mMobileDataStateListeners.put(subId, listener); + } + } + + // Unregister old callbacks + Iterator> iterator = + mMobileDataStateListeners.entrySet().iterator(); + while (iterator.hasNext()) { + final Entry entry = iterator.next(); + if (!subIdsInGroup.contains(entry.getKey())) { + getTelephonyManager().unregisterTelephonyCallback(entry.getValue()); + iterator.remove(); + } + } + } + + private void handleMobileDataToggled() { + final boolean oldMobileDataEnabledStatus = mIsMobileDataEnabled; + mIsMobileDataEnabled = getMobileDataStatus(); + + if (oldMobileDataEnabledStatus != mIsMobileDataEnabled) { + // Teardown any GatewayConnections that advertise INTERNET or DUN. If they provide other + // services, the VcnGatewayConnections will be restarted without advertising INTERNET or + // DUN. + for (Entry entry : + mVcnGatewayConnections.entrySet()) { + final VcnGatewayConnectionConfig gatewayConnectionConfig = entry.getKey(); + final VcnGatewayConnection gatewayConnection = entry.getValue(); + + final Set exposedCaps = + gatewayConnectionConfig.getAllExposedCapabilities(); + if (exposedCaps.contains(NET_CAPABILITY_INTERNET) + || exposedCaps.contains(NET_CAPABILITY_DUN)) { + if (gatewayConnection == null) { + logWtf("Found gatewayConnectionConfig without" + " GatewayConnection"); + } else { + // TODO(b/184868850): Optimize by restarting NetworkAgents without teardown. + gatewayConnection.teardownAsynchronously(); + } + } + } + + // Trigger re-evaluation of all requests; mobile data state impacts supported caps. + mVcnContext.getVcnNetworkProvider().resendAllRequests(mRequestListener); + + logInfo("Mobile data " + (mIsMobileDataEnabled ? "enabled" : "disabled")); + } + } + + private boolean getMobileDataStatus() { + for (int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) { + if (getTelephonyManagerForSubid(subId).isDataEnabled()) { + return true; + } + } + + return false; + } + + private boolean isRequestSatisfiedByGatewayConnectionConfig( + @NonNull NetworkRequest request, @NonNull VcnGatewayConnectionConfig config) { + final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder(); + builder.addTransportType(TRANSPORT_CELLULAR); + builder.addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + for (int cap : getExposedCapabilitiesForMobileDataState(config)) { + builder.addCapability(cap); + } + + return request.canBeSatisfiedBy(builder.build()); + } + + private TelephonyManager getTelephonyManager() { + return mVcnContext.getContext().getSystemService(TelephonyManager.class); + } + + private TelephonyManager getTelephonyManagerForSubid(int subid) { + return getTelephonyManager().createForSubscriptionId(subid); + } + + private String getLogPrefix() { + return "(" + + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup) + + "-" + + 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 logInfo(String msg) { + Slog.i(TAG, getLogPrefix() + msg); + LOCAL_LOG.log(getLogPrefix() + "INFO: " + msg); + } + + private void logInfo(String msg, Throwable tr) { + Slog.i(TAG, getLogPrefix() + msg, tr); + LOCAL_LOG.log(getLogPrefix() + "INFO: " + 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 Vcn for logging and debugging purposes. + * + *

PII and credentials MUST NEVER be dumped here. + * + *

This method is not thread safe and MUST run on the VCN thread. + */ + public void dump(IndentingPrintWriter pw) { + mVcnContext.ensureRunningOnLooperThread(); + + pw.println("Vcn (" + mSubscriptionGroup + "):"); + pw.increaseIndent(); + + pw.println("mCurrentStatus: " + mCurrentStatus); + pw.println("mIsMobileDataEnabled: " + mIsMobileDataEnabled); + pw.println(); + + pw.println("mVcnGatewayConnections:"); + pw.increaseIndent(); + for (VcnGatewayConnection gw : mVcnGatewayConnections.values()) { + gw.dump(pw); + } + pw.decreaseIndent(); + pw.println(); + + pw.decreaseIndent(); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public boolean isMobileDataEnabled() { + return mIsMobileDataEnabled; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public void setMobileDataEnabled(boolean isMobileDataEnabled) { + mIsMobileDataEnabled = isMobileDataEnabled; + } + + /** Retrieves the network score for a VCN Network */ + // Package visibility for use in VcnGatewayConnection and VcnNetworkProvider + static NetworkScore getNetworkScore() { + // TODO(b/193687515): Stop setting TRANSPORT_PRIMARY, define a TRANSPORT_VCN, and set in + // NetworkOffer/NetworkAgent. + return new NetworkScore.Builder() + .setLegacyInt(VCN_LEGACY_SCORE_INT) + .setTransportPrimary(true) + .setVcn(true) + .build(); + } + + /** Callback used for passing status signals from a VcnGatewayConnection to its managing Vcn. */ + @VisibleForTesting(visibility = Visibility.PACKAGE) + public interface VcnGatewayStatusCallback { + /** Called by a VcnGatewayConnection to indicate that it's safe mode status has changed. */ + void onSafeModeStatusChanged(); + + /** Callback by a VcnGatewayConnection to indicate that an error occurred. */ + void onGatewayConnectionError( + @NonNull String gatewayConnectionName, + @VcnErrorCode int errorCode, + @Nullable String exceptionClass, + @Nullable String exceptionMessage); + + /** Called by a VcnGatewayConnection to indicate that it has fully torn down. */ + void onQuit(); + } + + private class VcnGatewayStatusCallbackImpl implements VcnGatewayStatusCallback { + public final VcnGatewayConnectionConfig mGatewayConnectionConfig; + + VcnGatewayStatusCallbackImpl(VcnGatewayConnectionConfig gatewayConnectionConfig) { + mGatewayConnectionConfig = gatewayConnectionConfig; + } + + @Override + public void onQuit() { + sendMessage(obtainMessage(MSG_EVENT_GATEWAY_CONNECTION_QUIT, mGatewayConnectionConfig)); + } + + @Override + public void onSafeModeStatusChanged() { + sendMessage(obtainMessage(MSG_EVENT_SAFE_MODE_STATE_CHANGED)); + } + + @Override + public void onGatewayConnectionError( + @NonNull String gatewayConnectionName, + @VcnErrorCode int errorCode, + @Nullable String exceptionClass, + @Nullable String exceptionMessage) { + mVcnCallback.onGatewayConnectionError( + gatewayConnectionName, errorCode, exceptionClass, exceptionMessage); + } + } + + private class VcnMobileDataContentObserver extends ContentObserver { + private VcnMobileDataContentObserver(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + sendMessage(obtainMessage(MSG_EVENT_MOBILE_DATA_TOGGLED)); + } + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + class VcnUserMobileDataStateListener extends TelephonyCallback + implements TelephonyCallback.UserMobileDataStateListener { + + @Override + public void onUserMobileDataStateChanged(boolean enabled) { + sendMessage(obtainMessage(MSG_EVENT_MOBILE_DATA_TOGGLED)); + } + } + + /** External dependencies used by Vcn, for injection in tests */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + /** Builds a new VcnGatewayConnection */ + public VcnGatewayConnection newVcnGatewayConnection( + VcnContext vcnContext, + ParcelUuid subscriptionGroup, + TelephonySubscriptionSnapshot snapshot, + VcnGatewayConnectionConfig connectionConfig, + VcnGatewayStatusCallback gatewayStatusCallback, + boolean isMobileDataEnabled) { + return new VcnGatewayConnection( + vcnContext, + subscriptionGroup, + snapshot, + connectionConfig, + gatewayStatusCallback, + isMobileDataEnabled); + } + + /** Builds a new VcnContentResolver instance */ + public VcnContentResolver newVcnContentResolver(VcnContext vcnContext) { + return new VcnContentResolver(vcnContext); + } + } + + /** Proxy Implementation of NetworkAgent, used for testing. */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class VcnContentResolver { + private final ContentResolver mImpl; + + public VcnContentResolver(VcnContext vcnContext) { + mImpl = vcnContext.getContext().getContentResolver(); + } + + /** Registers the content observer */ + public void registerContentObserver( + @NonNull Uri uri, boolean notifyForDescendants, @NonNull ContentObserver observer) { + mImpl.registerContentObserver(uri, notifyForDescendants, observer); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/VcnContext.java b/Vcn/service-b/src/com/android/server/vcn/VcnContext.java new file mode 100644 index 0000000000..9213d96ad4 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/VcnContext.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.vcn; + +import android.annotation.NonNull; +import android.content.Context; +import android.net.vcn.FeatureFlags; +import android.net.vcn.FeatureFlagsImpl; +import android.os.Looper; + +import java.util.Objects; + +/** + * A simple class to pass around context information. + * + * @hide + */ +public class VcnContext { + @NonNull private final Context mContext; + @NonNull private final Looper mLooper; + @NonNull private final VcnNetworkProvider mVcnNetworkProvider; + @NonNull private final FeatureFlags mFeatureFlags; + private final boolean mIsInTestMode; + + public VcnContext( + @NonNull Context context, + @NonNull Looper looper, + @NonNull VcnNetworkProvider vcnNetworkProvider, + boolean isInTestMode) { + mContext = Objects.requireNonNull(context, "Missing context"); + mLooper = Objects.requireNonNull(looper, "Missing looper"); + mVcnNetworkProvider = Objects.requireNonNull(vcnNetworkProvider, "Missing networkProvider"); + mIsInTestMode = isInTestMode; + + // Auto-generated class + mFeatureFlags = new FeatureFlagsImpl(); + } + + @NonNull + public Context getContext() { + return mContext; + } + + @NonNull + public Looper getLooper() { + return mLooper; + } + + @NonNull + public VcnNetworkProvider getVcnNetworkProvider() { + return mVcnNetworkProvider; + } + + public boolean isInTestMode() { + return mIsInTestMode; + } + + @NonNull + public FeatureFlags getFeatureFlags() { + return mFeatureFlags; + } + + /** + * Verifies that the caller is running on the VcnContext Thread. + * + * @throwsIllegalStateException if the caller is not running on the VcnContext Thread. + */ + public void ensureRunningOnLooperThread() { + if (getLooper().getThread() != Thread.currentThread()) { + throw new IllegalStateException("Not running on VcnMgmtSvc thread"); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java b/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java new file mode 100644 index 0000000000..0a160fc097 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/VcnGatewayConnection.java @@ -0,0 +1,3122 @@ +/* + * 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.VcnGatewayConnectionConfig.VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY; +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 android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; + +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.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.net.ConnectivityDiagnosticsManager; +import android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +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.PolicyDirection; +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.NetworkRequest; +import android.net.NetworkScore; +import android.net.RouteInfo; +import android.net.TelephonyNetworkSpecifier; +import android.net.Uri; +import android.net.ipsec.ike.ChildSaProposal; +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.IkeSessionConnectionInfo; +import android.net.ipsec.ike.IkeSessionParams; +import android.net.ipsec.ike.IkeTrafficSelector; +import android.net.ipsec.ike.IkeTunnelConnectionParams; +import android.net.ipsec.ike.TunnelModeChildSessionParams; +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.VcnManager; +import android.net.vcn.VcnTransportInfo; +import android.net.vcn.util.LogUtils; +import android.net.vcn.util.MtuUtils; +import android.net.vcn.util.OneWayBoolean; +import android.net.wifi.WifiInfo; +import android.os.Build; +import android.os.Handler; +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.IndentingPrintWriter; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; +import com.android.internal.util.WakeupMessage; +import com.android.modules.utils.HandlerExecutor; +import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import com.android.server.vcn.Vcn.VcnGatewayStatusCallback; +import com.android.server.vcn.metrics.VcnMetrics; +import com.android.server.vcn.routeselection.UnderlyingNetworkController; +import com.android.server.vcn.routeselection.UnderlyingNetworkController.UnderlyingNetworkControllerCallback; +import com.android.server.vcn.routeselection.UnderlyingNetworkRecord; + +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. + * + *

This class handles mobility events, performs retries, and tracks safe-mode conditions. + * + *

Internal state transitions are as follows:
+ *
+ * +----------------------------+                 +------------------------------+
+ * |     DisconnectedState      |    Teardown or  |      DisconnectingState      |
+ * |                            |<--no available--|                              |
+ * |       Initial state.       |    underlying   | Transitive state for tearing |
+ * +----------------------------+     networks    | tearing down an IKE session. |
+ *               |                                +------------------------------+
+ *               |                                         ^          |
+ *       Underlying Network            Teardown requested  |   Not tearing down
+ *            changed               +--or retriable error--+  and has available
+ *               |                  |      occurred           underlying network
+ *               |                  ^                                 |
+ *               v                  |                                 v
+ * +----------------------------+   |             +------------------------------+
+ * |      ConnectingState       |<----------------|      RetryTimeoutState       |
+ * |                            |   |             |                              |
+ * |    Transitive state for    |   |             |     Transitive state for     |
+ * |  starting IKE negotiation. |---+             |  handling retriable errors.  |
+ * +----------------------------+   |             +------------------------------+
+ *               |                  |
+ *          IKE session             |
+ *           negotiated             |
+ *               |                  |
+ *               v                  |
+ * +----------------------------+   ^
+ * |      ConnectedState        |   |
+ * |                            |   |
+ * |     Stable state where     |   |
+ * |  gateway connection is set |   |
+ * | up, and Android Network is |   |
+ * |         connected.         |---+
+ * +----------------------------+
+ * 
+ * + *

All messages in VcnGatewayConnection should 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). + * + *

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 + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class VcnGatewayConnection extends StateMachine { + private static final String TAG = VcnGatewayConnection.class.getSimpleName(); + + /** Default number of parallel SAs requested */ + static final int TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT = 1; + + // The returned string of + // TelephonyManager#getNetworkTypeName(TelephonyManager.NETWORK_TYPE_UNKNOWN) + private static final String NETWORK_TYPE_STRING_UNKNOWN = "UNKNOWN"; + + // 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 UnderlyingNetworkController). + * + *

May indicate an entirely new underlying network, OR a change in network properties. + * + *

Relevant in ALL states. + * + *

In the Connected state, this MAY indicate a mobility even occurred. + * + * @param arg1 The "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. + * + *

Only relevant in the Retry-timeout state, discarded in all other states. + * + *

Upon receipt of this signal, the state machine will transition from the Retry-timeout + * state to the Connecting state. + * + * @param arg1 The "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. + * + *

Relevant in all states that have an IKE session. + * + *

Upon receipt of this signal, the state machine will (unless loss of the session is + * expected) transition to the Disconnecting state, to ensure IKE session closure before + * retrying, or fully shutting down. + * + * @param arg1 The session token for the IKE Session that was lost, used to prevent out-of-date + * signals from propagating. + * @param obj @NonNull An EventSessionLostInfo instance with relevant data. + */ + private static final int EVENT_SESSION_LOST = 3; + + private static class EventSessionLostInfo implements EventInfo { + @Nullable public final Exception exception; + + EventSessionLostInfo(@NonNull Exception exception) { + this.exception = exception; + } + + @Override + public int hashCode() { + return Objects.hash(exception); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventSessionLostInfo)) { + return false; + } + + final EventSessionLostInfo rhs = (EventSessionLostInfo) other; + return Objects.equals(exception, rhs.exception); + } + } + + /** + * Sent when an IKE session has completely closed. + * + *

Relevant only in the Disconnecting State, used to identify that a session being torn down + * was fully closed. If this event is not fired within a timely fashion, the IKE session will be + * forcibly terminated. + * + *

Upon receipt of this signal, the state machine will (unless closure of the session is + * expected) transition to the Disconnected or RetryTimeout states, depending on whether the + * GatewayConnection is being fully torn down. + * + * @param arg1 The session token for the IKE Session that was lost, used to prevent out-of-date + * signals from propagating. + * @param obj @NonNull An EventSessionLostInfo instance with relevant data. + */ + private static final int EVENT_SESSION_CLOSED = 4; + + /** + * Sent when an IKE Child Transform was created, and should be applied to the tunnel. + * + *

Only relevant in the Connecting, Connected and Migrating states. This callback MUST be + * handled in the Connected or Migrating states, and should be deferred if necessary. + * + * @param arg1 The session token for the IKE Session that had a new child created, used to + * prevent out-of-date signals from propagating. + * @param obj @NonNull An EventTransformCreatedInfo instance with relevant data. + */ + private static final int EVENT_TRANSFORM_CREATED = 5; + + private static class EventTransformCreatedInfo implements EventInfo { + @PolicyDirection public final int direction; + @NonNull public final IpSecTransform transform; + + EventTransformCreatedInfo( + @PolicyDirection int direction, @NonNull IpSecTransform transform) { + this.direction = direction; + this.transform = Objects.requireNonNull(transform); + } + + @Override + public int hashCode() { + return Objects.hash(direction, transform); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventTransformCreatedInfo)) { + return false; + } + + final EventTransformCreatedInfo rhs = (EventTransformCreatedInfo) other; + return direction == rhs.direction && Objects.equals(transform, rhs.transform); + } + } + + /** + * Sent when an IKE Child Session was completely opened and configured successfully. + * + *

Only relevant in the Connected and Migrating states. + * + * @param arg1 The session token for the IKE Session for which a child was opened and configured + * successfully, used to prevent out-of-date signals from propagating. + * @param obj @NonNull An EventSetupCompletedInfo instance with relevant data. + */ + private static final int EVENT_SETUP_COMPLETED = 6; + + private static class EventSetupCompletedInfo implements EventInfo { + @NonNull public final 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. + * + *

Relevant in all states except the Disconnected state. + * + *

This signal is often fired with a timeout in order to prevent disconnecting during + * transient conditions, such as network switches. Upon the transient passing, the signal is + * canceled based on the disconnect reason. + * + *

Upon receipt of this signal, the state machine MUST tear down all active sessions, cancel + * any pending work items, and move to the Disconnected state. + * + * @param arg1 The "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; + + // TODO(b/440645797): Consider not taking in shouldQuit as a param and relying on the reason + // deterministically. + 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; + } + + @VcnMetrics.GatewayTeardownReason + int getGatewayTeardownReason() { + // TODO(b/440645797): Assuming DISCONNECT_REASON_UNDERLYING_NETWORK_LOST means + // shouldQuit == false. Consider removing shouldQuit variable and rely on the reason + // deterministically. + if (!shouldQuit) { + return VcnMetrics.GATEWAY_TEARDOWN_REASON_NONE; + } + switch (reason) { + case DISCONNECT_REASON_INTERNAL_ERROR: + return VcnMetrics.GATEWAY_TEARDOWN_REASON_INTERNAL_ERROR; + case DISCONNECT_REASON_NETWORK_AGENT_UNWANTED: + return VcnMetrics.GATEWAY_TEARDOWN_REASON_NETWORK_AGENT_UNWANTED; + case DISCONNECT_REASON_TEARDOWN: + return VcnMetrics.GATEWAY_TEARDOWN_REASON_REQUESTED; + default: + return VcnMetrics.GATEWAY_TEARDOWN_REASON_UNSPECIFIED; + } + } + } + + /** + * Sent (delayed) to trigger a forcible close of an IKE session. + * + *

Only relevant in the Disconnecting state, discarded in all other states. + * + *

Upon receipt of this signal, the state machine will transition from the Disconnecting + * state to the Disconnected state. + * + * @param arg1 The session token for the IKE Session that is being torn down, used to prevent + * out-of-date signals from propagating. + */ + private static final int EVENT_TEARDOWN_TIMEOUT_EXPIRED = 8; + + /** + * Sent when this VcnGatewayConnection is notified of a change in TelephonySubscriptions. + * + *

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. + * + *

A VcnGatewayConnection enters safe mode when it takes over {@link + * #SAFEMODE_TIMEOUT_SECONDS} to enter {@link ConnectedState}. + * + *

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). + * + *

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. + * + *

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); + } + } + + /** + * Sent when an IKE session connection information has changed. + * + *

This signal is always fired before EVENT_SETUP_COMPLETED and EVENT_MIGRATION_COMPLETED. + * + *

Only relevant in the Connecting and Connected state. + * + * @param arg1 The session token for the IKE Session whose connection information has changed, + * used to prevent out-of-date signals from propagating. + * @param obj @NonNull An EventIkeConnectionInfoChangedInfo instance with relevant data. + */ + private static final int EVENT_IKE_CONNECTION_INFO_CHANGED = 12; + + private static class EventIkeConnectionInfoChangedInfo implements EventInfo { + @NonNull public final IkeSessionConnectionInfo ikeConnectionInfo; + + EventIkeConnectionInfoChangedInfo(@NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + this.ikeConnectionInfo = ikeConnectionInfo; + } + + @Override + public int hashCode() { + return Objects.hash(ikeConnectionInfo); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventIkeConnectionInfoChangedInfo)) { + return false; + } + + final EventIkeConnectionInfoChangedInfo rhs = (EventIkeConnectionInfoChangedInfo) other; + return Objects.equals(ikeConnectionInfo, rhs.ikeConnectionInfo); + } + } + + /** + * Sent when there is a suspected data stall on a network + * + *

Only relevant in the Connected state. + * + * @param arg1 The "all" token; this signal is always honored. + * @param obj @NonNull An EventDataStallSuspectedInfo instance with relevant data. + */ + private static final int EVENT_DATA_STALL_SUSPECTED = 13; + + private static class EventDataStallSuspectedInfo implements EventInfo { + @NonNull public final Network network; + + EventDataStallSuspectedInfo(@NonNull Network network) { + this.network = network; + } + + @Override + public int hashCode() { + return Objects.hash(network); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof EventDataStallSuspectedInfo)) { + return false; + } + + final EventDataStallSuspectedInfo rhs = (EventDataStallSuspectedInfo) other; + return Objects.equals(network, rhs.network); + } + } + + @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 UnderlyingNetworkController mUnderlyingNetworkController; + @NonNull private final VcnGatewayConnectionConfig mConnectionConfig; + @NonNull private final VcnGatewayStatusCallback mGatewayStatusCallback; + @NonNull private final VcnMetrics mVcnMetrics; + @NonNull private final Dependencies mDeps; + + @NonNull + private final VcnUnderlyingNetworkControllerCallback mUnderlyingNetworkControllerCallback; + + @NonNull private final VcnConnectivityDiagnosticsCallback mConnectivityDiagnosticsCallback; + + private final boolean mIsMobileDataEnabled; + private final int mId = System.identityHashCode(this); + + @NonNull private final IpSecManager mIpSecManager; + @NonNull private final ConnectivityManager mConnectivityManager; + @NonNull private final ConnectivityDiagnosticsManager mConnectivityDiagnosticsManager; + + @Nullable private IpSecTunnelInterface mTunnelIface = null; + + /** + * WakeLock to be held when processing messages on the Handler queue. + * + *

Used to prevent the device from going to sleep while there are VCN-related events to + * process for this VcnGatewayConnection. + * + *

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. + * + *

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. + * + *

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. + * + *

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(); + + /** + * The reason we are about to quit/teardown VcnGatewayConnection. + * + *

When mIsQuitting is set true, we also update the reason for logging purposes. If multiple + * reasons/triggers end up sending teardown command, we will log with the last reason received. + */ + private @VcnMetrics.GatewayTeardownReason int mTeardownReason = + VcnMetrics.GATEWAY_TEARDOWN_REASON_NONE; + + /** + * Whether the VcnGatewayConnection is in safe mode. + * + *

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. + * + *

This token MUST be updated when a new stateful/async session becomes the + * primary/current/active session. Example cases where the session changes are: + * + *

    + *
  • Switching to an IKE session as the primary session + *
+ * + *

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: + * + *

    + *
  • The new session connects successfully, and becomes the primary session + *
  • The existing session is lost, and the remaining (new) session becomes the primary + * session + *
+ */ + private int mCurrentToken = -1; + + /** + * The number of unsuccessful attempts since the last successful connection. + * + *

This number MUST be incremented each time the RetryTimeout state is entered, and cleared + * each time the Connected state is entered. + */ + private int mFailedAttempts = 0; + + /** + * The current underlying network. + * + *

Set in any states, always @NonNull in all states except Disconnected, null otherwise. + */ + private UnderlyingNetworkRecord mUnderlying; + + /** + * The current IKE Session connection information + * + *

Set in Connected and Migrating states, always @NonNull in Connected, Migrating + * states, @Nullable otherwise. + */ + private IkeSessionConnectionInfo mIkeConnectionInfo; + + /** + * The active IKE session. + * + *

Set in Connecting or Migrating States, always @NonNull in Connecting, Connected, and + * Migrating states, null otherwise. + */ + private VcnIkeSession mIkeSession; + + /** + * The last known child configuration. + * + *

Set in Connected and Migrating states, always @NonNull in Connected, Migrating + * states, @Nullable otherwise. + */ + private VcnChildSessionConfiguration mChildConfig; + + /** + * The active network agent. + * + *

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"); + mVcnMetrics = mDeps.newVcnMetrics(); + + mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot"); + + mUnderlyingNetworkControllerCallback = new VcnUnderlyingNetworkControllerCallback(); + + mWakeLock = + mDeps.newWakeLock(mVcnContext.getContext(), PowerManager.PARTIAL_WAKE_LOCK, TAG); + + mUnderlyingNetworkController = + mDeps.newUnderlyingNetworkController( + mVcnContext, + mConnectionConfig, + mId, + subscriptionGroup, + mLastSnapshot, + mUnderlyingNetworkControllerCallback); + mIpSecManager = mVcnContext.getContext().getSystemService(IpSecManager.class); + mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class); + mConnectivityDiagnosticsManager = + mVcnContext.getContext().getSystemService(ConnectivityDiagnosticsManager.class); + + mConnectivityDiagnosticsCallback = new VcnConnectivityDiagnosticsCallback(); + + if (mConnectionConfig.hasGatewayOption( + VCN_GATEWAY_OPTION_ENABLE_DATA_STALL_RECOVERY_WITH_MOBILITY)) { + final NetworkRequest diagRequest = + new NetworkRequest.Builder().addTransportType(TRANSPORT_CELLULAR).build(); + mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback( + diagRequest, + new HandlerExecutor(new Handler(vcnContext.getLooper())), + mConnectivityDiagnosticsCallback); + } + + 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. + * + *

Once torn down, this VcnTunnel CANNOT be started again. + */ + public void teardownAsynchronously() { + logDbg("Triggering async teardown"); + sendDisconnectRequestedAndAcquireWakelock( + DISCONNECT_REASON_TEARDOWN, true /* shouldQuit */); + } + + @Override + protected void onQuitting() { + logInfo("Quitting VcnGatewayConnection"); + + if (mNetworkAgent != null) { + logWtf("NetworkAgent was non-null in onQuitting"); + mVcnMetrics.logVcnNetworkNotConnected(mId, mNetworkAgent.getIdentityHashCode()); + 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(); + + mUnderlyingNetworkController.teardown(); + + mGatewayStatusCallback.onQuit(); + + mVcnMetrics.logVcnGatewayTeardown(mId, mTeardownReason); + mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback( + mConnectivityDiagnosticsCallback); + } + + /** + * Notify this Gateway that subscriptions have changed. + * + *

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; + mUnderlyingNetworkController.updateSubscriptionSnapshot(mLastSnapshot); + + sendMessageAndAcquireWakeLock(EVENT_SUBSCRIPTIONS_CHANGED, TOKEN_ALL); + } + + private class VcnConnectivityDiagnosticsCallback extends ConnectivityDiagnosticsCallback { + @Override + public void onDataStallSuspected(ConnectivityDiagnosticsManager.DataStallReport report) { + mVcnContext.ensureRunningOnLooperThread(); + + final Network network = report.getNetwork(); + logInfo("Data stall suspected on " + network); + sendMessageAndAcquireWakeLock( + EVENT_DATA_STALL_SUSPECTED, + TOKEN_ALL, + new EventDataStallSuspectedInfo(network)); + } + } + + private class VcnUnderlyingNetworkControllerCallback + implements UnderlyingNetworkControllerCallback { + @Override + public void onSelectedUnderlyingNetworkChanged( + @Nullable UnderlyingNetworkRecord underlying) { + // TODO(b/180132994): explore safely removing this Thread check + mVcnContext.ensureRunningOnLooperThread(); + + if (!UnderlyingNetworkRecord.isSameNetwork(mUnderlying, underlying)) { + logInfo( + "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 void markQuitting(@VcnMetrics.GatewayTeardownReason int teardownReason) { + mIsQuitting.setTrue(); + mTeardownReason = teardownReason; + } + + 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() { + if (!mConnectionConfig.isSafeModeEnabled()) { + logVdbg("setSafeModeAlarm: safe mode disabled"); + return; + } + + 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, + getSafeModeTimeoutMs(mVcnContext, mLastSnapshot, mSubscriptionGroup)); + } + + /** Gets the safe mode timeout */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static long getSafeModeTimeoutMs( + VcnContext vcnContext, TelephonySubscriptionSnapshot snapshot, ParcelUuid subGrp) { + final int defaultSeconds = + vcnContext.isInTestMode() + ? SAFEMODE_TIMEOUT_SECONDS_TEST_MODE + : SAFEMODE_TIMEOUT_SECONDS; + + final PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp); + int resultSeconds = defaultSeconds; + + if (carrierConfig != null) { + resultSeconds = + carrierConfig.getInt( + VcnManager.VCN_SAFE_MODE_TIMEOUT_SECONDS_KEY, defaultSeconds); + } + + return TimeUnit.SECONDS.toMillis(resultSeconds); + } + + 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 ikeConnectionInfoChanged( + int token, @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + sendMessageAndAcquireWakeLock( + EVENT_IKE_CONNECTION_INFO_CHANGED, + token, + new EventIkeConnectionInfoChangedInfo(ikeConnectionInfo)); + } + + 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. + * + *

By default, States consider any and all token to be 'valid'. + * + *

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. + * + *

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: // Fallthrough + case EVENT_IKE_CONNECTION_INFO_CHANGED: // Fallthrough + case EVENT_DATA_STALL_SUSPECTED: + logUnexpectedEvent(msg.what); + break; + default: + logWtfUnknownEvent(msg.what); + break; + } + } + + protected void teardownNetwork() { + if (mNetworkAgent != null) { + mVcnMetrics.logVcnNetworkNotConnected(mId, mNetworkAgent.getIdentityHashCode()); + mNetworkAgent.unregister(); + mNetworkAgent = null; + } + } + + protected void handleDisconnectRequested(EventDisconnectRequestedInfo info) { + // TODO(b/180526152): notify VcnStatusCallback for Network loss + + logInfo("Tearing down. Cause: " + info.reason + "; quitting = " + info.shouldQuit); + if (info.shouldQuit) { + markQuitting(info.getGatewayTeardownReason()); + } + + 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; + logInfo("Entering safe mode after timeout exceeded"); + + // Connectivity for this GatewayConnection is broken; tear down the Network. + teardownNetwork(); + if (!mIsInSafeMode) { + mVcnMetrics.logEnterSafeMode(mId); + } + mIsInSafeMode = true; + mGatewayStatusCallback.onSafeModeStatusChanged(); + } + + protected void logUnexpectedEvent(int what) { + logVdbg( + "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. + * + *

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: + EventDisconnectRequestedInfo info = (EventDisconnectRequestedInfo) msg.obj; + if (info.shouldQuit) { + markQuitting(info.getGatewayTeardownReason()); + + 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. + * + *

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. + * + *

This is used when an underlying network change triggered a restart on a new network. + * + *

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) { + markQuitting(info.getGatewayTeardownReason()); + } + + 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. + * + *

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_IKE_CONNECTION_INFO_CHANGED: // 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, + @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + final NetworkCapabilities caps = + buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled); + final LinkProperties lp = + buildConnectedLinkProperties( + mConnectionConfig, + tunnelIface, + childConfig, + mUnderlying, + ikeConnectionInfo); + + agent.sendNetworkCapabilities(caps); + agent.sendLinkProperties(lp); + + agent.setUnderlyingNetworks( + mUnderlying == null ? null : Collections.singletonList(mUnderlying.network)); + } + + protected VcnNetworkAgent buildNetworkAgent( + @NonNull IpSecTunnelInterface tunnelIface, + @NonNull VcnChildSessionConfiguration childConfig, + @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + final NetworkCapabilities caps = + buildNetworkCapabilities(mConnectionConfig, mUnderlying, mIsMobileDataEnabled); + final LinkProperties lp = + buildConnectedLinkProperties( + mConnectionConfig, + tunnelIface, + childConfig, + mUnderlying, + ikeConnectionInfo); + final NetworkAgentConfig nac = + new NetworkAgentConfig.Builder() + .setLegacyType(ConnectivityManager.TYPE_MOBILE) + .setLegacyTypeName(NETWORK_INFO_NETWORK_TYPE_STRING) + .setLegacySubType(TelephonyManager.NETWORK_TYPE_UNKNOWN) + .setLegacySubTypeName(NETWORK_TYPE_STRING_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; + } + + logInfo("NetworkAgent was unwanted"); + // TODO(b/440645797): This tears down with _TEARDOWN reason rather + // than _NETWORK_AGENT_UNWANTED. + teardownAsynchronously(); + } /* networkUnwantedCallback */, + (status) -> { + if (mIsQuitting.getValue()) { + return; // Ignore; VcnGatewayConnection quitting or already quit + } + // TODO(b/443831229): check if mNetworkAgent is stale first. + + switch (status) { + case NetworkAgent.VALIDATION_STATUS_VALID: + clearFailedAttemptCounterAndSafeModeAlarm(); + mVcnMetrics.logVcnNetworkValidated( + mId, mNetworkAgent.getIdentityHashCode()); + break; + case NetworkAgent.VALIDATION_STATUS_NOT_VALID: + // Trigger re-validation of underlying networks; if it + // fails, the VCN will attempt to migrate away. + if (mUnderlying != null) { + mConnectivityManager.reportNetworkConnectivity( + mUnderlying.network, + false /* hasConnectivity */); + } + + // Will only set a new alarm if no safe mode alarm is + // currently scheduled. + setSafeModeAlarm(); + mVcnMetrics.logVcnNetworkNotValidated( + mId, mNetworkAgent.getIdentityHashCode()); + break; + default: + logWtf( + "Unknown validation status " + + status + + "; ignoring"); + break; + } + } /* validationStatusCallback */); + + agent.register(); + agent.markConnected(); + mVcnMetrics.logVcnNetworkConnected(mId, agent.getIdentityHashCode()); + + return agent; + } + + protected void clearFailedAttemptCounterAndSafeModeAlarm() { + mVcnContext.ensureRunningOnLooperThread(); + + // Validated connection, clear failed attempt counter + mFailedAttempts = 0; + cancelSafeModeAlarm(); + + if (mIsInSafeMode) { + mVcnMetrics.logExitSafeMode(mId); + } + 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); + + if (direction == IpSecManager.DIRECTION_IN) { + mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform); + } + + // For inbound transforms, additionally allow forwarded traffic to bridge to DUN (as + // needed) + final Set exposedCaps = mConnectionConfig.getAllExposedCapabilities(); + if (direction == IpSecManager.DIRECTION_IN + && exposedCaps.contains(NET_CAPABILITY_DUN)) { + mIpSecManager.applyTunnelModeTransform( + tunnelIface, IpSecManager.DIRECTION_FWD, transform); + } + } catch (IOException | IllegalArgumentException e) { + logInfo("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 newAddrs = + new ArraySet<>(childConfig.getInternalAddresses()); + final Set existingAddrs = new ArraySet<>(); + if (oldChildConfig != null) { + existingAddrs.addAll(oldChildConfig.getInternalAddresses()); + } + + final Set toAdd = new ArraySet<>(); + toAdd.addAll(newAddrs); + toAdd.removeAll(existingAddrs); + + final Set 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) { + logInfo("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. + * + *

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, + mIkeConnectionInfo); + + // Create opportunistic child SAs; this allows SA aggregation in the downlink, + // reducing lock/atomic contention in high throughput scenarios. All SAs will + // share the same UDP encap socket (and keepalives) as necessary, and are + // effectively free. + final int parallelTunnelCount = + mDeps.getParallelTunnelCount(mLastSnapshot, mSubscriptionGroup); + logInfo("Parallel tunnel count: " + parallelTunnelCount); + + for (int i = 0; i < parallelTunnelCount - 1; i++) { + mIkeSession.openChildSession( + buildOpportunisticChildParams(), + new VcnChildSessionCallback( + mCurrentToken, true /* isOpportunistic */)); + } + + 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; + case EVENT_IKE_CONNECTION_INFO_CHANGED: + mIkeConnectionInfo = + ((EventIkeConnectionInfoChangedInfo) msg.obj).ikeConnectionInfo; + break; + case EVENT_DATA_STALL_SUSPECTED: + final Network networkWithDataStall = + ((EventDataStallSuspectedInfo) msg.obj).network; + handleDataStallSuspected(networkWithDataStall); + break; + default: + logUnhandledMessage(msg); + break; + } + } + + private void handleMigrationCompleted(EventMigrationCompletedInfo migrationCompletedInfo) { + logInfo("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, mIkeConnectionInfo); + + // Trigger re-validation after migration events. + mConnectivityManager.reportNetworkConnectivity( + mNetworkAgent.getNetwork(), false /* hasConnectivity */); + } + + private void handleUnderlyingNetworkChanged(@NonNull Message msg) { + final UnderlyingNetworkRecord oldUnderlying = mUnderlying; + mUnderlying = ((EventUnderlyingNetworkChangedInfo) msg.obj).newUnderlying; + + if (mUnderlying == null) { + logInfo("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)) { + logInfo("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, mIkeConnectionInfo); + } + } + } + + private void handleDataStallSuspected(Network networkWithDataStall) { + if (mUnderlying != null + && mNetworkAgent != null + && mNetworkAgent.getNetwork().equals(networkWithDataStall)) { + logInfo("Perform Mobility update to recover from suspected data stall"); + mIkeSession.setNetwork(mUnderlying.network); + } + } + + protected void setupInterfaceAndNetworkAgent( + int token, + @NonNull IpSecTunnelInterface tunnelIface, + @NonNull VcnChildSessionConfiguration childConfig, + @NonNull VcnChildSessionConfiguration oldChildConfig, + @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + setupInterface(token, tunnelIface, childConfig, oldChildConfig); + + if (mNetworkAgent == null) { + mNetworkAgent = buildNetworkAgent(tunnelIface, childConfig, ikeConnectionInfo); + } else { + updateNetworkAgent(tunnelIface, mNetworkAgent, childConfig, ikeConnectionInfo); + + // 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. + * + *

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, + gatewayConnectionConfig.getMinUdpPort4500NatTimeoutSeconds())); + } else if (underlyingCaps.hasTransport(TRANSPORT_CELLULAR) + && underlyingCaps.getNetworkSpecifier() instanceof TelephonyNetworkSpecifier) { + final TelephonyNetworkSpecifier telNetSpecifier = + (TelephonyNetworkSpecifier) underlyingCaps.getNetworkSpecifier(); + builder.setTransportInfo( + new VcnTransportInfo( + telNetSpecifier.getSubscriptionId(), + gatewayConnectionConfig.getMinUdpPort4500NatTimeoutSeconds())); + } else { + Slog.wtf( + TAG, + "Unknown transport type or missing TransportInfo/NetworkSpecifier for" + + " non-null underlying network"); + } + builder.setUnderlyingNetworks(List.of(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, + @NonNull IkeSessionConnectionInfo ikeConnectionInfo) { + 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, + ikeConnectionInfo.getLocalAddress() instanceof Inet4Address)); + + 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); + ikeConnectionInfoChanged(mToken, ikeSessionConfig.getIkeSessionConnectionInfo()); + } + + @Override + public void onClosed() { + logDbg("IkeClosed for token " + mToken); + sessionClosed(mToken, null); + } + + @Override + public void onClosedExceptionally(@NonNull IkeException exception) { + logInfo("IkeClosedExceptionally for token " + mToken, exception); + sessionClosed(mToken, exception); + } + + @Override + public void onError(@NonNull IkeProtocolException exception) { + logInfo("IkeError for token " + mToken, exception); + // Non-fatal, log and continue. + } + + @Override + public void onIkeSessionConnectionInfoChanged( + @NonNull IkeSessionConnectionInfo connectionInfo) { + logDbg("onIkeSessionConnectionInfoChanged for token " + mToken); + ikeConnectionInfoChanged(mToken, connectionInfo); + } + } + + /** Implementation of ChildSessionCallback, exposed for testing. */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public class VcnChildSessionCallback implements ChildSessionCallback { + private final int mToken; + private final boolean mIsOpportunistic; + + private boolean mIsChildOpened = false; + + VcnChildSessionCallback(int token) { + this(token, false /* isOpportunistic */); + } + + /** + * Creates a ChildSessionCallback + * + *

If configured as opportunistic, transforms will not report initial startup, or + * associated startup failures. This serves the dual purposes of ensuring that if the server + * does not support connection multiplexing, new child SA negotiations will be ignored, and + * at the same time, will notify the VCN session if a successfully negotiated opportunistic + * child SA is subsequently torn down, which could impact uplink traffic if the SA in use + * for outbound/uplink traffic is this opportunistic SA. + * + *

While inbound SAs can be used in parallel, the IPsec stack explicitly selects the last + * applied outbound transform for outbound traffic. This means that unlike inbound traffic, + * outbound does not benefit from these parallel SAs in the same manner. + */ + VcnChildSessionCallback(int token, boolean isOpportunistic) { + mToken = token; + mIsOpportunistic = isOpportunistic; + } + + /** Internal proxy method for injecting of mocked ChildSessionConfiguration */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + void onOpened(@NonNull VcnChildSessionConfiguration childConfig) { + logDbg("ChildOpened for token " + mToken); + + if (mIsOpportunistic) { + logDbg("ChildOpened for opportunistic child; suppressing event message"); + mIsChildOpened = true; + return; + } + + childOpened(mToken, childConfig); + } + + @Override + public void onOpened(@NonNull ChildSessionConfiguration childConfig) { + onOpened(new VcnChildSessionConfiguration(childConfig)); + } + + @Override + public void onClosed() { + logDbg("ChildClosed for token " + mToken); + + if (mIsOpportunistic && !mIsChildOpened) { + logDbg("ChildClosed for unopened opportunistic child; ignoring"); + return; + } + + sessionLost(mToken, null); + } + + @Override + public void onClosedExceptionally(@NonNull IkeException exception) { + logInfo("ChildClosedExceptionally for token " + mToken, exception); + + if (mIsOpportunistic && !mIsChildOpened) { + logInfo("ChildClosedExceptionally for unopened opportunistic child; ignoring"); + return; + } + + 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); + } + } + + // Used in Vcn.java, but must be public for mockito to mock this. + public String getLogPrefix() { + return "(" + + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup) + + "-" + + mConnectionConfig.getGatewayConnectionName() + + "-" + mId + + ") "; + } + + private String getTagLogPrefix() { + return "[ " + TAG + " " + getLogPrefix() + "]"; + } + + 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 logInfo(String msg) { + Slog.i(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[INFO] " + getTagLogPrefix() + msg); + } + + private void logInfo(String msg, Throwable tr) { + Slog.i(TAG, getLogPrefix() + msg, tr); + LOCAL_LOG.log("[INFO] " + getTagLogPrefix() + msg + tr); + } + + private void logWarn(String msg) { + Slog.w(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[WARN] " + getTagLogPrefix() + msg); + } + + private void logWarn(String msg, Throwable tr) { + Slog.w(TAG, getLogPrefix() + msg, tr); + LOCAL_LOG.log("[WARN] " + getTagLogPrefix() + msg + tr); + } + + private void logErr(String msg) { + Slog.e(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[ERR ] " + getTagLogPrefix() + msg); + } + + private void logErr(String msg, Throwable tr) { + Slog.e(TAG, getLogPrefix() + msg, tr); + LOCAL_LOG.log("[ERR ] " + getTagLogPrefix() + msg + tr); + } + + private void logWtf(String msg) { + Slog.wtf(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[WTF ] " + msg); + } + + private void logWtf(String msg, Throwable tr) { + Slog.wtf(TAG, getLogPrefix() + msg, tr); + LOCAL_LOG.log("[WTF ] " + msg + tr); + } + + /** + * Dumps the state of this VcnGatewayConnection for logging and debugging purposes. + * + *

PII and credentials MUST NEVER be dumped here. + * + *

This method is not thread safe and MUST run on the VCN thread. + */ + public void dump(IndentingPrintWriter pw) { + mVcnContext.ensureRunningOnLooperThread(); + + 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(); + + mUnderlyingNetworkController.dump(pw); + pw.println(); + + if (mIkeSession == null) { + pw.println("mIkeSession: null"); + } else { + pw.println("mIkeSession:"); + + // Add a try catch block in case IkeSession#dump is not thread-safe + try { + mIkeSession.dump(pw); + } catch (Exception e) { + Slog.wtf(TAG, "Failed to dump IkeSession: " + e); + } + } + + pw.decreaseIndent(); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + void setTunnelInterface(IpSecTunnelInterface tunnelIface) { + mTunnelIface = tunnelIface; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + UnderlyingNetworkControllerCallback getUnderlyingNetworkControllerCallback() { + return mUnderlyingNetworkControllerCallback; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + ConnectivityDiagnosticsCallback getConnectivityDiagnosticsCallback() { + return mConnectivityDiagnosticsCallback; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + UnderlyingNetworkRecord getUnderlyingNetwork() { + return mUnderlying; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + void setUnderlyingNetwork(@Nullable UnderlyingNetworkRecord record) { + mUnderlying = record; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + IkeSessionConnectionInfo getIkeConnectionInfo() { + return mIkeConnectionInfo; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + boolean isQuitting() { + return mIsQuitting.getValue(); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + @VcnMetrics.GatewayTeardownReason + int getTeardownReason() { + return mTeardownReason; + } + + @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(); + } + + private ChildSessionParams buildOpportunisticChildParams() { + final ChildSessionParams baseParams = + mConnectionConfig.getTunnelConnectionParams().getTunnelModeChildSessionParams(); + + final TunnelModeChildSessionParams.Builder builder = + new TunnelModeChildSessionParams.Builder(); + for (ChildSaProposal proposal : baseParams.getChildSaProposals()) { + builder.addChildSaProposal(proposal); + } + + for (IkeTrafficSelector inboundSelector : baseParams.getInboundTrafficSelectors()) { + builder.addInboundTrafficSelectors(inboundSelector); + } + + for (IkeTrafficSelector outboundSelector : baseParams.getOutboundTrafficSelectors()) { + builder.addOutboundTrafficSelectors(outboundSelector); + } + + builder.setLifetimeSeconds( + baseParams.getHardLifetimeSeconds(), baseParams.getSoftLifetimeSeconds()); + + return builder.build(); + } + + @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 VcnMetrics. */ + public VcnMetrics newVcnMetrics() { + return new VcnMetrics(); + } + + /** Builds a new UnderlyingNetworkController. */ + public UnderlyingNetworkController newUnderlyingNetworkController( + VcnContext vcnContext, + VcnGatewayConnectionConfig connectionConfig, + int gatewayConnectionId, + ParcelUuid subscriptionGroup, + TelephonySubscriptionSnapshot snapshot, + UnderlyingNetworkControllerCallback callback) { + return new UnderlyingNetworkController( + vcnContext, + connectionConfig, + gatewayConnectionId, + 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 networkUnwantedCallback, + @NonNull Consumer 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; + } + } + + /** Gets the max number of parallel tunnels allowed for tunnel aggregation. */ + public int getParallelTunnelCount( + TelephonySubscriptionSnapshot snapshot, ParcelUuid subGrp) { + PersistableBundleWrapper carrierConfig = snapshot.getCarrierConfigForSubGrp(subGrp); + int result = TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT; + + if (carrierConfig != null) { + result = + carrierConfig.getInt( + VcnManager.VCN_TUNNEL_AGGREGATION_SA_COUNT_MAX_KEY, + TUNNEL_AGGREGATION_SA_COUNT_MAX_DEFAULT); + } + + // Guard against tunnel count < 1 + return Math.max(1, result); + } + } + + /** + * Proxy implementation of Child Session Configuration, used for testing. + * + *

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. + * + *

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 getInternalAddresses() { + return mChildConfig.getInternalAddresses(); + } + + /** Retrieves the DNS servers to be used inside the tunnel. */ + public List 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); + } + + /** Dumps the state of the IkeSession */ + public void dump(@NonNull IndentingPrintWriter pw) { + mImpl.dump(pw); + } + } + + /** 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. + * + *

Synchronize this action to minimize locking around WakeLock use. + */ + // WakelockTimeout suppressed because the time the wake lock is needed for is unknown. The + // wakelock is only acquired when a Message is sent to this state machine and will be + // released when the message is processed or the state machin quits + @SuppressLint("WakelockTimeout") + public synchronized void acquire() { + mImpl.acquire(); + } + + /** + * Release this Wakelock. + * + *

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; + private final int mId; + + 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 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); + } + }; + mId = System.identityHashCode(this); + } + + /** 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 underlyingNetworks) { + mImpl.setUnderlyingNetworks(underlyingNetworks); + } + + /** Retrieves the identity hash code of the underlying NetworkAgent */ + public int getIdentityHashCode() { + return mId; + } + + /** Retrieves the Network for the underlying NetworkAgent */ + @Nullable + public Network getNetwork() { + return mImpl.getNetwork(); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java b/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java new file mode 100644 index 0000000000..bc815eb274 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/VcnNetworkProvider.java @@ -0,0 +1,233 @@ +/* + * 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_NOT_RESTRICTED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; + +import static com.android.server.VcnManagementService.VDBG; + +import android.annotation.NonNull; +import android.annotation.TargetApi; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkCapabilities; +import android.net.NetworkProvider; +import android.net.NetworkRequest; +import android.net.NetworkScore; +import android.net.vcn.VcnGatewayConnectionConfig; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.util.IndentingPrintWriter; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.modules.utils.HandlerExecutor; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; + +/** + * VCN Network Provider routes NetworkRequests to listeners to bring up tunnels as needed. + * + *

The VcnNetworkProvider provides a caching layer to ensure that all listeners receive all + * active NetworkRequest(s), including ones that were filed prior to listener registration. + * + * @hide + */ +// TODO(b/388919146): Implement a more generic solution to prevent concurrent modifications on +// mListeners and mRequests +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class VcnNetworkProvider extends NetworkProvider { + private static final String TAG = VcnNetworkProvider.class.getSimpleName(); + + private final Set mListeners = ConcurrentHashMap.newKeySet(); + + private final Context mContext; + private final Handler mHandler; + private final Dependencies mDeps; + + /** + * Cache of NetworkRequest(s). + * + *

NetworkRequests are immutable once created, and therefore can be used as stable keys. + */ + private final Set mRequests = ConcurrentHashMap.newKeySet(); + + public VcnNetworkProvider(@NonNull Context context, @NonNull Looper looper) { + this(context, looper, new Dependencies()); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public VcnNetworkProvider( + @NonNull Context context, @NonNull Looper looper, @NonNull Dependencies dependencies) { + super( + Objects.requireNonNull(context, "Missing context"), + Objects.requireNonNull(looper, "Missing looper"), + TAG); + + mContext = context; + mHandler = new Handler(looper); + mDeps = Objects.requireNonNull(dependencies, "Missing dependencies"); + } + + /** Registers this VcnNetworkProvider and a generic network offer with ConnectivityService. */ + public void register() { + mContext.getSystemService(ConnectivityManager.class).registerNetworkProvider(this); + mDeps.registerNetworkOffer( + this, + Vcn.getNetworkScore(), // score filter + buildCapabilityFilter(), + new HandlerExecutor(mHandler), + new NetworkOfferCallback() { + @Override + public void onNetworkNeeded(@NonNull NetworkRequest request) { + handleNetworkRequested(request); + } + + @Override + public void onNetworkUnneeded(@NonNull NetworkRequest request) { + handleNetworkRequestWithdrawn(request); + } + }); + } + + /** Builds the filter for NetworkRequests that can be served by the VcnNetworkProvider. */ + private NetworkCapabilities buildCapabilityFilter() { + final NetworkCapabilities.Builder builder = + new NetworkCapabilities.Builder() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_TRUSTED) + .addCapability(NET_CAPABILITY_NOT_RESTRICTED) + .addCapability(NET_CAPABILITY_NOT_VPN) + .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED); + + for (int cap : VcnGatewayConnectionConfig.ALLOWED_CAPABILITIES) { + builder.addCapability(cap); + } + + return builder.build(); + } + + /** + * Registers a NetworkRequestListener with this NetworkProvider. + * + *

Upon registering, the provided listener will receive all cached requests. + */ + @VisibleForTesting(visibility = Visibility.PACKAGE) + public void registerListener(@NonNull NetworkRequestListener listener) { + mListeners.add(listener); + + // Send listener all cached requests + resendAllRequests(listener); + } + + /** Unregisters the specified listener from receiving future NetworkRequests. */ + @VisibleForTesting(visibility = Visibility.PACKAGE) + public void unregisterListener(@NonNull NetworkRequestListener listener) { + mListeners.remove(listener); + } + + /** Sends all cached NetworkRequest(s) to the specified listener. */ + @VisibleForTesting(visibility = Visibility.PACKAGE) + public void resendAllRequests(@NonNull NetworkRequestListener listener) { + for (NetworkRequest request : mRequests) { + notifyListenerForEvent(listener, request); + } + } + + private void notifyListenerForEvent( + @NonNull NetworkRequestListener listener, @NonNull NetworkRequest request) { + listener.onNetworkRequested(request); + } + + private void handleNetworkRequested(@NonNull NetworkRequest request) { + if (VDBG) { + Slog.v(TAG, "Network requested: Request = " + request); + } + + mRequests.add(request); + + // TODO(b/176939047): Intelligently route requests to prioritized VcnInstances (based on + // Default Data Sub, or similar) + for (NetworkRequestListener listener : mListeners) { + notifyListenerForEvent(listener, request); + } + } + + private void handleNetworkRequestWithdrawn(@NonNull NetworkRequest request) { + if (VDBG) { + Slog.v(TAG, "Network request withdrawn: Request = " + request); + } + + mRequests.remove(request); + } + + // package-private + interface NetworkRequestListener { + void onNetworkRequested(@NonNull NetworkRequest request); + } + + /** + * 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:"); + pw.increaseIndent(); + for (NetworkRequestListener listener : mListeners) { + pw.println(listener); + } + pw.decreaseIndent(); + pw.println(); + + pw.println("mRequests:"); + pw.increaseIndent(); + for (NetworkRequest request : mRequests) { + pw.println(request); + } + pw.decreaseIndent(); + pw.println(); + + pw.decreaseIndent(); + } + + /** Proxy class for dependencies used for testing. */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + /** Registers a given network offer for the given provider. */ + public void registerNetworkOffer( + @NonNull VcnNetworkProvider provider, + @NonNull NetworkScore score, + @NonNull NetworkCapabilities capabilitiesFilter, + @NonNull Executor executor, + @NonNull NetworkOfferCallback callback) { + provider.registerNetworkOffer(score, capabilitiesFilter, executor, callback); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/metrics/VcnMetrics.java b/Vcn/service-b/src/com/android/server/vcn/metrics/VcnMetrics.java new file mode 100644 index 0000000000..021606bb6d --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/metrics/VcnMetrics.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2025 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.metrics; + +import static com.android.server.vcn.metrics.VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_INTERNAL_ERROR; +import static com.android.server.vcn.metrics.VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_NETWORK_AGENT_UNWANTED; +import static com.android.server.vcn.metrics.VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_NONE; +import static com.android.server.vcn.metrics.VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_REQUESTED; +import static com.android.server.vcn.metrics.VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_UNSPECIFIED; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** Utility class for logging VCN metrics. */ +public class VcnMetrics { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"GATEWAY_TEARDOWN_REASON_"}, + value = { + GATEWAY_TEARDOWN_REASON_NONE, + GATEWAY_TEARDOWN_REASON_INTERNAL_ERROR, + GATEWAY_TEARDOWN_REASON_NETWORK_AGENT_UNWANTED, + GATEWAY_TEARDOWN_REASON_REQUESTED + }) + public @interface GatewayTeardownReason {} + + public static final int GATEWAY_TEARDOWN_REASON_NONE = + VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_NONE; + public static final int GATEWAY_TEARDOWN_REASON_INTERNAL_ERROR = + VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_INTERNAL_ERROR; + public static final int GATEWAY_TEARDOWN_REASON_NETWORK_AGENT_UNWANTED = + VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_NETWORK_AGENT_UNWANTED; + public static final int GATEWAY_TEARDOWN_REASON_REQUESTED = + VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_REQUESTED; + public static final int GATEWAY_TEARDOWN_REASON_UNSPECIFIED = + VCN_GATEWAY_CONNECTION_STATE_CHANGED__GW_TEARDOWN_REASON__GATEWAY_TEARDOWN_REASON_UNSPECIFIED; + + /** Log an atom when a VcnGatewayConnection has entered safe mode. */ + public void logEnterSafeMode(int gatewayConnectionId) { + VcnStatsLog.write( + VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED, + gatewayConnectionId, + GATEWAY_TEARDOWN_REASON_NONE, + true /* isInSafeMode */); + } + + /** Log an atom when a VcnGatewayConnection has exited safe mode. */ + public void logExitSafeMode(int gatewayConnectionId) { + VcnStatsLog.write( + VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED, + gatewayConnectionId, + GATEWAY_TEARDOWN_REASON_NONE, + false /* isInSafeMode */); + } + + /** + * Log an atom when a VcnGatewayConnection has been torn down with reason. It will also reset + * other VcnGatewayConnection related states i.e. safemode. + */ + public void logVcnGatewayTeardown(int gatewayConnectionId, @GatewayTeardownReason int reason) { + VcnStatsLog.write( + VcnStatsLog.VCN_GATEWAY_CONNECTION_STATE_CHANGED, + gatewayConnectionId, + reason, + false /* isInSafeMode */); + } + + /** Log an atom when VCN network is connected. */ + public void logVcnNetworkConnected(int gatewayConnectionId, int networkId) { + VcnStatsLog.write( + VcnStatsLog.VCN_NETWORK_STATE_CHANGED, + gatewayConnectionId, + networkId, + true /* isConnected */, + false /* isValidated */); + } + + /** + * Log an atom when VCN network is being torn down. It will also reset other state related to + * the network i.e. validated. + */ + public void logVcnNetworkNotConnected(int gatewayConnectionId, int networkId) { + VcnStatsLog.write( + VcnStatsLog.VCN_NETWORK_STATE_CHANGED, + gatewayConnectionId, + networkId, + false /* isConnected */, + false /* isValidated */); + } + + /** Log an atom when VCN network has been validated. */ + public void logVcnNetworkValidated(int gatewayConnectionId, int networkId) { + VcnStatsLog.write( + VcnStatsLog.VCN_NETWORK_STATE_CHANGED, + gatewayConnectionId, + networkId, + true /* isConnected */, + true /* isValidated */); + } + + /** Log an atom when VCN network has been not validated. */ + public void logVcnNetworkNotValidated(int gatewayConnectionId, int networkId) { + VcnStatsLog.write( + VcnStatsLog.VCN_NETWORK_STATE_CHANGED, + gatewayConnectionId, + networkId, + true /* isConnected */, + false /* isValidated */); + } + + /** + * Log an atom about number of validated underlying network available for VCN network selection. + */ + public void logValidatedUnderlyingNetworkCount(int gatewayConnectionId, int count) { + VcnStatsLog.write( + VcnStatsLog.VCN_UNDERLYING_NETWORKS_STATE_CHANGED, gatewayConnectionId, count); + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java new file mode 100644 index 0000000000..aff7068b6c --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -0,0 +1,605 @@ +/* + * Copyright (C) 2023 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.routeselection; + +import static android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; + +import static com.android.internal.annotations.VisibleForTesting.Visibility; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.IpSecTransformState; +import android.net.Network; +import android.net.vcn.VcnManager; +import android.os.Build; +import android.os.Handler; +import android.os.OutcomeReceiver; +import android.os.PowerManager; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.modules.utils.HandlerExecutor; +import com.android.server.vcn.VcnContext; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.BitSet; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * IpSecPacketLossDetector is responsible for continuously monitoring IPsec packet loss + * + *

When the packet loss rate surpass the threshold, IpSecPacketLossDetector will report it to the + * caller + * + *

IpSecPacketLossDetector will start monitoring when the network being monitored is selected AND + * an inbound IpSecTransform has been applied to this network. + * + *

This class is flag gated by "network_metric_monitor" and "ipsec_tramsform_state" + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class IpSecPacketLossDetector extends NetworkMetricMonitor { + private static final String TAG = IpSecPacketLossDetector.class.getSimpleName(); + + private static final int PACKET_LOSS_PERCENT_UNAVAILABLE = -1; + + // Ignore the packet loss detection result if the expected packet number is smaller than 10. + // Solarwinds NPM uses 10 ICMP echos to calculate packet loss rate (as per + // https://thwack.solarwinds.com/products/network-performance-monitor-npm/f/forum/63829/how-is-packet-loss-calculated) + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int MIN_VALID_EXPECTED_RX_PACKET_NUM = 10; + + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"PACKET_LOSS_"}, + value = { + PACKET_LOSS_RATE_VALID, + PACKET_LOSS_RATE_INVALID, + PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP, + }) + @Target({ElementType.TYPE_USE}) + private @interface PacketLossResultType {} + + /** Indicates a valid packet loss rate is available */ + private static final int PACKET_LOSS_RATE_VALID = 0; + + /** + * Indicates that the detector cannot get a valid packet loss rate due to one of the following + * reasons: + * + *

    + *
  • The replay window did not proceed and thus all packets might have been delivered out of + * order + *
  • The expected received packet number is too small and thus the detection result is not + * reliable + *
  • There are unexpected errors + *
+ */ + private static final int PACKET_LOSS_RATE_INVALID = 1; + + /** + * The sequence number increase is unusually large and might be caused an intentional leap on + * the server's downlink + * + *

Inbound sequence number will not always increase consecutively. During load balancing the + * server might add a big leap on the sequence number intentionally. In such case a high packet + * loss rate does not always indicate a lossy network + */ + private static final int PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP = 2; + + // For VoIP, losses between 5% and 10% of the total packet stream will affect the quality + // significantly (as per "Computer Networking for LANS to WANS: Hardware, Software and + // Security"). For audio and video streaming, above 10-12% packet loss is unacceptable (as per + // "ICTP-SDU: About PingER"). Thus choose 12% as a conservative default threshold to declare a + // validation failure. + private static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT = 12; + + /** Carriers can disable the detector by setting the threshold to -1 */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR = -1; + + private static final int POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT = 20; + + // By default, there's no maximum limit enforced + private static final int MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED = -1; + + private long mPollIpSecStateIntervalMs; + private int mPacketLossRatePercentThreshold; + private int mMaxSeqNumIncreasePerSecond; + + @NonNull private final Handler mHandler; + @NonNull private final PowerManager mPowerManager; + @NonNull private final ConnectivityManager mConnectivityManager; + @NonNull private final Object mCancellationToken = new Object(); + @NonNull private final PacketLossCalculator mPacketLossCalculator; + + @Nullable private BroadcastReceiver mDeviceIdleReceiver; + + @Nullable private IpSecTransformWrapper mInboundTransform; + @Nullable private IpSecTransformState mLastIpSecTransformState; + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public IpSecPacketLossDetector( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkMetricMonitorCallback callback, + @NonNull Dependencies deps) + throws IllegalAccessException { + super(vcnContext, network, carrierConfig, callback); + + Objects.requireNonNull(deps, "Missing deps"); + + mHandler = new Handler(getVcnContext().getLooper()); + + mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class); + mConnectivityManager = + getVcnContext().getContext().getSystemService(ConnectivityManager.class); + + mPacketLossCalculator = deps.getPacketLossCalculator(); + + mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig); + mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig); + mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig); + + // Register for system broadcasts to monitor idle mode change + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + + mDeviceIdleReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED.equals( + intent.getAction()) + && mPowerManager.isDeviceIdleMode()) { + mLastIpSecTransformState = null; + } + } + }; + getVcnContext() + .getContext() + .registerReceiver( + mDeviceIdleReceiver, + intentFilter, + null /* broadcastPermission not required */, + mHandler); + } + + public IpSecPacketLossDetector( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkMetricMonitorCallback callback) + throws IllegalAccessException { + this(vcnContext, network, carrierConfig, callback, new Dependencies()); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + public PacketLossCalculator getPacketLossCalculator() { + return new PacketLossCalculator(); + } + } + + private static long getPollIpSecStateIntervalMs( + @Nullable PersistableBundleWrapper carrierConfig) { + final int seconds; + + if (carrierConfig != null) { + seconds = + carrierConfig.getInt( + VcnManager.VCN_NETWORK_SELECTION_POLL_IPSEC_STATE_INTERVAL_SECONDS_KEY, + POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT); + } else { + seconds = POLL_IPSEC_STATE_INTERVAL_SECONDS_DEFAULT; + } + + return TimeUnit.SECONDS.toMillis(seconds); + } + + private static int getPacketLossRatePercentThreshold( + @Nullable PersistableBundleWrapper carrierConfig) { + if (carrierConfig != null) { + return carrierConfig.getInt( + VcnManager.VCN_NETWORK_SELECTION_IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_KEY, + IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT); + } + return IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DEFAULT; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + static int getMaxSeqNumIncreasePerSecond(@Nullable PersistableBundleWrapper carrierConfig) { + int maxSeqNumIncrease = MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED; + if (carrierConfig != null) { + maxSeqNumIncrease = + carrierConfig.getInt( + VcnManager.VCN_NETWORK_SELECTION_MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY, + MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED); + } + + if (maxSeqNumIncrease < MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED) { + logE(TAG, "Invalid value of MAX_SEQ_NUM_INCREASE_PER_SECOND_KEY " + maxSeqNumIncrease); + return MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED; + } + + return maxSeqNumIncrease; + } + + @Override + protected void onSelectedUnderlyingNetworkChanged() { + if (!isSelectedUnderlyingNetwork()) { + mInboundTransform = null; + stop(); + } + + // No action when the underlying network got selected. Wait for the inbound transform to + // start the monitor + } + + @Override + public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inboundTransform) { + Objects.requireNonNull(inboundTransform, "inboundTransform is null"); + + if (Objects.equals(inboundTransform, mInboundTransform)) { + return; + } + + if (!isSelectedUnderlyingNetwork()) { + logWtf("setInboundTransform called but network not selected"); + return; + } + + // When multiple parallel inbound transforms are created, NetworkMetricMonitor will be + // enabled on the last one as a sample + mInboundTransform = inboundTransform; + + if (canStart()) { + start(); + } + } + + @Override + public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) { + // The already scheduled event will not be affected. The followup events will be scheduled + // with the new interval + mPollIpSecStateIntervalMs = getPollIpSecStateIntervalMs(carrierConfig); + + mPacketLossRatePercentThreshold = getPacketLossRatePercentThreshold(carrierConfig); + mMaxSeqNumIncreasePerSecond = getMaxSeqNumIncreasePerSecond(carrierConfig); + + if (canStart() != isStarted()) { + if (canStart()) { + start(); + } else { + stop(); + } + } + } + + @Override + public void onLinkPropertiesOrCapabilitiesChanged() { + if (!isStarted()) return; + + reschedulePolling(); + } + + private void reschedulePolling() { + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L); + } + + private boolean canStart() { + return mInboundTransform != null + && mPacketLossRatePercentThreshold + != IPSEC_PACKET_LOSS_PERCENT_THRESHOLD_DISABLE_DETECTOR; + } + + @Override + protected void start() { + super.start(); + clearTransformStateAndPollingEvents(); + mHandler.postDelayed(new PollIpSecStateRunnable(), mCancellationToken, 0L); + } + + @Override + public void stop() { + super.stop(); + clearTransformStateAndPollingEvents(); + } + + private void clearTransformStateAndPollingEvents() { + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + mLastIpSecTransformState = null; + } + + @Override + public void close() { + super.close(); + + if (mInboundTransform != null) { + mInboundTransform = null; + } + + if (mDeviceIdleReceiver != null) { + getVcnContext().getContext().unregisterReceiver(mDeviceIdleReceiver); + mDeviceIdleReceiver = null; + } + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + @Nullable + public IpSecTransformState getLastTransformState() { + return mLastIpSecTransformState; + } + + @VisibleForTesting(visibility = Visibility.PROTECTED) + @Nullable + public IpSecTransformWrapper getInboundTransformInternal() { + return mInboundTransform; + } + + private class PollIpSecStateRunnable implements Runnable { + @Override + public void run() { + if (!isStarted()) { + logWtf("Monitor stopped but PollIpSecStateRunnable not removed from Handler"); + return; + } + + getInboundTransformInternal() + .requestIpSecTransformState( + new HandlerExecutor(mHandler), new IpSecTransformStateReceiver()); + + // Schedule for next poll + mHandler.postDelayed( + new PollIpSecStateRunnable(), mCancellationToken, mPollIpSecStateIntervalMs); + } + } + + private class IpSecTransformStateReceiver + implements OutcomeReceiver { + @Override + public void onResult(@NonNull IpSecTransformState state) { + getVcnContext().ensureRunningOnLooperThread(); + + if (!isStarted()) { + return; + } + + onIpSecTransformStateReceived(state); + } + + @Override + public void onError(@NonNull RuntimeException error) { + getVcnContext().ensureRunningOnLooperThread(); + + // Nothing we can do here + logW("TransformStateReceiver#onError " + error.toString()); + } + } + + private void onIpSecTransformStateReceived(@NonNull IpSecTransformState state) { + if (mLastIpSecTransformState == null) { + // This is first time to poll the state + mLastIpSecTransformState = state; + return; + } + + final PacketLossCalculationResult calculateResult = + mPacketLossCalculator.getPacketLossRatePercentage( + mLastIpSecTransformState, + state, + mMaxSeqNumIncreasePerSecond, + getLogPrefix()); + + if (calculateResult.getResultType() == PACKET_LOSS_RATE_INVALID) { + return; + } + + final String logMsg = + "calculateResult: " + + calculateResult + + "% in the past " + + (state.getTimestampMillis() + - mLastIpSecTransformState.getTimestampMillis()) + + "ms"; + + mLastIpSecTransformState = state; + if (calculateResult.getPacketLossRatePercent() < mPacketLossRatePercentThreshold) { + logV(logMsg); + + // In both "valid" or "unusual_seq_num_leap" cases, notify that the network has passed + // the validation + onValidationResultReceivedInternal(false /* isFailed */); + } else { + logInfo(logMsg); + + if (calculateResult.getResultType() == PACKET_LOSS_RATE_VALID) { + onValidationResultReceivedInternal(true /* isFailed */); + } + + // In both "invalid" and "unusual_seq_num_leap" cases, trigger network validation. If + // validation fails, the VCN will attempt to migrate away. + mConnectivityManager.reportNetworkConnectivity( + getNetwork(), false /* hasConnectivity */); + } + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class PacketLossCalculator { + /** Calculate the packet loss rate between two timestamps */ + public PacketLossCalculationResult getPacketLossRatePercentage( + @NonNull IpSecTransformState oldState, + @NonNull IpSecTransformState newState, + int maxSeqNumIncreasePerSecond, + String logPrefix) { + logVIpSecTransform("oldState", oldState, logPrefix); + logVIpSecTransform("newState", newState, logPrefix); + + final int replayWindowSize = oldState.getReplayBitmap().length * 8; + final long oldSeqHi = oldState.getRxHighestSequenceNumber(); + final long oldSeqLow = Math.max(0L, oldSeqHi - replayWindowSize + 1); + final long newSeqHi = newState.getRxHighestSequenceNumber(); + final long newSeqLow = Math.max(0L, newSeqHi - replayWindowSize + 1); + + if (oldSeqHi == newSeqHi || newSeqHi < replayWindowSize) { + // The replay window did not proceed and all packets might have been delivered out + // of order + return PacketLossCalculationResult.invalid(); + } + + boolean isUnusualSeqNumLeap = false; + + // Handle sequence number leap + if (maxSeqNumIncreasePerSecond != MAX_SEQ_NUM_INCREASE_DEFAULT_DISABLED) { + final long timeDiffMillis = + newState.getTimestampMillis() - oldState.getTimestampMillis(); + final long maxSeqNumIncrease = timeDiffMillis * maxSeqNumIncreasePerSecond / 1000; + + // Sequence numbers are unsigned 32-bit values. If maxSeqNumIncrease overflows, + // isUnusualSeqNumLeap can never be true. + if (maxSeqNumIncrease >= 0 && newSeqHi - oldSeqHi >= maxSeqNumIncrease) { + isUnusualSeqNumLeap = true; + } + } + + // Get the expected packet count by assuming there is no packet loss. In this case, SA + // should receive all packets whose sequence numbers are smaller than the lower bound of + // the replay window AND the packets received within the window. + // When the lower bound is 0, it's not possible to tell whether packet with seqNo 0 is + // received or not. For simplicity just assume that packet is received. + final long newExpectedPktCnt = newSeqLow + getPacketCntInReplayWindow(newState); + final long oldExpectedPktCnt = oldSeqLow + getPacketCntInReplayWindow(oldState); + + final long expectedPktCntDiff = newExpectedPktCnt - oldExpectedPktCnt; + final long actualPktCntDiff = newState.getPacketCount() - oldState.getPacketCount(); + + logV( + TAG, + logPrefix + + " expectedPktCntDiff: " + + expectedPktCntDiff + + " actualPktCntDiff: " + + actualPktCntDiff); + + if (expectedPktCntDiff < MIN_VALID_EXPECTED_RX_PACKET_NUM) { + // The sample size is too small to ensure a reliable detection result + return PacketLossCalculationResult.invalid(); + } + + if (expectedPktCntDiff < 0 + || expectedPktCntDiff == 0 + || actualPktCntDiff < 0 + || actualPktCntDiff > expectedPktCntDiff) { + logWtf(TAG, "Impossible values for expectedPktCntDiff or" + " actualPktCntDiff"); + return PacketLossCalculationResult.invalid(); + } + + final int percent = 100 - (int) (actualPktCntDiff * 100 / expectedPktCntDiff); + return isUnusualSeqNumLeap + ? PacketLossCalculationResult.unusualSeqNumLeap(percent) + : PacketLossCalculationResult.valid(percent); + } + } + + private static void logVIpSecTransform( + String transformTag, IpSecTransformState state, String logPrefix) { + final String stateString = + " seqNo: " + + state.getRxHighestSequenceNumber() + + " | pktCnt: " + + state.getPacketCount() + + " | pktCntInWindow: " + + getPacketCntInReplayWindow(state); + logV(TAG, logPrefix + " " + transformTag + stateString); + } + + /** Get the number of received packets within the replay window */ + private static long getPacketCntInReplayWindow(@NonNull IpSecTransformState state) { + return BitSet.valueOf(state.getReplayBitmap()).cardinality(); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class PacketLossCalculationResult { + @PacketLossResultType private final int mResultType; + private final int mPacketLossRatePercent; + + private PacketLossCalculationResult(@PacketLossResultType int type, int percent) { + mResultType = type; + mPacketLossRatePercent = percent; + } + + /** Construct an instance that contains a valid packet loss rate */ + public static PacketLossCalculationResult valid(int percent) { + return new PacketLossCalculationResult(PACKET_LOSS_RATE_VALID, percent); + } + + /** Construct an instance indicating the inability to get a valid packet loss rate */ + public static PacketLossCalculationResult invalid() { + return new PacketLossCalculationResult( + PACKET_LOSS_RATE_INVALID, PACKET_LOSS_PERCENT_UNAVAILABLE); + } + + /** Construct an instance indicating that there is an unusual sequence number leap */ + public static PacketLossCalculationResult unusualSeqNumLeap(int percent) { + return new PacketLossCalculationResult(PACKET_LOSS_UNUSUAL_SEQ_NUM_LEAP, percent); + } + + @PacketLossResultType + public int getResultType() { + return mResultType; + } + + public int getPacketLossRatePercent() { + return mPacketLossRatePercent; + } + + @Override + public int hashCode() { + return Objects.hash(mResultType, mPacketLossRatePercent); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof PacketLossCalculationResult)) { + return false; + } + + final PacketLossCalculationResult rhs = (PacketLossCalculationResult) other; + return mResultType == rhs.mResultType + && mPacketLossRatePercent == rhs.mPacketLossRatePercent; + } + + @Override + public String toString() { + return "mResultType: " + + mResultType + + " | mPacketLossRatePercent: " + + mPacketLossRatePercent; + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java b/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java new file mode 100644 index 0000000000..fc9c7ac8a3 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkMetricMonitor.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2023 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.routeselection; + +import static android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; + +import static com.android.server.VcnManagementService.LOCAL_LOG; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.net.IpSecTransform; +import android.net.IpSecTransformState; +import android.net.Network; +import android.os.Build; +import android.os.OutcomeReceiver; +import android.util.CloseGuard; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.server.vcn.VcnContext; + +import java.util.Objects; +import java.util.concurrent.Executor; + +/** + * NetworkMetricMonitor is responsible for managing metric monitoring and tracking validation + * results. + * + *

This class is flag gated by "network_metric_monitor" + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public abstract class NetworkMetricMonitor implements AutoCloseable { + private static final String TAG = NetworkMetricMonitor.class.getSimpleName(); + + private static final boolean VDBG = false; // STOPSHIP: if true + + @NonNull private final CloseGuard mCloseGuard = new CloseGuard(); + + @NonNull private final VcnContext mVcnContext; + @NonNull private final Network mNetwork; + @NonNull private final NetworkMetricMonitorCallback mCallback; + + private boolean mIsSelectedUnderlyingNetwork; + private boolean mIsStarted; + private boolean mIsValidationFailed; + + protected NetworkMetricMonitor( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkMetricMonitorCallback callback) + throws IllegalAccessException { + mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext"); + mNetwork = Objects.requireNonNull(network, "Missing network"); + mCallback = Objects.requireNonNull(callback, "Missing callback"); + + mIsSelectedUnderlyingNetwork = false; + mIsStarted = false; + mIsValidationFailed = false; + } + + /** Callback to notify caller of the validation result */ + public interface NetworkMetricMonitorCallback { + /** Called when there is a validation result is ready */ + void onValidationResultReceived(); + } + + /** + * Start monitoring + * + *

This method might be called on a an already started monitor for updating monitor + * properties (e.g. IpSecTransform, carrier config) + * + *

Subclasses MUST call super.start() when overriding this method + */ + protected void start() { + mIsStarted = true; + } + + /** + * Stop monitoring + * + *

Subclasses MUST call super.stop() when overriding this method + */ + public void stop() { + mIsValidationFailed = false; + mIsStarted = false; + } + + /** Called by the subclasses when the validation result is ready */ + protected void onValidationResultReceivedInternal(boolean isFailed) { + mIsValidationFailed = isFailed; + mCallback.onValidationResultReceived(); + } + + /** Called when the underlying network changes to selected or unselected */ + protected abstract void onSelectedUnderlyingNetworkChanged(); + + /** + * Mark the network being monitored selected or unselected + * + *

Subclasses MUST call super when overriding this method + */ + public void setIsSelectedUnderlyingNetwork(boolean isSelectedUnderlyingNetwork) { + if (mIsSelectedUnderlyingNetwork == isSelectedUnderlyingNetwork) { + return; + } + + mIsSelectedUnderlyingNetwork = isSelectedUnderlyingNetwork; + onSelectedUnderlyingNetworkChanged(); + } + + /** Wrapper that allows injection for testing purposes */ + @VisibleForTesting(visibility = Visibility.PROTECTED) + public static class IpSecTransformWrapper { + @NonNull public final IpSecTransform ipSecTransform; + + public IpSecTransformWrapper(@NonNull IpSecTransform ipSecTransform) { + this.ipSecTransform = ipSecTransform; + } + + /** Poll an IpSecTransformState */ + public void requestIpSecTransformState( + @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + ipSecTransform.requestIpSecTransformState(executor, callback); + } + + /** Close this instance and release the underlying resources */ + public void close() { + ipSecTransform.close(); + } + + @Override + public int hashCode() { + return Objects.hash(ipSecTransform); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof IpSecTransformWrapper)) { + return false; + } + + final IpSecTransformWrapper other = (IpSecTransformWrapper) o; + + return Objects.equals(ipSecTransform, other.ipSecTransform); + } + } + + /** Set the IpSecTransform that is applied to the Network being monitored */ + public void setInboundTransform(@NonNull IpSecTransform inTransform) { + setInboundTransformInternal(new IpSecTransformWrapper(inTransform)); + } + + /** + * Set the IpSecTransform that applied to the Network being monitored * + * + *

Subclasses MUST call super when overriding this method + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + public void setInboundTransformInternal(@NonNull IpSecTransformWrapper inTransform) { + // Subclasses MUST override it if they care + } + + /** Update the carrierconfig */ + public void setCarrierConfig(@Nullable PersistableBundleWrapper carrierConfig) { + // Subclasses MUST override it if they care + } + + /** Called when LinkProperties or NetworkCapabilities have changed */ + public void onLinkPropertiesOrCapabilitiesChanged() { + // Subclasses MUST override it if they care + } + + public boolean isValidationFailed() { + return mIsValidationFailed; + } + + public boolean isSelectedUnderlyingNetwork() { + return mIsSelectedUnderlyingNetwork; + } + + public boolean isStarted() { + return mIsStarted; + } + + @NonNull + public VcnContext getVcnContext() { + return mVcnContext; + } + + @NonNull + public Network getNetwork() { + return mNetwork; + } + + // Override methods for AutoCloseable. Subclasses MUST call super when overriding this method + @Override + public void close() { + mCloseGuard.close(); + + stop(); + } + + // Override #finalize() to use closeGuard for flagging that #close() was not called + @SuppressWarnings("Finalize") + @Override + protected void finalize() throws Throwable { + try { + if (mCloseGuard != null) { + mCloseGuard.warnIfOpen(); + } + close(); + } finally { + super.finalize(); + } + } + + private String getClassName() { + return this.getClass().getSimpleName(); + } + + protected String getLogPrefix() { + return " [Network " + mNetwork + "] "; + } + + protected void logV(String msg) { + if (VDBG) { + Slog.v(getClassName(), getLogPrefix() + msg); + LOCAL_LOG.log("[VERBOSE ] " + getClassName() + getLogPrefix() + msg); + } + } + + protected void logInfo(String msg) { + Slog.i(getClassName(), getLogPrefix() + msg); + LOCAL_LOG.log("[INFO ] " + getClassName() + getLogPrefix() + msg); + } + + protected void logW(String msg) { + Slog.w(getClassName(), getLogPrefix() + msg); + LOCAL_LOG.log("[WARN ] " + getClassName() + getLogPrefix() + msg); + } + + protected void logWtf(String msg) { + Slog.wtf(getClassName(), getLogPrefix() + msg); + LOCAL_LOG.log("[WTF ] " + getClassName() + getLogPrefix() + msg); + } + + protected static void logV(String className, String msgWithPrefix) { + if (VDBG) { + Slog.wtf(className, msgWithPrefix); + LOCAL_LOG.log("[VERBOSE ] " + className + msgWithPrefix); + } + } + + protected static void logE(String className, String msgWithPrefix) { + Slog.w(className, msgWithPrefix); + LOCAL_LOG.log("[ERROR ] " + className + msgWithPrefix); + } + + protected static void logWtf(String className, String msgWithPrefix) { + Slog.wtf(className, msgWithPrefix); + LOCAL_LOG.log("[WTF ] " + className + msgWithPrefix); + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java b/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java new file mode 100644 index 0000000000..7cb3257193 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/routeselection/NetworkPriorityClassifier.java @@ -0,0 +1,367 @@ +/* + * 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.routeselection; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING; +import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.NetworkCapabilities.TRANSPORT_WIFI; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_FORBIDDEN; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED; +import static android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; + +import static com.android.server.VcnManagementService.LOCAL_LOG; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.net.NetworkCapabilities; +import android.net.TelephonyNetworkSpecifier; +import android.net.vcn.VcnCellUnderlyingNetworkTemplate; +import android.net.vcn.VcnManager; +import android.net.vcn.VcnUnderlyingNetworkTemplate; +import android.net.vcn.VcnWifiUnderlyingNetworkTemplate; +import android.os.Build; +import android.os.ParcelUuid; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import com.android.server.vcn.VcnContext; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** @hide */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +class NetworkPriorityClassifier { + @NonNull private static final String TAG = NetworkPriorityClassifier.class.getSimpleName(); + /** + * Minimum signal strength for a WiFi network to be eligible for switching to + * + *

A network that satisfies this is eligible to become the selected underlying network with + * no additional conditions + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT = -70; + /** + * Minimum signal strength to continue using a WiFi network + * + *

A network that satisfies the conditions may ONLY continue to be used if it is already + * selected as the underlying network. A WiFi network satisfying this condition, but NOT the + * prospective-network RSSI threshold CANNOT be switched to. + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int WIFI_EXIT_RSSI_THRESHOLD_DEFAULT = -74; + + /** + * Priority for networks that VCN can fall back to. + * + *

If none of the network candidates are validated or match any template, VCN will fall back + * to any INTERNET network. + */ + @VisibleForTesting(visibility = Visibility.PRIVATE) + static final int PRIORITY_FALLBACK = Integer.MAX_VALUE; + + /** + * Priority for networks that cannot be selected as VCN's underlying networks. + * + *

VCN MUST never select a non-INTERNET network that are unvalidated or fail to match any + * template as the underlying network. + */ + static final int PRIORITY_INVALID = -1; + + /** Gives networks a priority class, based on configured VcnGatewayConnectionConfig */ + public static int calculatePriorityClass( + VcnContext vcnContext, + UnderlyingNetworkRecord networkRecord, + List underlyingNetworkTemplates, + ParcelUuid subscriptionGroup, + TelephonySubscriptionSnapshot snapshot, + boolean isSelected, + PersistableBundleWrapper carrierConfig) { + // mRouteSelectionNetworkRequest requires a network be both VALIDATED and NOT_SUSPENDED + + if (networkRecord.isBlocked) { + logWtf("Network blocked for System Server: " + networkRecord.network); + return PRIORITY_INVALID; + } + + if (snapshot == null) { + logWtf("Got null snapshot"); + return PRIORITY_INVALID; + } + + int priorityIndex = 0; + for (VcnUnderlyingNetworkTemplate nwPriority : underlyingNetworkTemplates) { + if (checkMatchesPriorityRule( + vcnContext, + nwPriority, + networkRecord, + subscriptionGroup, + snapshot, + isSelected, + carrierConfig)) { + return priorityIndex; + } + priorityIndex++; + } + + final NetworkCapabilities caps = networkRecord.networkCapabilities; + if (caps.hasCapability(NET_CAPABILITY_INTERNET) + || (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST))) { + return PRIORITY_FALLBACK; + } + return PRIORITY_INVALID; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static boolean checkMatchesPriorityRule( + VcnContext vcnContext, + VcnUnderlyingNetworkTemplate networkPriority, + UnderlyingNetworkRecord networkRecord, + ParcelUuid subscriptionGroup, + TelephonySubscriptionSnapshot snapshot, + boolean isSelected, + PersistableBundleWrapper carrierConfig) { + final NetworkCapabilities caps = networkRecord.networkCapabilities; + + final int meteredMatch = networkPriority.getMetered(); + final boolean isMetered = !caps.hasCapability(NET_CAPABILITY_NOT_METERED); + if (meteredMatch == MATCH_REQUIRED && !isMetered + || meteredMatch == MATCH_FORBIDDEN && isMetered) { + return false; + } + + // Fails bandwidth requirements if either (a) less than exit threshold, or (b), not + // selected, but less than entry threshold + if (caps.getLinkUpstreamBandwidthKbps() < networkPriority.getMinExitUpstreamBandwidthKbps() + || (caps.getLinkUpstreamBandwidthKbps() + < networkPriority.getMinEntryUpstreamBandwidthKbps() + && !isSelected)) { + return false; + } + + if (caps.getLinkDownstreamBandwidthKbps() + < networkPriority.getMinExitDownstreamBandwidthKbps() + || (caps.getLinkDownstreamBandwidthKbps() + < networkPriority.getMinEntryDownstreamBandwidthKbps() + && !isSelected)) { + return false; + } + + for (Map.Entry entry : + networkPriority.getCapabilitiesMatchCriteria().entrySet()) { + final int cap = entry.getKey(); + final int matchCriteria = entry.getValue(); + + if (matchCriteria == MATCH_REQUIRED && !caps.hasCapability(cap)) { + return false; + } else if (matchCriteria == MATCH_FORBIDDEN && caps.hasCapability(cap)) { + return false; + } + } + + if (vcnContext.isInTestMode() && caps.hasTransport(TRANSPORT_TEST)) { + return true; + } + + if (networkPriority instanceof VcnWifiUnderlyingNetworkTemplate) { + return checkMatchesWifiPriorityRule( + (VcnWifiUnderlyingNetworkTemplate) networkPriority, + networkRecord, + isSelected, + carrierConfig); + } + + if (networkPriority instanceof VcnCellUnderlyingNetworkTemplate) { + return checkMatchesCellPriorityRule( + vcnContext, + (VcnCellUnderlyingNetworkTemplate) networkPriority, + networkRecord, + subscriptionGroup, + snapshot); + } + + logWtf( + "Got unknown VcnUnderlyingNetworkTemplate class: " + + networkPriority.getClass().getSimpleName()); + return false; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static boolean checkMatchesWifiPriorityRule( + VcnWifiUnderlyingNetworkTemplate networkPriority, + UnderlyingNetworkRecord networkRecord, + boolean isSelected, + PersistableBundleWrapper carrierConfig) { + final NetworkCapabilities caps = networkRecord.networkCapabilities; + + if (!caps.hasTransport(TRANSPORT_WIFI)) { + return false; + } + + // TODO: Move the Network Quality check to the network metric monitor framework. + if (!isWifiRssiAcceptable(networkRecord, isSelected, carrierConfig)) { + return false; + } + + if (!networkPriority.getSsids().isEmpty() + && !networkPriority.getSsids().contains(caps.getSsid())) { + return false; + } + + return true; + } + + private static boolean isWifiRssiAcceptable( + UnderlyingNetworkRecord networkRecord, + boolean isSelected, + PersistableBundleWrapper carrierConfig) { + final NetworkCapabilities caps = networkRecord.networkCapabilities; + + if (isSelected && caps.getSignalStrength() >= getWifiExitRssiThreshold(carrierConfig)) { + return true; + } + + if (caps.getSignalStrength() >= getWifiEntryRssiThreshold(carrierConfig)) { + return true; + } + + return false; + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static boolean checkMatchesCellPriorityRule( + VcnContext vcnContext, + VcnCellUnderlyingNetworkTemplate networkPriority, + UnderlyingNetworkRecord networkRecord, + ParcelUuid subscriptionGroup, + TelephonySubscriptionSnapshot snapshot) { + final NetworkCapabilities caps = networkRecord.networkCapabilities; + + if (!caps.hasTransport(TRANSPORT_CELLULAR)) { + return false; + } + + final TelephonyNetworkSpecifier telephonyNetworkSpecifier = + ((TelephonyNetworkSpecifier) caps.getNetworkSpecifier()); + if (telephonyNetworkSpecifier == null) { + logWtf("Got null NetworkSpecifier"); + return false; + } + + final int subId = telephonyNetworkSpecifier.getSubscriptionId(); + final TelephonyManager subIdSpecificTelephonyMgr = + vcnContext + .getContext() + .getSystemService(TelephonyManager.class) + .createForSubscriptionId(subId); + + if (!networkPriority.getOperatorPlmnIds().isEmpty()) { + final String plmnId = subIdSpecificTelephonyMgr.getNetworkOperator(); + if (!networkPriority.getOperatorPlmnIds().contains(plmnId)) { + return false; + } + } + + if (!networkPriority.getSimSpecificCarrierIds().isEmpty()) { + final int carrierId = subIdSpecificTelephonyMgr.getSimSpecificCarrierId(); + if (!networkPriority.getSimSpecificCarrierIds().contains(carrierId)) { + return false; + } + } + + final int roamingMatch = networkPriority.getRoaming(); + final boolean isRoaming = !caps.hasCapability(NET_CAPABILITY_NOT_ROAMING); + if (roamingMatch == MATCH_REQUIRED && !isRoaming + || roamingMatch == MATCH_FORBIDDEN && isRoaming) { + return false; + } + + final int opportunisticMatch = networkPriority.getOpportunistic(); + final boolean isOpportunistic = isOpportunistic(snapshot, caps.getSubscriptionIds()); + if (opportunisticMatch == MATCH_REQUIRED) { + if (!isOpportunistic) { + return false; + } + + // If this carrier is the active data provider, ensure that opportunistic is only + // ever prioritized if it is also the active data subscription. This ensures that + // if an opportunistic subscription is still in the process of being switched to, + // or switched away from, the VCN does not attempt to continue using it against the + // decision made at the telephony layer. Failure to do so may result in the modem + // switching back and forth. + // + // Allow the following two cases: + // 1. Active subId is NOT in the group that this VCN is supporting + // 2. This opportunistic subscription is for the active subId + if (snapshot.getAllSubIdsInGroup(subscriptionGroup) + .contains(SubscriptionManager.getActiveDataSubscriptionId()) + && !caps.getSubscriptionIds() + .contains(SubscriptionManager.getActiveDataSubscriptionId())) { + return false; + } + } else if (opportunisticMatch == MATCH_FORBIDDEN && !isOpportunistic) { + return false; + } + + return true; + } + + static boolean isOpportunistic( + @NonNull TelephonySubscriptionSnapshot snapshot, Set subIds) { + if (snapshot == null) { + logWtf("Got null snapshot"); + return false; + } + for (int subId : subIds) { + if (snapshot.isOpportunistic(subId)) { + return true; + } + } + return false; + } + + static int getWifiEntryRssiThreshold(@Nullable PersistableBundleWrapper carrierConfig) { + if (carrierConfig != null) { + return carrierConfig.getInt( + VcnManager.VCN_NETWORK_SELECTION_WIFI_ENTRY_RSSI_THRESHOLD_KEY, + WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT); + } + return WIFI_ENTRY_RSSI_THRESHOLD_DEFAULT; + } + + static int getWifiExitRssiThreshold(@Nullable PersistableBundleWrapper carrierConfig) { + if (carrierConfig != null) { + return carrierConfig.getInt( + VcnManager.VCN_NETWORK_SELECTION_WIFI_EXIT_RSSI_THRESHOLD_KEY, + WIFI_EXIT_RSSI_THRESHOLD_DEFAULT); + } + return WIFI_EXIT_RSSI_THRESHOLD_DEFAULT; + } + + private static void logWtf(String msg) { + Slog.wtf(TAG, msg); + LOCAL_LOG.log(TAG + " WTF: " + msg); + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java new file mode 100644 index 0000000000..1012ba4823 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkController.java @@ -0,0 +1,782 @@ +/* + * 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.routeselection; + +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_ANY; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_FORBIDDEN; +import static android.net.vcn.VcnUnderlyingNetworkTemplate.MATCH_REQUIRED; +import static android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; +import static android.telephony.TelephonyCallback.ActiveDataSubscriptionIdListener; + +import static com.android.server.VcnManagementService.LOCAL_LOG; +import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.getWifiEntryRssiThreshold; +import static com.android.server.vcn.routeselection.NetworkPriorityClassifier.getWifiExitRssiThreshold; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.IpSecTransform; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.TelephonyNetworkSpecifier; +import android.net.vcn.VcnCellUnderlyingNetworkTemplate; +import android.net.vcn.VcnGatewayConnectionConfig; +import android.net.vcn.VcnUnderlyingNetworkTemplate; +import android.net.vcn.util.LogUtils; +import android.os.Build; +import android.os.Handler; +import android.os.ParcelUuid; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.IndentingPrintWriter; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.modules.utils.HandlerExecutor; +import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import com.android.server.vcn.VcnContext; +import com.android.server.vcn.metrics.VcnMetrics; +import com.android.server.vcn.routeselection.UnderlyingNetworkEvaluator.NetworkEvaluatorCallback; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +/** + * Tracks a set of Networks underpinning a VcnGatewayConnection. + * + *

A single UnderlyingNetworkController is built to serve a SINGLE VCN Gateway Connection, and + * MUST be torn down with the VcnGatewayConnection in order to ensure underlying networks are + * allowed to be reaped. + * + * @hide + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class UnderlyingNetworkController { + @NonNull private static final String TAG = UnderlyingNetworkController.class.getSimpleName(); + + @NonNull private final VcnContext mVcnContext; + @NonNull private final VcnGatewayConnectionConfig mConnectionConfig; + @NonNull private final ParcelUuid mSubscriptionGroup; + @NonNull private final UnderlyingNetworkControllerCallback mCb; + @NonNull private final Dependencies mDeps; + @NonNull private final Handler mHandler; + @NonNull private final ConnectivityManager mConnectivityManager; + @NonNull private final TelephonyCallback mActiveDataSubIdListener = + new VcnActiveDataSubscriptionIdListener(); + + private final Map mUnderlyingNetworkRecords = + new ArrayMap<>(); + + @NonNull private final List mCellBringupCallbacks = new ArrayList<>(); + @Nullable private NetworkCallback mWifiBringupCallback; + @Nullable private NetworkCallback mWifiEntryRssiThresholdCallback; + @Nullable private NetworkCallback mWifiExitRssiThresholdCallback; + @Nullable private UnderlyingNetworkListener mRouteSelectionCallback; + + @NonNull private TelephonySubscriptionSnapshot mLastSnapshot; + @Nullable private PersistableBundleWrapper mCarrierConfig; + private boolean mIsQuitting = false; + + @Nullable private UnderlyingNetworkRecord mCurrentRecord; + @Nullable private UnderlyingNetworkRecord.Builder mRecordInProgress; + + private final int mGatewayConnectionId; + private final VcnMetrics mVcnMetrics; + + public UnderlyingNetworkController( + @NonNull VcnContext vcnContext, + @NonNull VcnGatewayConnectionConfig connectionConfig, + int gatewayConnectionId, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot snapshot, + @NonNull UnderlyingNetworkControllerCallback cb) { + this( + vcnContext, + connectionConfig, + gatewayConnectionId, + subscriptionGroup, + snapshot, + cb, + new Dependencies()); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + UnderlyingNetworkController( + @NonNull VcnContext vcnContext, + @NonNull VcnGatewayConnectionConfig connectionConfig, + int gatewayConnectionId, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot snapshot, + @NonNull UnderlyingNetworkControllerCallback cb, + @NonNull Dependencies deps) { + mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext"); + mConnectionConfig = Objects.requireNonNull(connectionConfig, "Missing connectionConfig"); + mSubscriptionGroup = Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); + mLastSnapshot = Objects.requireNonNull(snapshot, "Missing snapshot"); + mCb = Objects.requireNonNull(cb, "Missing cb"); + mDeps = Objects.requireNonNull(deps, "Missing deps"); + mGatewayConnectionId = gatewayConnectionId; + mVcnMetrics = mDeps.newVcnMetrics(); + + mHandler = new Handler(mVcnContext.getLooper()); + + mConnectivityManager = mVcnContext.getContext().getSystemService(ConnectivityManager.class); + mVcnContext + .getContext() + .getSystemService(TelephonyManager.class) + .registerTelephonyCallback(new HandlerExecutor(mHandler), mActiveDataSubIdListener); + + mCarrierConfig = mLastSnapshot.getCarrierConfigForSubGrp(mSubscriptionGroup); + + registerOrUpdateNetworkRequests(); + } + + private static class CapabilityMatchCriteria { + public final int capability; + public final int matchCriteria; + + CapabilityMatchCriteria(int capability, int matchCriteria) { + this.capability = capability; + this.matchCriteria = matchCriteria; + } + + @Override + public int hashCode() { + return Objects.hash(capability, matchCriteria); + } + + @Override + public boolean equals(@Nullable Object other) { + if (!(other instanceof CapabilityMatchCriteria)) { + return false; + } + + final CapabilityMatchCriteria rhs = (CapabilityMatchCriteria) other; + return capability == rhs.capability && matchCriteria == rhs.matchCriteria; + } + } + + private static Set> dedupAndGetCapRequirementsForCell( + VcnGatewayConnectionConfig connectionConfig) { + final Set> dedupedCapsMatchSets = new ArraySet<>(); + + for (VcnUnderlyingNetworkTemplate template : + connectionConfig.getVcnUnderlyingNetworkPriorities()) { + if (template instanceof VcnCellUnderlyingNetworkTemplate) { + final Set capsMatchSet = new ArraySet<>(); + + for (Map.Entry entry : + ((VcnCellUnderlyingNetworkTemplate) template) + .getCapabilitiesMatchCriteria() + .entrySet()) { + + final int capability = entry.getKey(); + final int matchCriteria = entry.getValue(); + if (matchCriteria != MATCH_ANY) { + capsMatchSet.add(new CapabilityMatchCriteria(capability, matchCriteria)); + } + } + + dedupedCapsMatchSets.add(capsMatchSet); + } + } + + dedupedCapsMatchSets.add( + Collections.singleton( + new CapabilityMatchCriteria( + NetworkCapabilities.NET_CAPABILITY_INTERNET, MATCH_REQUIRED))); + return dedupedCapsMatchSets; + } + + private void registerOrUpdateNetworkRequests() { + NetworkCallback oldRouteSelectionCallback = mRouteSelectionCallback; + NetworkCallback oldWifiCallback = mWifiBringupCallback; + NetworkCallback oldWifiEntryRssiThresholdCallback = mWifiEntryRssiThresholdCallback; + NetworkCallback oldWifiExitRssiThresholdCallback = mWifiExitRssiThresholdCallback; + List oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks); + mCellBringupCallbacks.clear(); + + for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { + evaluator.close(); + } + + mUnderlyingNetworkRecords.clear(); + + // Register new callbacks. Make-before-break; always register new callbacks before removal + // of old callbacks + if (!mIsQuitting) { + mRouteSelectionCallback = new UnderlyingNetworkListener(); + mConnectivityManager.registerNetworkCallback( + getRouteSelectionRequest(), mRouteSelectionCallback, mHandler); + + mWifiEntryRssiThresholdCallback = new NetworkBringupCallback(); + mConnectivityManager.registerNetworkCallback( + getWifiEntryRssiThresholdNetworkRequest(), + mWifiEntryRssiThresholdCallback, + mHandler); + + mWifiExitRssiThresholdCallback = new NetworkBringupCallback(); + mConnectivityManager.registerNetworkCallback( + getWifiExitRssiThresholdNetworkRequest(), + mWifiExitRssiThresholdCallback, + mHandler); + + mWifiBringupCallback = new NetworkBringupCallback(); + mConnectivityManager.requestBackgroundNetwork( + getWifiNetworkRequest(), mWifiBringupCallback, mHandler); + + for (final int subId : mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) { + for (Set capsMatchCriteria : + dedupAndGetCapRequirementsForCell(mConnectionConfig)) { + final NetworkBringupCallback cb = new NetworkBringupCallback(); + mCellBringupCallbacks.add(cb); + + mConnectivityManager.requestBackgroundNetwork( + getCellNetworkRequestForSubId(subId, capsMatchCriteria), cb, mHandler); + } + } + } else { + mRouteSelectionCallback = null; + mWifiBringupCallback = null; + mWifiEntryRssiThresholdCallback = null; + mWifiExitRssiThresholdCallback = null; + // mCellBringupCallbacks already cleared above. + } + + // Unregister old callbacks (as necessary) + if (oldRouteSelectionCallback != null) { + mConnectivityManager.unregisterNetworkCallback(oldRouteSelectionCallback); + } + if (oldWifiCallback != null) { + mConnectivityManager.unregisterNetworkCallback(oldWifiCallback); + } + if (oldWifiEntryRssiThresholdCallback != null) { + mConnectivityManager.unregisterNetworkCallback(oldWifiEntryRssiThresholdCallback); + } + if (oldWifiExitRssiThresholdCallback != null) { + mConnectivityManager.unregisterNetworkCallback(oldWifiExitRssiThresholdCallback); + } + for (NetworkCallback cellBringupCallback : oldCellCallbacks) { + mConnectivityManager.unregisterNetworkCallback(cellBringupCallback); + } + } + + /** + * Builds the Route selection request + * + *

This request is guaranteed to select carrier-owned, non-VCN underlying networks by virtue + * of a populated set of subIds as expressed in NetworkCapabilities#getSubscriptionIds(). Only + * carrier owned networks may be selected, as the request specifies only subIds in the VCN's + * subscription group, while the VCN networks are excluded by virtue of not having subIds set on + * the VCN-exposed networks. + * + *

If the VCN that this UnderlyingNetworkController belongs to is in test-mode, this will + * return a NetworkRequest that only matches Test Networks. + */ + private NetworkRequest getRouteSelectionRequest() { + if (mVcnContext.isInTestMode()) { + return getTestNetworkRequest(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)); + } + + return getBaseNetworkRequestBuilder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED) + .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)) + .build(); + } + + private NetworkRequest.Builder getBaseWifiNetworkRequestBuilder() { + return getBaseNetworkRequestBuilder() + .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .setSubscriptionIds(mLastSnapshot.getAllSubIdsInGroup(mSubscriptionGroup)); + } + + /** + * Builds the WiFi bringup request + * + *

This request is built specifically to match only carrier-owned WiFi networks, but is also + * built to ONLY keep Carrier WiFi Networks alive (but never bring them up). This is a result of + * the WifiNetworkFactory not advertising a list of subIds, and therefore not accepting this + * request. As such, it will bind to a Carrier WiFi Network that has already been brought up, + * but will NEVER bring up a Carrier WiFi network itself. + */ + private NetworkRequest getWifiNetworkRequest() { + return getBaseWifiNetworkRequestBuilder().build(); + } + + /** + * Builds the WiFi entry threshold signal strength request + * + *

This request ensures that WiFi reports the crossing of the wifi entry RSSI threshold. + * Without this request, WiFi rate-limits, and reports signal strength changes at too slow a + * pace to effectively select a short-lived WiFi offload network. + */ + private NetworkRequest getWifiEntryRssiThresholdNetworkRequest() { + return getBaseWifiNetworkRequestBuilder() + // Ensure wifi updates signal strengths when crossing this threshold. + .setSignalStrength(getWifiEntryRssiThreshold(mCarrierConfig)) + .build(); + } + + /** + * Builds the WiFi exit threshold signal strength request + * + *

This request ensures that WiFi reports the crossing of the wifi exit RSSI threshold. + * Without this request, WiFi rate-limits, and reports signal strength changes at too slow a + * pace to effectively select away from a failing WiFi network. + */ + private NetworkRequest getWifiExitRssiThresholdNetworkRequest() { + return getBaseWifiNetworkRequestBuilder() + // Ensure wifi updates signal strengths when crossing this threshold. + .setSignalStrength(getWifiExitRssiThreshold(mCarrierConfig)) + .build(); + } + + /** + * Builds a Cellular bringup request for a given subId + * + *

This request is filed in order to ensure that the Telephony stack always has a + * NetworkRequest to bring up a VCN underlying cellular network. It is required in order to + * ensure that even when a VCN (appears as Cellular) satisfies the default request, Telephony + * will bring up additional underlying Cellular networks. + * + *

Since this request MUST make it to the TelephonyNetworkFactory, subIds are not specified + * in the NetworkCapabilities, but rather in the TelephonyNetworkSpecifier. + */ + private NetworkRequest getCellNetworkRequestForSubId( + int subId, Set capsMatchCriteria) { + final NetworkRequest.Builder nrBuilder = + getBaseNetworkRequestBuilder() + .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier( + new TelephonyNetworkSpecifier.Builder() + .setSubscriptionId(subId) + .build()); + + for (CapabilityMatchCriteria capMatchCriteria : capsMatchCriteria) { + final int cap = capMatchCriteria.capability; + final int matchCriteria = capMatchCriteria.matchCriteria; + + if (matchCriteria == MATCH_REQUIRED) { + nrBuilder.addCapability(cap); + } else if (matchCriteria == MATCH_FORBIDDEN) { + nrBuilder.addForbiddenCapability(cap); + } + } + + return nrBuilder.build(); + } + + /** + * Builds and returns a NetworkRequest builder common to all Underlying Network requests + */ + private NetworkRequest.Builder getBaseNetworkRequestBuilder() { + return new NetworkRequest.Builder() + .removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + .removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED); + } + + /** Builds and returns a NetworkRequest for the given subIds to match Test Networks. */ + private NetworkRequest getTestNetworkRequest(@NonNull Set subIds) { + return new NetworkRequest.Builder() + .clearCapabilities() + .addTransportType(NetworkCapabilities.TRANSPORT_TEST) + .setSubscriptionIds(subIds) + .build(); + } + + /** + * Update this UnderlyingNetworkController's TelephonySubscriptionSnapshot. + * + *

Updating the TelephonySubscriptionSnapshot will cause this UnderlyingNetworkController to + * reevaluate its NetworkBringupCallbacks. This may result in NetworkRequests being registered + * or unregistered if the subIds mapped to the this Tracker's SubscriptionGroup change. + */ + public void updateSubscriptionSnapshot(@NonNull TelephonySubscriptionSnapshot newSnapshot) { + Objects.requireNonNull(newSnapshot, "Missing newSnapshot"); + + final TelephonySubscriptionSnapshot oldSnapshot = mLastSnapshot; + mLastSnapshot = newSnapshot; + + // Update carrier config + mCarrierConfig = mLastSnapshot.getCarrierConfigForSubGrp(mSubscriptionGroup); + + // Make sure all evaluators use the same updated TelephonySubscriptionSnapshot and carrier + // config to calculate their cached priority classes. For simplicity, the + // UnderlyingNetworkController does not listen for changes in VCN-related carrier config + // keys, and changes are applied at restart of the VcnGatewayConnection + for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { + evaluator.reevaluate( + mConnectionConfig.getVcnUnderlyingNetworkPriorities(), + mSubscriptionGroup, + mLastSnapshot, + mCarrierConfig); + } + + // Only trigger re-registration if subIds in this group have changed + if (oldSnapshot + .getAllSubIdsInGroup(mSubscriptionGroup) + .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) { + reevaluateNetworks(); + return; + } + registerOrUpdateNetworkRequests(); + } + + /** + * Pass the IpSecTransform of the VCN to UnderlyingNetworkController for metric monitoring + * + *

Caller MUST call it when IpSecTransforms have been created for VCN creation or migration + */ + public void updateInboundTransform( + @NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) { + Objects.requireNonNull(currentNetwork, "currentNetwork is null"); + Objects.requireNonNull(transform, "transform is null"); + + if (mCurrentRecord == null + || mRouteSelectionCallback == null + || !Objects.equals(currentNetwork.network, mCurrentRecord.network)) { + // The caller (VcnGatewayConnection) is out-of-dated. Ignore this call. + return; + } + + mUnderlyingNetworkRecords.get(mCurrentRecord.network).setInboundTransform(transform); + } + + /** Tears down this Tracker, and releases all underlying network requests. */ + public void teardown() { + mVcnContext.ensureRunningOnLooperThread(); + mIsQuitting = true; + + // Will unregister all existing callbacks, but not register new ones due to quitting flag. + registerOrUpdateNetworkRequests(); + + mVcnContext + .getContext() + .getSystemService(TelephonyManager.class) + .unregisterTelephonyCallback(mActiveDataSubIdListener); + + // Log empty validated network count left, as a clean up state. + mVcnMetrics.logValidatedUnderlyingNetworkCount(mGatewayConnectionId, 0); + } + + private TreeSet getSortedUnderlyingNetworks() { + TreeSet sorted = + new TreeSet<>(UnderlyingNetworkEvaluator.getComparator(mVcnContext)); + + for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { + if (evaluator.getPriorityClass() != NetworkPriorityClassifier.PRIORITY_INVALID) { + sorted.add(evaluator); + } + } + + return sorted; + } + + private void reevaluateNetworks() { + if (mIsQuitting || mRouteSelectionCallback == null) { + return; // UnderlyingNetworkController has quit. + } + + TreeSet sorted = getSortedUnderlyingNetworks(); + mVcnMetrics.logValidatedUnderlyingNetworkCount(mGatewayConnectionId, sorted.size()); + + UnderlyingNetworkEvaluator candidateEvaluator = sorted.isEmpty() ? null : sorted.first(); + UnderlyingNetworkRecord candidate = + candidateEvaluator == null ? null : candidateEvaluator.getNetworkRecord(); + if (Objects.equals(mCurrentRecord, candidate)) { + return; + } + + String allNetworkPriorities = ""; + for (UnderlyingNetworkEvaluator recordEvaluator : sorted) { + if (!allNetworkPriorities.isEmpty()) { + allNetworkPriorities += ", "; + } + allNetworkPriorities += + recordEvaluator.getNetwork() + ": " + recordEvaluator.getPriorityClass(); + } + + if (!UnderlyingNetworkRecord.isSameNetwork(mCurrentRecord, candidate)) { + logInfo( + "Selected network changed to " + + (candidate == null ? null : candidate.network) + + ", selected from list: " + + allNetworkPriorities); + } + + mCurrentRecord = candidate; + mCb.onSelectedUnderlyingNetworkChanged(mCurrentRecord); + + // Need to update all evaluators to ensure the previously selected one is unselected + for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { + evaluator.setIsSelected( + candidateEvaluator == evaluator, + mConnectionConfig.getVcnUnderlyingNetworkPriorities(), + mSubscriptionGroup, + mLastSnapshot, + mCarrierConfig); + } + } + + /** + * NetworkBringupCallback is used to keep background, VCN-managed Networks from being reaped. + * + *

NetworkBringupCallback only exists to prevent matching (VCN-managed) Networks from being + * reaped, and no action is taken on any events firing. + */ + @VisibleForTesting + class NetworkBringupCallback extends NetworkCallback {} + + /** + * RouteSelectionCallback is used to select the "best" underlying Network. + * + *

The "best" network is determined by ConnectivityService, which is treated as a source of + * truth. + */ + @VisibleForTesting + class UnderlyingNetworkListener extends NetworkCallback { + UnderlyingNetworkListener() { + super(NetworkCallback.FLAG_INCLUDE_LOCATION_INFO); + } + + @Override + public void onAvailable(@NonNull Network network) { + mUnderlyingNetworkRecords.put( + network, + mDeps.newUnderlyingNetworkEvaluator( + mVcnContext, + network, + mConnectionConfig.getVcnUnderlyingNetworkPriorities(), + mSubscriptionGroup, + mLastSnapshot, + mCarrierConfig, + new NetworkEvaluatorCallbackImpl())); + } + + @Override + public void onLost(@NonNull Network network) { + mUnderlyingNetworkRecords.get(network).close(); + mUnderlyingNetworkRecords.remove(network); + + reevaluateNetworks(); + } + + @Override + public void onCapabilitiesChanged( + @NonNull Network network, @NonNull NetworkCapabilities networkCapabilities) { + final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network); + if (evaluator == null) { + logWtf("Got capabilities change for unknown key: " + network); + return; + } + + evaluator.setNetworkCapabilities( + networkCapabilities, + mConnectionConfig.getVcnUnderlyingNetworkPriorities(), + mSubscriptionGroup, + mLastSnapshot, + mCarrierConfig); + + if (evaluator.isValid()) { + reevaluateNetworks(); + } + } + + @Override + public void onLinkPropertiesChanged( + @NonNull Network network, @NonNull LinkProperties linkProperties) { + final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network); + if (evaluator == null) { + logWtf("Got link properties change for unknown key: " + network); + return; + } + + evaluator.setLinkProperties( + linkProperties, + mConnectionConfig.getVcnUnderlyingNetworkPriorities(), + mSubscriptionGroup, + mLastSnapshot, + mCarrierConfig); + + if (evaluator.isValid()) { + reevaluateNetworks(); + } + } + + @Override + public void onBlockedStatusChanged(@NonNull Network network, boolean isBlocked) { + final UnderlyingNetworkEvaluator evaluator = mUnderlyingNetworkRecords.get(network); + if (evaluator == null) { + logWtf("Got blocked status change for unknown key: " + network); + return; + } + + evaluator.setIsBlocked( + isBlocked, + mConnectionConfig.getVcnUnderlyingNetworkPriorities(), + mSubscriptionGroup, + mLastSnapshot, + mCarrierConfig); + + if (evaluator.isValid()) { + reevaluateNetworks(); + } + } + } + + @VisibleForTesting + class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback { + @Override + public void onEvaluationResultChanged() { + mVcnContext.ensureRunningOnLooperThread(); + reevaluateNetworks(); + } + } + + private String getLogPrefix() { + return "(" + + LogUtils.getHashedSubscriptionGroup(mSubscriptionGroup) + + "-" + + mConnectionConfig.getGatewayConnectionName() + + "-" + + System.identityHashCode(this) + + ") "; + } + + private String getTagLogPrefix() { + return "[ " + TAG + " " + getLogPrefix() + "]"; + } + + private void logInfo(String msg) { + Slog.i(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[INFO] " + getTagLogPrefix() + msg); + } + + private void logInfo(String msg, Throwable tr) { + Slog.i(TAG, getLogPrefix() + msg, tr); + LOCAL_LOG.log("[INFO] " + getTagLogPrefix() + msg + tr); + } + + private void logWtf(String msg) { + Slog.wtf(TAG, msg); + LOCAL_LOG.log(TAG + "[WTF ] " + getTagLogPrefix() + msg); + } + + private void logWtf(String msg, Throwable tr) { + Slog.wtf(TAG, msg, tr); + LOCAL_LOG.log(TAG + "[WTF ] " + getTagLogPrefix() + msg + tr); + } + + /** Dumps the state of this record for logging and debugging purposes. */ + public void dump(IndentingPrintWriter pw) { + pw.println("UnderlyingNetworkController:"); + pw.increaseIndent(); + + pw.println("Carrier WiFi Entry Threshold: " + getWifiEntryRssiThreshold(mCarrierConfig)); + pw.println("Carrier WiFi Exit Threshold: " + getWifiExitRssiThreshold(mCarrierConfig)); + pw.println( + "Currently selected: " + (mCurrentRecord == null ? null : mCurrentRecord.network)); + + pw.println("VcnUnderlyingNetworkTemplate list:"); + pw.increaseIndent(); + int index = 0; + for (VcnUnderlyingNetworkTemplate priority : + mConnectionConfig.getVcnUnderlyingNetworkPriorities()) { + pw.println("Priority index: " + index); + priority.dump(pw); + index++; + } + pw.decreaseIndent(); + pw.println(); + + pw.println("Underlying networks:"); + pw.increaseIndent(); + if (mRouteSelectionCallback != null) { + for (UnderlyingNetworkEvaluator recordEvaluator : getSortedUnderlyingNetworks()) { + recordEvaluator.dump(pw); + } + } + pw.decreaseIndent(); + pw.println(); + + pw.decreaseIndent(); + } + + private class VcnActiveDataSubscriptionIdListener extends TelephonyCallback + implements ActiveDataSubscriptionIdListener { + @Override + public void onActiveDataSubscriptionIdChanged(int subId) { + reevaluateNetworks(); + } + } + + /** Callbacks for being notified of the changes in, or to the selected underlying network. */ + public interface UnderlyingNetworkControllerCallback { + /** + * Fired when a new underlying network is selected, or properties have changed. + * + *

This callback does NOT signal a mobility event. + * + * @param underlyingNetworkRecord The details of the new underlying network + */ + void onSelectedUnderlyingNetworkChanged( + @Nullable UnderlyingNetworkRecord underlyingNetworkRecord); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + /* External dependencies, for injection in tests */ + public static class Dependencies { + /** Builds a new UnderlyingNetworkEvaluator. */ + public UnderlyingNetworkEvaluator newUnderlyingNetworkEvaluator( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkEvaluatorCallback evaluatorCallback) { + return new UnderlyingNetworkEvaluator( + vcnContext, + network, + underlyingNetworkTemplates, + subscriptionGroup, + lastSnapshot, + carrierConfig, + evaluatorCallback); + } + + /** Builds a new VcnMetrics. */ + public VcnMetrics newVcnMetrics() { + return new VcnMetrics(); + } + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java new file mode 100644 index 0000000000..164b59f6f0 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2023 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.routeselection; + +import static android.net.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; + +import static com.android.server.VcnManagementService.LOCAL_LOG; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.TargetApi; +import android.net.IpSecTransform; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.vcn.VcnManager; +import android.net.vcn.VcnUnderlyingNetworkTemplate; +import android.os.Build; +import android.os.Handler; +import android.os.ParcelUuid; +import android.util.IndentingPrintWriter; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; +import com.android.server.vcn.TelephonySubscriptionTracker.TelephonySubscriptionSnapshot; +import com.android.server.vcn.VcnContext; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * UnderlyingNetworkEvaluator evaluates the quality and priority class of a network candidate for + * route selection. + * + * @hide + */ +@TargetApi(Build.VERSION_CODES.BAKLAVA) +public class UnderlyingNetworkEvaluator { + private static final String TAG = UnderlyingNetworkEvaluator.class.getSimpleName(); + + private static final int[] PENALTY_TIMEOUT_MINUTES_DEFAULT = new int[] {5}; + + @NonNull private final VcnContext mVcnContext; + @NonNull private final Handler mHandler; + @NonNull private final Object mCancellationToken = new Object(); + + @NonNull private final UnderlyingNetworkRecord.Builder mNetworkRecordBuilder; + + @NonNull private final NetworkEvaluatorCallback mEvaluatorCallback; + @NonNull private final List mMetricMonitors = new ArrayList<>(); + + @NonNull private final Dependencies mDependencies; + + // TODO: Support back-off timeouts + private long mPenalizedTimeoutMs; + + private boolean mIsSelected; + private boolean mIsPenalized; + private int mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID; + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public UnderlyingNetworkEvaluator( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkEvaluatorCallback evaluatorCallback, + @NonNull Dependencies dependencies) { + mVcnContext = Objects.requireNonNull(vcnContext, "Missing vcnContext"); + mHandler = new Handler(mVcnContext.getLooper()); + + mDependencies = Objects.requireNonNull(dependencies, "Missing dependencies"); + mEvaluatorCallback = Objects.requireNonNull(evaluatorCallback, "Missing deps"); + + Objects.requireNonNull(underlyingNetworkTemplates, "Missing underlyingNetworkTemplates"); + Objects.requireNonNull(subscriptionGroup, "Missing subscriptionGroup"); + Objects.requireNonNull(lastSnapshot, "Missing lastSnapshot"); + + mNetworkRecordBuilder = + new UnderlyingNetworkRecord.Builder( + Objects.requireNonNull(network, "Missing network")); + mIsSelected = false; + mIsPenalized = false; + mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig); + + updatePriorityClass( + underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + try { + mMetricMonitors.add( + mDependencies.newIpSecPacketLossDetector( + mVcnContext, + mNetworkRecordBuilder.getNetwork(), + carrierConfig, + new MetricMonitorCallbackImpl())); + } catch (IllegalAccessException e) { + // No action. Do not add anything to mMetricMonitors + } + } + + public UnderlyingNetworkEvaluator( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkEvaluatorCallback evaluatorCallback) { + this( + vcnContext, + network, + underlyingNetworkTemplates, + subscriptionGroup, + lastSnapshot, + carrierConfig, + evaluatorCallback, + new Dependencies()); + } + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public static class Dependencies { + /** Get an IpSecPacketLossDetector instance */ + public IpSecPacketLossDetector newIpSecPacketLossDetector( + @NonNull VcnContext vcnContext, + @NonNull Network network, + @Nullable PersistableBundleWrapper carrierConfig, + @NonNull NetworkMetricMonitor.NetworkMetricMonitorCallback callback) + throws IllegalAccessException { + return new IpSecPacketLossDetector(vcnContext, network, carrierConfig, callback); + } + } + + /** Callback to notify caller to reevaluate network selection */ + public interface NetworkEvaluatorCallback { + /** + * Called when mIsPenalized changed + * + *

When receiving this call, UnderlyingNetworkController should reevaluate all network + * candidates for VCN underlying network selection + */ + void onEvaluationResultChanged(); + } + + private class MetricMonitorCallbackImpl + implements NetworkMetricMonitor.NetworkMetricMonitorCallback { + public void onValidationResultReceived() { + mVcnContext.ensureRunningOnLooperThread(); + + handleValidationResult(); + } + } + + private void updatePriorityClass( + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig) { + if (mNetworkRecordBuilder.isValid()) { + mPriorityClass = + NetworkPriorityClassifier.calculatePriorityClass( + mVcnContext, + mNetworkRecordBuilder.build(), + underlyingNetworkTemplates, + subscriptionGroup, + lastSnapshot, + mIsSelected, + carrierConfig); + } else { + mPriorityClass = NetworkPriorityClassifier.PRIORITY_INVALID; + } + } + + /** Get the comparator for UnderlyingNetworkEvaluator */ + public static Comparator getComparator(VcnContext vcnContext) { + return (left, right) -> { + if (left.mIsPenalized != right.mIsPenalized) { + // A penalized network should have lower priority which means a larger index + return left.mIsPenalized ? 1 : -1; + } + + final int leftIndex = left.mPriorityClass; + final int rightIndex = right.mPriorityClass; + + // In the case of networks in the same priority class, prioritize based on other + // criteria (eg. actively selected network, link metrics, etc) + if (leftIndex == rightIndex) { + // TODO: Improve the strategy of network selection when both UnderlyingNetworkRecord + // fall into the same priority class. + if (left.mIsSelected) { + return -1; + } + if (right.mIsSelected) { + return 1; + } + } + return Integer.compare(leftIndex, rightIndex); + }; + } + + private static long getPenaltyTimeoutMs(@Nullable PersistableBundleWrapper carrierConfig) { + final int[] timeoutMinuteList; + + if (carrierConfig != null) { + timeoutMinuteList = + carrierConfig.getIntArray( + VcnManager.VCN_NETWORK_SELECTION_PENALTY_TIMEOUT_MINUTES_LIST_KEY, + PENALTY_TIMEOUT_MINUTES_DEFAULT); + } else { + timeoutMinuteList = PENALTY_TIMEOUT_MINUTES_DEFAULT; + } + + // TODO: Add the support of back-off timeouts and return the full list + return TimeUnit.MINUTES.toMillis(timeoutMinuteList[0]); + } + + private void handleValidationResult() { + final boolean wasPenalized = mIsPenalized; + mIsPenalized = false; + for (NetworkMetricMonitor monitor : mMetricMonitors) { + mIsPenalized |= monitor.isValidationFailed(); + } + + if (wasPenalized == mIsPenalized) { + return; + } + + logInfo( + "#handleValidationResult: wasPenalized " + + wasPenalized + + " mIsPenalized " + + mIsPenalized); + + if (mIsPenalized) { + mHandler.postDelayed( + new ExitPenaltyBoxRunnable(), mCancellationToken, mPenalizedTimeoutMs); + } else { + // Exit the penalty box + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + } + mEvaluatorCallback.onEvaluationResultChanged(); + } + + public class ExitPenaltyBoxRunnable implements Runnable { + @Override + public void run() { + if (!mIsPenalized) { + logWtf("Evaluator not being penalized but ExitPenaltyBoxRunnable was scheduled"); + return; + } + + // TODO: There might be a future metric monitor (e.g. ping) that will require the + // validation to pass before exiting the penalty box. + mIsPenalized = false; + mEvaluatorCallback.onEvaluationResultChanged(); + } + } + + /** Set the NetworkCapabilities */ + public void setNetworkCapabilities( + @NonNull NetworkCapabilities nc, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig) { + mNetworkRecordBuilder.setNetworkCapabilities(nc); + + updatePriorityClass( + underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.onLinkPropertiesOrCapabilitiesChanged(); + } + } + + /** Set the LinkProperties */ + public void setLinkProperties( + @NonNull LinkProperties lp, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig) { + mNetworkRecordBuilder.setLinkProperties(lp); + + updatePriorityClass( + underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.onLinkPropertiesOrCapabilitiesChanged(); + } + } + + /** Set whether the network is blocked */ + public void setIsBlocked( + boolean isBlocked, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig) { + mNetworkRecordBuilder.setIsBlocked(isBlocked); + + updatePriorityClass( + underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + } + + /** Set whether the network is selected as VCN's underlying network */ + public void setIsSelected( + boolean isSelected, + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig) { + mIsSelected = isSelected; + + updatePriorityClass( + underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.setIsSelectedUnderlyingNetwork(isSelected); + } + } + + /** + * Update the last TelephonySubscriptionSnapshot and carrier config to reevaluate the network + */ + public void reevaluate( + @NonNull List underlyingNetworkTemplates, + @NonNull ParcelUuid subscriptionGroup, + @NonNull TelephonySubscriptionSnapshot lastSnapshot, + @Nullable PersistableBundleWrapper carrierConfig) { + updatePriorityClass( + underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); + + // The already scheduled event will not be affected. The followup events will be scheduled + // with the new timeout + mPenalizedTimeoutMs = getPenaltyTimeoutMs(carrierConfig); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.setCarrierConfig(carrierConfig); + } + } + + /** Update the inbound IpSecTransform applied to the network */ + public void setInboundTransform(@NonNull IpSecTransform transform) { + if (!mIsSelected) { + logWtf("setInboundTransform on an unselected evaluator"); + return; + } + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.setInboundTransform(transform); + } + } + + /** Close the evaluator and stop all the underlying network metric monitors */ + public void close() { + mHandler.removeCallbacksAndEqualMessages(mCancellationToken); + + for (NetworkMetricMonitor monitor : mMetricMonitors) { + monitor.close(); + } + } + + /** Return whether this network evaluator is valid */ + public boolean isValid() { + return mNetworkRecordBuilder.isValid(); + } + + /** Return the network */ + public Network getNetwork() { + return mNetworkRecordBuilder.getNetwork(); + } + + /** Return the network record */ + public UnderlyingNetworkRecord getNetworkRecord() { + return mNetworkRecordBuilder.build(); + } + + /** Return the priority class for network selection */ + public int getPriorityClass() { + return mPriorityClass; + } + + /** Return whether the network is being penalized */ + public boolean isPenalized() { + return mIsPenalized; + } + + /** Dump the information of this instance */ + public void dump(IndentingPrintWriter pw) { + pw.println("UnderlyingNetworkEvaluator:"); + pw.increaseIndent(); + + if (mNetworkRecordBuilder.isValid()) { + getNetworkRecord().dump(pw); + } else { + pw.println( + "UnderlyingNetworkRecord incomplete: mNetwork: " + + mNetworkRecordBuilder.getNetwork()); + } + + pw.println("mIsSelected: " + mIsSelected); + pw.println("mPriorityClass: " + mPriorityClass); + pw.println("mIsPenalized: " + mIsPenalized); + + pw.decreaseIndent(); + } + + private String getLogPrefix() { + return "[Network " + mNetworkRecordBuilder.getNetwork() + "] "; + } + + private void logInfo(String msg) { + Slog.i(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[INFO ] " + TAG + getLogPrefix() + msg); + } + + private void logWtf(String msg) { + Slog.wtf(TAG, getLogPrefix() + msg); + LOCAL_LOG.log("[WTF ] " + TAG + getLogPrefix() + msg); + } +} diff --git a/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java b/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java new file mode 100644 index 0000000000..1945052b92 --- /dev/null +++ b/Vcn/service-b/src/com/android/server/vcn/routeselection/UnderlyingNetworkRecord.java @@ -0,0 +1,143 @@ +/* + * 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.routeselection; + + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.util.IndentingPrintWriter; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.annotations.VisibleForTesting.Visibility; + +import java.util.Objects; + +/** + * A record of a single underlying network, caching relevant fields. + * + * @hide + */ +public class UnderlyingNetworkRecord { + @NonNull public final Network network; + @NonNull public final NetworkCapabilities networkCapabilities; + @NonNull public final LinkProperties linkProperties; + public final boolean isBlocked; + + @VisibleForTesting(visibility = Visibility.PRIVATE) + public UnderlyingNetworkRecord( + @NonNull Network network, + @NonNull NetworkCapabilities networkCapabilities, + @NonNull LinkProperties linkProperties, + boolean isBlocked) { + this.network = network; + this.networkCapabilities = networkCapabilities; + this.linkProperties = linkProperties; + this.isBlocked = isBlocked; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UnderlyingNetworkRecord)) return false; + final UnderlyingNetworkRecord that = (UnderlyingNetworkRecord) o; + + return network.equals(that.network) + && networkCapabilities.equals(that.networkCapabilities) + && linkProperties.equals(that.linkProperties) + && isBlocked == that.isBlocked; + } + + @Override + public int hashCode() { + return Objects.hash(network, networkCapabilities, linkProperties, isBlocked); + } + + /** Return whether two records represent the same network */ + public static boolean isSameNetwork( + @Nullable UnderlyingNetworkRecord leftRecord, + @Nullable UnderlyingNetworkRecord rightRecord) { + final Network left = leftRecord == null ? null : leftRecord.network; + final Network right = rightRecord == null ? null : rightRecord.network; + return Objects.equals(left, right); + } + + /** Dumps the state of this record for logging and debugging purposes. */ + void dump(IndentingPrintWriter pw) { + pw.println("UnderlyingNetworkRecord:"); + pw.increaseIndent(); + + pw.println("mNetwork: " + network); + pw.println("mNetworkCapabilities: " + networkCapabilities); + pw.println("mLinkProperties: " + linkProperties); + + pw.decreaseIndent(); + } + + /** Builder to incrementally construct an UnderlyingNetworkRecord. */ + static class Builder { + @NonNull private final Network mNetwork; + + @Nullable private NetworkCapabilities mNetworkCapabilities; + @Nullable private LinkProperties mLinkProperties; + boolean mIsBlocked; + boolean mWasIsBlockedSet; + + Builder(@NonNull Network network) { + mNetwork = network; + } + + @NonNull + Network getNetwork() { + return mNetwork; + } + + void setNetworkCapabilities(@NonNull NetworkCapabilities networkCapabilities) { + mNetworkCapabilities = networkCapabilities; + } + + @Nullable + NetworkCapabilities getNetworkCapabilities() { + return mNetworkCapabilities; + } + + void setLinkProperties(@NonNull LinkProperties linkProperties) { + mLinkProperties = linkProperties; + } + + void setIsBlocked(boolean isBlocked) { + mIsBlocked = isBlocked; + mWasIsBlockedSet = true; + } + + boolean isValid() { + return mNetworkCapabilities != null && mLinkProperties != null && mWasIsBlockedSet; + } + + UnderlyingNetworkRecord build() { + if (!isValid()) { + throw new IllegalArgumentException( + "Called build before UnderlyingNetworkRecord was valid"); + } + + return new UnderlyingNetworkRecord( + mNetwork, mNetworkCapabilities, mLinkProperties, mIsBlocked); + } + } +} diff --git a/Vcn/service-b/vcn-location-flag/module/com/android/server/vcn/VcnLocation.java b/Vcn/service-b/vcn-location-flag/module/com/android/server/vcn/VcnLocation.java new file mode 100644 index 0000000000..6c7d24db8b --- /dev/null +++ b/Vcn/service-b/vcn-location-flag/module/com/android/server/vcn/VcnLocation.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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; + +/** + * Class to represent that VCN is in a mainline module + * + *

This class is used to check whether VCN is in the non-updatable platform or in a mainline + * module. + */ +// When VCN is in a mainline module, this class (module/com/android/server/vcn/VcnLocation.java) +// will be built in to the vcn-location-sources filegroup. When VCN is in the non-updatable +// platform, platform/com/android/server/vcn/VcnLocation.java will be built in to the filegroup +public class VcnLocation { + /** Indicate that VCN is the platform */ + public static final boolean IS_VCN_IN_MAINLINE = true; +} diff --git a/Vcn/service-b/vcn-location-flag/platform/com/android/server/vcn/VcnLocation.java b/Vcn/service-b/vcn-location-flag/platform/com/android/server/vcn/VcnLocation.java new file mode 100644 index 0000000000..c6c82a55de --- /dev/null +++ b/Vcn/service-b/vcn-location-flag/platform/com/android/server/vcn/VcnLocation.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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; + +/** + * Class to represent that VCN is in the platform + * + *

This class is used to check whether VCN is in the non-updatable platform or in a mainline + * module. + */ +// When VCN is in a mainline module, module/com/android/server/vcn/VcnLocation.java +// will be built in to the vcn-location-sources filegroup. When VCN is in the non-updatable +// platform, this class (platform/com/android/server/vcn/VcnLocation.java) will be built in to the +// filegroup +public class VcnLocation { + /** Indicate that VCN is the platform */ + public static final boolean IS_VCN_IN_MAINLINE = false; +} diff --git a/Vcn/tests/cts/Android.bp b/Vcn/tests/cts/Android.bp new file mode 100644 index 0000000000..09101b2ef7 --- /dev/null +++ b/Vcn/tests/cts/Android.bp @@ -0,0 +1,68 @@ +// 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 { + default_team: "trendy_team_enigma", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "CtsVcnTestCases", + defaults: [ + "cts_defaults", + "framework-connectivity-test-defaults", + ], + + min_sdk_version: "36", + + // Include both the 32 and 64 bit versions + compile_multilib: "both", + + libs: [ + "android.test.base.stubs.system", + ], + + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.ext.junit", + "compatibility-device-util-axt", + "cts-net-utils", + "ctstestrunner-axt", + "ike-tun-utils", + "net-tests-utils", + "telephony-cts-utils", + "android.net.vcn.flags-aconfig-java-export", + "flag-junit", + ], + + platform_apis: true, + + // Tag this module as a cts test artifact + // TODO: b/375213246 Add "mcts-tethering" when VCN libs are + // built into Tethering module + // TODO: b/379938062 Move CtsVcnTestCases to the Tethering + // module folder (packages/modules/Connectivity) + test_suites: [ + "cts", + "general-tests", + "mts-tethering", + "mcts-tethering", + ], + data: [":ConnectivityTestPreparer"], + host_required: ["net-tests-utils-host-common"], + host_common_data: [":net-tests-utils-host-common"], +} diff --git a/Vcn/tests/cts/AndroidManifest.xml b/Vcn/tests/cts/AndroidManifest.xml new file mode 100644 index 0000000000..4c33d87e24 --- /dev/null +++ b/Vcn/tests/cts/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Vcn/tests/cts/AndroidTest.xml b/Vcn/tests/cts/AndroidTest.xml new file mode 100644 index 0000000000..a5cde0baf2 --- /dev/null +++ b/Vcn/tests/cts/AndroidTest.xml @@ -0,0 +1,46 @@ + + +