Expose network support configuration into AVF Java Framework API

Bug: 340376951
Test: atest MicrodroidTests
Test: atest MicrodroidTestAppNoInternetPerm

Change-Id: I107de04e4c2890936a9c3fb4d6f61b8aff509ace
diff --git a/TEST_MAPPING b/TEST_MAPPING
index db0b43a..6d585a6 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -18,6 +18,9 @@
       "name": "MicrodroidTestApp"
     },
     {
+      "name": "MicrodroidTestAppNoInternetPerm"
+    },
+    {
       "name": "MicrodroidTestAppNoPerm"
     },
     {
diff --git a/java/framework/api/test-current.txt b/java/framework/api/test-current.txt
index d20d543..30dd7d9 100644
--- a/java/framework/api/test-current.txt
+++ b/java/framework/api/test-current.txt
@@ -11,12 +11,14 @@
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public java.util.List<java.lang.String> getExtraApks();
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public String getOs();
     method @Nullable public String getPayloadConfigPath();
+    method public boolean isNetworkSupported();
     method public boolean isVmConsoleInputSupported();
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String MICRODROID = "microdroid";
   }
 
   public static final class VirtualMachineConfig.Builder {
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder addExtraApk(@NonNull String);
+    method @NonNull @RequiresPermission(allOf={android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION, android.Manifest.permission.INTERNET}) public android.system.virtualmachine.VirtualMachineConfig.Builder setNetworkSupported(boolean);
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setOs(@NonNull String);
     method @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadConfigPath(@NonNull String);
     method @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") @NonNull @RequiresPermission(android.system.virtualmachine.VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION) public android.system.virtualmachine.VirtualMachineConfig.Builder setVendorDiskImage(@NonNull java.io.File);
@@ -31,6 +33,7 @@
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    field public static final String FEATURE_NETWORK = "com.android.kvm.NETWORK";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
     field @FlaggedApi("com.android.system.virtualmachine.flags.avf_v_test_apis") public static final String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
   }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 4d3bf2d..428d3e3 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -22,6 +22,7 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.Manifest;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -79,7 +80,8 @@
     private static String[] EMPTY_STRING_ARRAY = {};
 
     // These define the schema of the config file persisted on disk.
-    private static final int VERSION = 8;
+    // Please bump up the version number when adding a new key.
+    private static final int VERSION = 9;
     private static final String KEY_VERSION = "version";
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
@@ -98,6 +100,7 @@
     private static final String KEY_VENDOR_DISK_IMAGE_PATH = "vendorDiskImagePath";
     private static final String KEY_OS = "os";
     private static final String KEY_EXTRA_APKS = "extraApks";
+    private static final String KEY_NETWORK_SUPPORTED = "networkSupported";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -205,6 +208,9 @@
     /** OS name of the VM using payload binaries. */
     @NonNull @OsName private final String mOs;
 
+    /** Whether to run the VM with supporting network feature or not. */
+    private final boolean mNetworkSupported;
+
     @Retention(RetentionPolicy.SOURCE)
     @StringDef(
             prefix = "MICRODROID",
@@ -239,7 +245,8 @@
             boolean vmConsoleInputSupported,
             boolean connectVmConsole,
             @Nullable File vendorDiskImage,
-            @NonNull @OsName String os) {
+            @NonNull @OsName String os,
+            boolean networkSupported) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -262,6 +269,7 @@
         mConnectVmConsole = connectVmConsole;
         mVendorDiskImage = vendorDiskImage;
         mOs = os;
+        mNetworkSupported = networkSupported;
     }
 
     /** Loads a config from a file. */
@@ -362,6 +370,8 @@
             }
         }
 
+        builder.setNetworkSupported(b.getBoolean(KEY_NETWORK_SUPPORTED));
+
         return builder.build();
     }
 
@@ -412,6 +422,7 @@
             String[] extraApks = mExtraApks.toArray(new String[0]);
             b.putStringArray(KEY_EXTRA_APKS, extraApks);
         }
+        b.putBoolean(KEY_NETWORK_SUPPORTED, mNetworkSupported);
         b.writeToStream(output);
     }
 
@@ -588,6 +599,16 @@
     }
 
     /**
+     * Returns whether the network feature is supported to the VM or not.
+     *
+     * @hide
+     */
+    @TestApi
+    public boolean isNetworkSupported() {
+        return mNetworkSupported;
+    }
+
+    /**
      * Tests if this config is compatible with other config. Being compatible means that the configs
      * can be interchangeably used for the same virtual machine; they do not change the VM identity
      * or secrets. Such changes include varying the number of CPUs or the size of the RAM. Changes
@@ -682,6 +703,7 @@
         config.cpuTopology = (byte) this.mCpuTopology;
         config.consoleInputDevice = mConsoleInputDevice;
         config.devices = EMPTY_STRING_ARRAY;
+        config.networkSupported = this.mNetworkSupported;
         config.platformVersion = "~1.0";
         return config;
     }
@@ -733,18 +755,23 @@
                 vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
                 break;
         }
-        if (mVendorDiskImage != null) {
+
+        if (mVendorDiskImage != null || mNetworkSupported) {
             VirtualMachineAppConfig.CustomConfig customConfig =
                     new VirtualMachineAppConfig.CustomConfig();
             customConfig.devices = EMPTY_STRING_ARRAY;
-            try {
-                customConfig.vendorImage =
-                        ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
-            } catch (FileNotFoundException e) {
-                throw new VirtualMachineException(
-                        "Failed to open vendor disk image " + mVendorDiskImage.getAbsolutePath(),
-                        e);
+            if (mVendorDiskImage != null) {
+                try {
+                    customConfig.vendorImage =
+                            ParcelFileDescriptor.open(mVendorDiskImage, MODE_READ_ONLY);
+                } catch (FileNotFoundException e) {
+                    throw new VirtualMachineException(
+                            "Failed to open vendor disk image "
+                                    + mVendorDiskImage.getAbsolutePath(),
+                            e);
+                }
             }
+            customConfig.networkSupported = mNetworkSupported;
             vsConfig.customConfig = customConfig;
         }
         return vsConfig;
@@ -826,6 +853,7 @@
         private boolean mConnectVmConsole = false;
         @Nullable private File mVendorDiskImage;
         @NonNull @OsName private String mOs = DEFAULT_OS;
+        private boolean mNetworkSupported;
 
         /**
          * Creates a builder for the given context.
@@ -902,6 +930,10 @@
                         "debug level must be FULL to connect to the console");
             }
 
+            if (mNetworkSupported && mProtectedVm) {
+                throw new IllegalStateException("network is not supported on pVM");
+            }
+
             return new VirtualMachineConfig(
                     packageName,
                     apkPath,
@@ -919,7 +951,8 @@
                     mVmConsoleInputSupported,
                     mConnectVmConsole,
                     mVendorDiskImage,
-                    mOs);
+                    mOs,
+                    mNetworkSupported);
         }
 
         /**
@@ -1224,5 +1257,22 @@
             mOs = requireNonNull(os, "os must not be null");
             return this;
         }
+
+        /**
+         * Sets whether to support network feature to VM. Default is {@code false}.
+         *
+         * @hide
+         */
+        @TestApi
+        @RequiresPermission(
+                allOf = {
+                    VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION,
+                    Manifest.permission.INTERNET
+                })
+        @NonNull
+        public Builder setNetworkSupported(boolean networkSupported) {
+            mNetworkSupported = networkSupported;
+            return this;
+        }
     }
 }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
index 4a9e943..abb2c81 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineManager.java
@@ -123,6 +123,7 @@
                 FEATURE_DICE_CHANGES,
                 FEATURE_LLPVM_CHANGES,
                 FEATURE_MULTI_TENANT,
+                FEATURE_NETWORK,
                 FEATURE_REMOTE_ATTESTATION,
                 FEATURE_VENDOR_MODULES,
             })
@@ -147,6 +148,13 @@
     public static final String FEATURE_MULTI_TENANT = IVirtualizationService.FEATURE_MULTI_TENANT;
 
     /**
+     * Feature to allow network features in VM.
+     *
+     * @hide
+     */
+    @TestApi public static final String FEATURE_NETWORK = IVirtualizationService.FEATURE_NETWORK;
+
+    /**
      * Feature to allow remote attestation in Microdroid.
      *
      * @hide
diff --git a/libs/avf_features/src/lib.rs b/libs/avf_features/src/lib.rs
index c0faab0..1ebe2a4 100644
--- a/libs/avf_features/src/lib.rs
+++ b/libs/avf_features/src/lib.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
     IVirtualizationService::FEATURE_DICE_CHANGES, IVirtualizationService::FEATURE_LLPVM_CHANGES,
-    IVirtualizationService::FEATURE_MULTI_TENANT,
+    IVirtualizationService::FEATURE_MULTI_TENANT, IVirtualizationService::FEATURE_NETWORK,
     IVirtualizationService::FEATURE_REMOTE_ATTESTATION,
     IVirtualizationService::FEATURE_VENDOR_MODULES,
 };
@@ -28,6 +28,7 @@
         FEATURE_DICE_CHANGES => cfg!(dice_changes),
         FEATURE_LLPVM_CHANGES => cfg!(llpvm_changes),
         FEATURE_MULTI_TENANT => cfg!(multi_tenant),
+        FEATURE_NETWORK => cfg!(network),
         FEATURE_REMOTE_ATTESTATION => cfg!(remote_attestation),
         FEATURE_VENDOR_MODULES => cfg!(vendor_modules),
         _ => {
diff --git a/tests/testapk/AndroidManifestV5.xml b/tests/testapk/AndroidManifestV5.xml
index 7d97680..b869586 100644
--- a/tests/testapk/AndroidManifestV5.xml
+++ b/tests/testapk/AndroidManifestV5.xml
@@ -18,6 +18,7 @@
       android:versionCode="5">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.INTERNET" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
     <queries>
diff --git a/tests/testapk/AndroidManifestV6.xml b/tests/testapk/AndroidManifestV6.xml
index 19d5674..c55da85 100644
--- a/tests/testapk/AndroidManifestV6.xml
+++ b/tests/testapk/AndroidManifestV6.xml
@@ -18,6 +18,7 @@
       android:versionCode="6">
     <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
     <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.INTERNET" />
     <uses-sdk android:minSdkVersion="33" android:targetSdkVersion="33" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
     <queries>
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 12a46f7..4d8dac6 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -571,8 +571,9 @@
         assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
         assertThat(minimal.isEncryptedStorageEnabled()).isFalse();
         assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
-        assertThat(minimal.isVmOutputCaptured()).isEqualTo(false);
+        assertThat(minimal.isVmOutputCaptured()).isFalse();
         assertThat(minimal.getOs()).isEqualTo("microdroid");
+        assertThat(minimal.isNetworkSupported()).isFalse();
 
         // Maximal has everything that can be set to some non-default value. (And has different
         // values than minimal for the required fields.)
@@ -589,6 +590,9 @@
                         .setEncryptedStorageBytes(1_000_000)
                         .setVmOutputCaptured(true)
                         .setOs("microdroid_gki-android14-6.1");
+        if (!mProtectedVm) {
+            maximalBuilder.setNetworkSupported(true);
+        }
         VirtualMachineConfig maximal = maximalBuilder.build();
 
         assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
@@ -603,8 +607,11 @@
         assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
         assertThat(maximal.isEncryptedStorageEnabled()).isTrue();
         assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
-        assertThat(maximal.isVmOutputCaptured()).isEqualTo(true);
+        assertThat(maximal.isVmOutputCaptured()).isTrue();
         assertThat(maximal.getOs()).isEqualTo("microdroid_gki-android14-6.1");
+        if (!mProtectedVm) {
+            assertThat(maximal.isNetworkSupported()).isTrue();
+        }
 
         assertThat(minimal.isCompatibleWith(maximal)).isFalse();
         assertThat(minimal.isCompatibleWith(minimal)).isTrue();
@@ -659,6 +666,18 @@
                         .setVmConsoleInputSupported(true);
         e = assertThrows(IllegalStateException.class, () -> captureInputOnNonDebuggable.build());
         assertThat(e).hasMessageThat().contains("debug level must be FULL to use console input");
+
+        if (mProtectedVm) {
+            VirtualMachineConfig.Builder networkSupportedOnProtectedVm =
+                    newVmConfigBuilderWithPayloadBinary("binary.so")
+                            .setProtectedVm(mProtectedVm)
+                            .setNetworkSupported(true);
+            e =
+                    assertThrows(
+                            IllegalStateException.class,
+                            () -> networkSupportedOnProtectedVm.build());
+            assertThat(e).hasMessageThat().contains("network is not supported on pVM");
+        }
     }
 
     @Test
@@ -2300,6 +2319,49 @@
         }
     }
 
+    private VirtualMachineConfig buildVmConfigWithNetworkSupported() throws Exception {
+        return buildVmConfigWithNetworkSupported("MicrodroidTestNativeLib.so");
+    }
+
+    private VirtualMachineConfig buildVmConfigWithNetworkSupported(String binaryPath)
+            throws Exception {
+        assumeSupportedDevice();
+        assumeNonProtectedVM();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_NETWORK);
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary(binaryPath)
+                        .setNetworkSupported(true)
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .build();
+        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+        return config;
+    }
+
+    @Test
+    public void configuringNetworkSupportedRequiresCustomPermission() throws Exception {
+        VirtualMachineConfig config = buildVmConfigWithNetworkSupported();
+        revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine(
+                        "test_network_supported_req_custom_permission", config);
+        SecurityException e =
+                assertThrows(
+                        SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
+        assertThat(e)
+                .hasMessageThat()
+                .contains("android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission");
+    }
+
+    @Test
+    public void bootsWithNetworkSupported() throws Exception {
+        VirtualMachineConfig config = buildVmConfigWithNetworkSupported();
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine("test_boot_with_network_supported", config);
+        runVmTestService(TAG, vm, (ts, tr) -> {}).assertNoException();
+    }
+
     private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
         return buildVmConfigWithVendor(vendorDiskImage, "MicrodroidTestNativeLib.so");
     }
diff --git a/tests/testapk_no_internet_perm/Android.bp b/tests/testapk_no_internet_perm/Android.bp
new file mode 100644
index 0000000..d23081f
--- /dev/null
+++ b/tests/testapk_no_internet_perm/Android.bp
@@ -0,0 +1,26 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+    name: "MicrodroidTestAppNoInternetPerm",
+    static_libs: [
+        "MicrodroidDeviceTestHelper",
+        "MicrodroidTestHelper",
+        "androidx.test.runner",
+        "androidx.test.ext.junit",
+        "com.android.microdroid.testservice-java",
+        "truth",
+        "compatibility-common-util-devicesidelib",
+    ],
+    jni_libs: [
+        "MicrodroidTestNativeLib",
+    ],
+    test_suites: [
+        "general-tests",
+        "cts",
+    ],
+    srcs: ["src/java/**/*.java"],
+    defaults: ["MicrodroidTestAppsDefaults"],
+    min_sdk_version: "34",
+}
diff --git a/tests/testapk_no_internet_perm/AndroidManifest.xml b/tests/testapk_no_internet_perm/AndroidManifest.xml
new file mode 100644
index 0000000..87b302a
--- /dev/null
+++ b/tests/testapk_no_internet_perm/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.android.microdroid.test_no_internet_perm">
+    <uses-permission android:name="android.permission.MANAGE_VIRTUAL_MACHINE" />
+    <uses-permission android:name="android.permission.USE_CUSTOM_VIRTUAL_MACHINE" />
+    <uses-sdk android:minSdkVersion="34" android:targetSdkVersion="34" />
+    <uses-feature android:name="android.software.virtualization_framework" android:required="false" />
+    <application />
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.microdroid.test_no_internet_perm"
+        android:label="No Internet Permission Microdroid Test" />
+</manifest>
diff --git a/tests/testapk_no_internet_perm/AndroidTest.xml b/tests/testapk_no_internet_perm/AndroidTest.xml
new file mode 100644
index 0000000..61f8b8c
--- /dev/null
+++ b/tests/testapk_no_internet_perm/AndroidTest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs Microdroid Tests with no internet permission">
+    <option name="test-suite-tag" value="cts" />
+    <option name="config-descriptor:metadata" key="component" value="security" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+    <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+    <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="test-file-name" value="MicrodroidTestAppNoInternetPerm.apk" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.microdroid.test_no_internet_perm" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+        <option name="shell-timeout" value="300000" />
+        <option name="test-timeout" value="300000" />
+    </test>
+</configuration>
diff --git a/tests/testapk_no_internet_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoInternetPerm.java b/tests/testapk_no_internet_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoInternetPerm.java
new file mode 100644
index 0000000..767f745
--- /dev/null
+++ b/tests/testapk_no_internet_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoInternetPerm.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 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.microdroid.test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.system.virtualmachine.VirtualMachine;
+import android.system.virtualmachine.VirtualMachineConfig;
+import android.system.virtualmachine.VirtualMachineManager;
+
+import com.android.microdroid.test.device.MicrodroidDeviceTestBase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+/**
+ * Test that the android.permission.MANAGE_VIRTUAL_MACHINE is enforced and that an app cannot launch
+ * a VM without said permission.
+ */
+@RunWith(Parameterized.class)
+public class MicrodroidTestAppNoInternetPerm extends MicrodroidDeviceTestBase {
+    private static final String TAG = "MicrodroidTestAppNoInternetPerm";
+
+    @Parameterized.Parameters(name = "protectedVm={0}")
+    public static Object[] protectedVmConfigs() {
+        return new Object[] {false, true};
+    }
+
+    @Parameterized.Parameter public boolean mProtectedVm;
+
+    @Before
+    public void setup() {
+        prepareTestSetup(mProtectedVm, null);
+    }
+
+    @Test
+    public void configuringNetworkSupportedRequiresInternetPermission() throws Exception {
+        assumeSupportedDevice();
+        assumeNonProtectedVM();
+        assumeFeatureEnabled(VirtualMachineManager.FEATURE_NETWORK);
+
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
+                        .setNetworkSupported(true)
+                        .build();
+
+        VirtualMachine vm =
+                forceCreateNewVirtualMachine(
+                        "config_network_supported_req_internet_permission", config);
+        SecurityException e =
+                assertThrows(
+                        SecurityException.class, () -> runVmTestService(TAG, vm, (ts, tr) -> {}));
+        assertThat(e).hasMessageThat().contains("android.permission.INTERNET permission");
+    }
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
index f8b5087..234d8d0 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/IVirtualizationService.aidl
@@ -25,6 +25,7 @@
     const String FEATURE_DICE_CHANGES = "com.android.kvm.DICE_CHANGES";
     const String FEATURE_LLPVM_CHANGES = "com.android.kvm.LLPVM_CHANGES";
     const String FEATURE_MULTI_TENANT = "com.android.kvm.MULTI_TENANT";
+    const String FEATURE_NETWORK = "com.android.kvm.NETWORK";
     const String FEATURE_REMOTE_ATTESTATION = "com.android.kvm.REMOTE_ATTESTATION";
     const String FEATURE_VENDOR_MODULES = "com.android.kvm.VENDOR_MODULES";
 
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 5592f14..3c45836 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -510,6 +510,7 @@
     }
 
     fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+        check_internet_permission()?;
         check_use_custom_virtual_machine()?;
         if !cfg!(network) {
             return Err(Status::new_exception_str(
@@ -911,6 +912,12 @@
     check_permission("android.permission.USE_CUSTOM_VIRTUAL_MACHINE")
 }
 
+/// Check whether the caller of the current Binder method is allowed to create socket and
+/// establish connection between the VM and the Internet.
+fn check_internet_permission() -> binder::Result<()> {
+    check_permission("android.permission.INTERNET")
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;