Merge "Ferrochrome: Extracting image while downloading" into main
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 6d585a6..db0b43a 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -18,9 +18,6 @@
       "name": "MicrodroidTestApp"
     },
     {
-      "name": "MicrodroidTestAppNoInternetPerm"
-    },
-    {
       "name": "MicrodroidTestAppNoPerm"
     },
     {
diff --git a/apex/product_packages.mk b/apex/product_packages.mk
index a318817..486334c 100644
--- a/apex/product_packages.mk
+++ b/apex/product_packages.mk
@@ -56,3 +56,9 @@
     $(error RELEASE_AVF_ENABLE_DICE_CHANGES must also be enabled)
   endif
 endif
+
+ifdef RELEASE_AVF_ENABLE_NETWORK
+  ifndef RELEASE_AVF_ENABLE_LLPVM_CHANGES
+    $(error RELEASE_AVF_ENABLE_LLPVM_CHANGES must also be enabled)
+  endif
+endif
diff --git a/compos/Android.bp b/compos/Android.bp
index b840506..220533a 100644
--- a/compos/Android.bp
+++ b/compos/Android.bp
@@ -25,7 +25,7 @@
         "librpcbinder_rs",
         "librustutils",
         "libscopeguard",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
     prefer_rlib: true,
     shared_libs: [
diff --git a/compos/src/compsvc_main.rs b/compos/src/compsvc_main.rs
index 06cc599..9bc522c 100644
--- a/compos/src/compsvc_main.rs
+++ b/compos/src/compsvc_main.rs
@@ -23,13 +23,9 @@
 mod fsverity;
 
 use anyhow::Result;
-use binder::unstable_api::AsNative;
 use compos_common::COMPOS_VSOCK_PORT;
 use log::{debug, error};
-use std::os::raw::c_void;
 use std::panic;
-use std::ptr;
-use vm_payload_bindgen::{AIBinder, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer};
 
 fn main() {
     if let Err(e) = try_main() {
@@ -50,17 +46,5 @@
     }));
 
     debug!("compsvc is starting as a rpc service.");
-    let param = ptr::null_mut();
-    let mut service = compsvc::new_binder()?.as_binder();
-    let service = service.as_native_mut() as *mut AIBinder;
-    // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
-    // is the same type as sys::AIBinder. It is safe for on_ready to be invoked at any time, with
-    // any parameter.
-    unsafe { AVmPayload_runVsockRpcServer(service, COMPOS_VSOCK_PORT, Some(on_ready), param) }
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
-    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
-    // call at any time.
-    unsafe { AVmPayload_notifyPayloadReady() };
+    vm_payload::run_single_vsock_service(compsvc::new_binder()?, COMPOS_VSOCK_PORT)
 }
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 1e15d16..945798f 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -65,25 +65,20 @@
 
 #### Download from build server
 
-  - Step 1) Go to the link https://ci.chromium.org/ui/p/chromeos/builders/chromiumos/ferrochrome-public-main/
-    - Note: I 'searched' the ferrochrome target with builder search.
-  - Step 2) Click a build number
-  - Step 3) Expand steps and find `48. upload artifacts`.
-  - Step 4) Click `gs upload dir`. You'll see Cloud storage with comprehensive artifacts (e.g. [Here](https://pantheon.corp.google.com/storage/browser/chromiumos-image-archive/ferrochrome-public/R126-15883.0.0) is the initial build of ferrochrome)
-  - Step 5) Download `image.zip`, which contains working vmlinuz.
-    - Note: DO NOT DOWNLOAD `vmlinuz.tar.xz` from the CI.
-  - Step 6) Uncompress `image.zip`, and boot with `chromiumos_test_image.bin` and `boot_images/vmlinuz`.
-    - Note: DO NOT USE `vmlinuz.bin`.
+Here's the link the comprehensive artifacts
+https://pantheon.corp.google.com/storage/browser/chromiumos-image-archive/ferrochrome-public
 
-IMPORTANT: DO NOT USE `vmlinuz.bin` for passing to crosvm. It doesn't pick-up the correct `init` process (picks `/init` instead of `/sbin/init`, and `cfg80211` keeps crashing (i.e. no network)
+Pick a build, download, and untar `chromiumos_test_image.tar.xz`. We'll boot with `chromiumos_test_image.bin` in it.
 
+To find the latest green build, check following:
+https://pantheon.corp.google.com/storage/browser/_details/chromiumos-image-archive/ferrochrome-public/LATEST-main
 
 #### Build ChromiumOS for VM
 
 First, check out source code from the ChromiumOS and Chromium projects.
 
+* Checking out Chromium: https://www.chromium.org/developers/how-tos/get-the-code/
 * Checking out ChromiumOS: https://www.chromium.org/chromium-os/developer-library/guides/development/developer-guide/
-* Checking out Chromium: https://g3doc.corp.google.com/chrome/chromeos/system_services_team/dev_instructions/g3doc/setup_checkout.md?cl=head
 
 Important: When you are at the step “Set up gclient args” in the Chromium checkout instruction, configure .gclient as follows.
 
@@ -95,9 +90,7 @@
     "url": "https://chromium.googlesource.com/chromium/src.git",
     "managed": False,
     "custom_deps": {},
-    "custom_vars": {
-      "checkout_src_internal": True,
-    },
+    "custom_vars": {},
   },
 ]
 target_os = ['chromeos']
@@ -162,10 +155,7 @@
 
 Don’t forget to call `build-image` afterwards.
 
-You need two outputs:
-
-* ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
-* The kernel: ~/chromiumos/src/build/images/ferrochrome/latest/boot_images/vmlinuz
+You need ChromiumOS disk image: ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin
 
 ### Create a guest VM configuration
 
@@ -173,7 +163,6 @@
 
 ```
 $ adb push  ~/chromiumos/src/build/images/ferrochrome/latest/chromiumos_test_image.bin /data/local/tmp/
-$ adb push ~/chromiumos/out/build/ferrochrome/boot/vmlinuz /data/local/tmp/kernel
 ```
 
 Create a VM config file as below.
@@ -182,7 +171,6 @@
 $ cat > vm_config.json; adb push vm_config.json /data/local/tmp
 {
     "name": "cros",
-    "kernel": "/data/local/tmp/kernel",
     "disks": [
         {
             "image": "/data/local/tmp/chromiumos_test_image.bin",
@@ -210,7 +198,7 @@
 
 ```
 $ adb root
-$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivity
+$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivityAlias
 $ adb unroot
 ```
 
@@ -218,71 +206,14 @@
 permission to the app.
 ```
 $ adb root
-$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivity
+$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivityAlias
 $ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
 $ adb unroot
 ```
-Then execute the below to set up the network. In the future, this step won't be necessary.
 
-```
-$ cat > setup_network.sh; adb push setup_network.sh /data/local/tmp
-#!/system/bin/sh
+Second, ensure your device is connected to the Internet.
 
-set -e
-
-TAP_IFACE=crosvm_tap
-TAP_ADDR=192.168.1.1
-TAP_NET=192.168.1.0
-
-function setup_network() {
-  local WAN_IFACE=$(ip route get 8.8.8.8 2> /dev/null | awk -- '{printf $5}')
-  if [ "${WAN_IFACE}" == "" ]; then
-    echo "No network. Connect to a WiFi network and start again"
-    return 1
-  fi
-
-  if ip link show ${TAP_IFACE} &> /dev/null ; then
-    echo "TAP interface ${TAP_IFACE} already exists"
-    return 1
-  fi
-
-  ip tuntap add mode tap group virtualmachine vnet_hdr ${TAP_IFACE}
-  ip addr add ${TAP_ADDR}/24 dev ${TAP_IFACE}
-  ip link set ${TAP_IFACE} up
-  ip rule flush
-  ip rule add from all lookup ${WAN_IFACE}
-  ip route add ${TAP_NET}/24 dev ${TAP_IFACE} table ${WAN_IFACE}
-  sysctl net.ipv4.ip_forward=1
-  iptables -t filter -F
-  iptables -t nat -A POSTROUTING -s ${TAP_NET}/24 -j MASQUERADE
-}
-
-function setup_if_necessary() {
-  if [ "$(getprop ro.crosvm.network.setup.done)" == 1 ]; then
-    return
-  fi
-  echo "Setting up..."
-  check_privilege
-  setup_network
-  setenforce 0
-  chmod 666 /dev/tun
-  setprop ro.crosvm.network.setup.done 1
-}
-
-function check_privilege() {
-  if [ "$(id -u)" -ne 0 ]; then
-    echo "Run 'adb root' first"
-    return 1
-  fi
-}
-
-setup_if_necessary
-^D
-
-adb root; adb shell /data/local/tmp/setup_network.sh
-```
-
-Then, finally tap the VmLauncherApp app from the launcher UI. You will see
+Finally, tap the VmLauncherApp app from the launcher UI. You will see
 Ferrochrome booting!
 
 If it doesn’t work well, try
diff --git a/ferrochrome_app/Android.bp b/ferrochrome_app/Android.bp
new file mode 100644
index 0000000..182d289
--- /dev/null
+++ b/ferrochrome_app/Android.bp
@@ -0,0 +1,23 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_app {
+    name: "FerrochromeApp",
+    srcs: ["java/**/*.java"],
+    resource_dirs: ["res"],
+    platform_apis: true,
+    // TODO(b/348113995): move this app to product partition
+    system_ext_specific: true,
+    privileged: true,
+    init_rc: ["custom_vm_setup.rc"],
+    required: ["custom_vm_setup"],
+    certificate: "platform",
+}
+
+sh_binary {
+    name: "custom_vm_setup",
+    src: "custom_vm_setup.sh",
+    system_ext_specific: true,
+    host_supported: false,
+}
diff --git a/ferrochrome_app/AndroidManifest.xml b/ferrochrome_app/AndroidManifest.xml
new file mode 100644
index 0000000..d783bbc
--- /dev/null
+++ b/ferrochrome_app/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.virtualization.ferrochrome"
+    android:sharedUserId="android.uid.system" >
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <application
+        android:label="Ferrochrome">
+        <activity android:name=".FerrochromeActivity"
+                  android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
+                  android:theme="@style/MyTheme"
+                  android:screenOrientation="landscape"
+                  android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/ferrochrome_app/custom_vm_setup.rc b/ferrochrome_app/custom_vm_setup.rc
new file mode 100644
index 0000000..f4244b7
--- /dev/null
+++ b/ferrochrome_app/custom_vm_setup.rc
@@ -0,0 +1,23 @@
+# 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.
+
+service custom_vm_setup /system_ext/bin/custom_vm_setup
+    user shell
+    group shell
+    disabled
+    oneshot
+    seclabel u:r:shell:s0
+
+on property:debug.custom_vm_setup.start=true
+    start custom_vm_setup
\ No newline at end of file
diff --git a/ferrochrome_app/custom_vm_setup.sh b/ferrochrome_app/custom_vm_setup.sh
new file mode 100644
index 0000000..92a23a3
--- /dev/null
+++ b/ferrochrome_app/custom_vm_setup.sh
@@ -0,0 +1,13 @@
+#!/system/bin/sh
+
+function copy_files() {
+  cp -u /sdcard/vm_config.json /data/local/tmp
+  cp -u /sdcard/chromiumos_test_image.bin /data/local/tmp
+  chmod 666 /data/local/tmp/vm_config.json
+  chmod 666 /data/local/tmp/chromiumos_test_image.bin
+}
+setprop debug.custom_vm_setup.done false
+copy_files
+pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+setprop debug.custom_vm_setup.start false
+setprop debug.custom_vm_setup.done true
\ No newline at end of file
diff --git a/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
new file mode 100644
index 0000000..8a13030
--- /dev/null
+++ b/ferrochrome_app/java/com/android/virtualization/ferrochrome/FerrochromeActivity.java
@@ -0,0 +1,180 @@
+/*
+ * 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.virtualization.ferrochrome;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+public class FerrochromeActivity extends Activity {
+    ExecutorService executorService = Executors.newSingleThreadExecutor();
+    private static final String TAG = "FerrochromeActivity";
+    private static final String FERROCHROME_VERSION = "R128-15926.0.0";
+    private static final String EXTERNAL_STORAGE_DIR =
+            Environment.getExternalStorageDirectory().getPath() + File.separator;
+    private static final Path IMAGE_PATH =
+            Path.of(EXTERNAL_STORAGE_DIR + "chromiumos_test_image.bin");
+    private static final Path IMAGE_VERSION_INFO =
+            Path.of(EXTERNAL_STORAGE_DIR + "ferrochrome_image_version");
+    private static final Path VM_CONFIG_PATH = Path.of(EXTERNAL_STORAGE_DIR + "vm_config.json");
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_ferrochrome);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+        // Clean up the existing vm launcher process if there is
+        ActivityManager am = getSystemService(ActivityManager.class);
+        am.killBackgroundProcesses(getVmLauncherAppPackageName());
+
+        executorService.execute(
+                () -> {
+                    if (Files.notExists(IMAGE_PATH)
+                            || !FERROCHROME_VERSION.equals(getVersionInfo())) {
+                        updateStatus("image doesn't exist");
+                        updateStatus("download image");
+                        if (download(FERROCHROME_VERSION)) {
+                            updateStatus("download done");
+                        } else {
+                            updateStatus("download failed, check internet connection and retry");
+                            return;
+                        }
+                    } else {
+                        updateStatus("there are already downloaded images");
+                    }
+                    updateStatus("write down vm config");
+                    copyVmConfigJson();
+                    updateStatus("custom_vm_setup: copy files to /data/local/tmp");
+                    SystemProperties.set("debug.custom_vm_setup.start", "true");
+                    while (!SystemProperties.getBoolean("debug.custom_vm_setup.done", false)) {
+                        // Wait for custom_vm_setup
+                        try {
+                            Thread.sleep(1000);
+                        } catch (Exception e) {
+                            Log.d(TAG, e.toString());
+                        }
+                        updateStatus("wait for custom_vm_setup");
+                    }
+                    updateStatus("enable vmlauncher");
+                    updateStatus("ready for ferrochrome");
+                    runOnUiThread(
+                            () ->
+                                    startActivity(
+                                            new Intent()
+                                                    .setClassName(
+                                                            getVmLauncherAppPackageName(),
+                                                            "com.android.virtualization.vmlauncher.MainActivity")));
+                });
+    }
+
+    private String getVmLauncherAppPackageName() {
+        PackageManager pm = getPackageManager();
+        for (String packageName :
+                new String[] {
+                    "com.android.virtualization.vmlauncher",
+                    "com.google.android.virtualization.vmlauncher"
+                }) {
+            try {
+                pm.getPackageInfo(packageName, 0);
+                return packageName;
+            } catch (PackageManager.NameNotFoundException e) {
+                continue;
+            }
+        }
+        return null;
+    }
+
+    private void updateStatus(String line) {
+        Log.d(TAG, line);
+        runOnUiThread(
+                () -> {
+                    TextView statusView = findViewById(R.id.status_txt_view);
+                    statusView.append(line + "\n");
+                });
+    }
+
+    private void copyVmConfigJson() {
+        try (InputStream is = getResources().openRawResource(R.raw.vm_config)) {
+            Files.copy(is, VM_CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException e) {
+            updateStatus(e.toString());
+        }
+    }
+
+    private String getVersionInfo() {
+        try {
+            return new String(Files.readAllBytes(IMAGE_VERSION_INFO), StandardCharsets.UTF_8);
+        } catch (IOException e) {
+            return null;
+        }
+    }
+
+    private boolean updateVersionInfo(String version) {
+        try {
+            Files.write(IMAGE_VERSION_INFO, version.getBytes(StandardCharsets.UTF_8));
+        } catch (IOException e) {
+            Log.d(TAG, e.toString());
+        }
+        return true;
+    }
+
+    private boolean download(String version) {
+        String urlString =
+                "https://storage.googleapis.com/chromiumos-image-archive/ferrochrome-public/"
+                        + version
+                        + "/image.zip";
+        try (InputStream is = (new URL(urlString)).openStream();
+                ZipInputStream zis = new ZipInputStream(is)) {
+            ZipEntry entry;
+            while ((entry = zis.getNextEntry()) != null) {
+                if (!entry.getName().contains("chromiumos_test_image.bin")) {
+                    continue;
+                }
+                updateStatus("copy " + entry.getName() + " start");
+                Files.copy(zis, IMAGE_PATH, StandardCopyOption.REPLACE_EXISTING);
+                updateStatus("copy " + entry.getName() + " done");
+                updateVersionInfo(version);
+                break;
+            }
+        } catch (Exception e) {
+            updateStatus(e.toString());
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/ferrochrome_app/res/layout/activity_ferrochrome.xml b/ferrochrome_app/res/layout/activity_ferrochrome.xml
new file mode 100644
index 0000000..7d5e8aa
--- /dev/null
+++ b/ferrochrome_app/res/layout/activity_ferrochrome.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".FerrochromeActivity">
+  <TextView
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"
+      android:id="@+id/status_txt_view"/>
+
+</RelativeLayout>
\ No newline at end of file
diff --git a/ferrochrome_app/res/raw/vm_config.json b/ferrochrome_app/res/raw/vm_config.json
new file mode 100644
index 0000000..d79400c
--- /dev/null
+++ b/ferrochrome_app/res/raw/vm_config.json
@@ -0,0 +1,20 @@
+{
+    "name": "cros",
+    "disks": [
+        {
+            "image": "/data/local/tmp/chromiumos_test_image.bin",
+            "partitions": [],
+            "writable": true
+        }
+    ],
+    "params": "root=/dev/vda3 rootwait noinitrd ro enforcing=0 cros_debug cros_secure",
+    "protected": false,
+    "cpu_topology": "match_host",
+    "platform_version": "~1.0",
+    "memory_mib": 8096,
+    "gpu": {
+        "backend": "virglrenderer",
+        "context_types": ["virgl2"]
+    },
+    "console_input_device": "ttyS0"
+}
\ No newline at end of file
diff --git a/ferrochrome_app/res/values/themes.xml b/ferrochrome_app/res/values/themes.xml
new file mode 100644
index 0000000..c9a9ed2
--- /dev/null
+++ b/ferrochrome_app/res/values/themes.xml
@@ -0,0 +1,4 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+    <style name="MyTheme" parent="@android:style/Theme.DeviceDefault.NoActionBar">
+    </style>
+</resources>
diff --git a/java/framework/api/test-current.txt b/java/framework/api/test-current.txt
index 30dd7d9..7e8da26 100644
--- a/java/framework/api/test-current.txt
+++ b/java/framework/api/test-current.txt
@@ -11,14 +11,12 @@
     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);
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachine.java b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
index b6f811e..195c538 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachine.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachine.java
@@ -924,6 +924,11 @@
         }
         rawConfig.inputDevices = inputDevices.toArray(new InputDevice[0]);
 
+        // Handle network support
+        if (vmConfig.getCustomImageConfig() != null) {
+            rawConfig.networkSupported = vmConfig.getCustomImageConfig().useNetwork();
+        }
+
         return android.system.virtualizationservice.VirtualMachineConfig.rawConfig(rawConfig);
     }
 
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
index 8b444fc..e18aca2 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -22,7 +22,6 @@
 
 import static java.util.Objects.requireNonNull;
 
-import android.Manifest;
 import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.IntRange;
@@ -82,7 +81,7 @@
 
     // These define the schema of the config file persisted on disk.
     // Please bump up the version number when adding a new key.
-    private static final int VERSION = 9;
+    private static final int VERSION = 10;
     private static final String KEY_VERSION = "version";
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
@@ -101,8 +100,8 @@
     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";
     private static final String KEY_SHOULD_BOOST_UCLAMP = "shouldBoostUclamp";
+    private static final String KEY_SHOULD_USE_HUGEPAGES = "shouldUseHugepages";
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -210,11 +209,10 @@
     /** 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;
-
     private final boolean mShouldBoostUclamp;
 
+    private final boolean mShouldUseHugepages;
+
     @Retention(RetentionPolicy.SOURCE)
     @StringDef(
             prefix = "MICRODROID",
@@ -250,8 +248,8 @@
             boolean connectVmConsole,
             @Nullable File vendorDiskImage,
             @NonNull @OsName String os,
-            boolean networkSupported,
-            boolean shouldBoostUclamp) {
+            boolean shouldBoostUclamp,
+            boolean shouldUseHugepages) {
         // This is only called from Builder.build(); the builder handles parameter validation.
         mPackageName = packageName;
         mApkPath = apkPath;
@@ -274,8 +272,8 @@
         mConnectVmConsole = connectVmConsole;
         mVendorDiskImage = vendorDiskImage;
         mOs = os;
-        mNetworkSupported = networkSupported;
         mShouldBoostUclamp = shouldBoostUclamp;
+        mShouldUseHugepages = shouldUseHugepages;
     }
 
     /** Loads a config from a file. */
@@ -376,9 +374,9 @@
             }
         }
 
-        builder.setNetworkSupported(b.getBoolean(KEY_NETWORK_SUPPORTED));
-
         builder.setShouldBoostUclamp(b.getBoolean(KEY_SHOULD_BOOST_UCLAMP));
+        builder.setShouldUseHugepages(b.getBoolean(KEY_SHOULD_USE_HUGEPAGES));
+
         return builder.build();
     }
 
@@ -429,8 +427,8 @@
             String[] extraApks = mExtraApks.toArray(new String[0]);
             b.putStringArray(KEY_EXTRA_APKS, extraApks);
         }
-        b.putBoolean(KEY_NETWORK_SUPPORTED, mNetworkSupported);
         b.putBoolean(KEY_SHOULD_BOOST_UCLAMP, mShouldBoostUclamp);
+        b.putBoolean(KEY_SHOULD_USE_HUGEPAGES, mShouldUseHugepages);
         b.writeToStream(output);
     }
 
@@ -607,16 +605,6 @@
     }
 
     /**
-     * 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
@@ -720,7 +708,6 @@
         config.cpuTopology = (byte) this.mCpuTopology;
         config.consoleInputDevice = mConsoleInputDevice;
         config.devices = EMPTY_STRING_ARRAY;
-        config.networkSupported = this.mNetworkSupported;
         config.platformVersion = "~1.0";
         return config;
     }
@@ -773,26 +760,24 @@
                 break;
         }
 
-        if (mVendorDiskImage != null || mNetworkSupported) {
+        if (mVendorDiskImage != null) {
             VirtualMachineAppConfig.CustomConfig customConfig =
                     new VirtualMachineAppConfig.CustomConfig();
             customConfig.devices = EMPTY_STRING_ARRAY;
-            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);
-                }
+            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;
         }
 
         vsConfig.boostUclamp = mShouldBoostUclamp;
+        vsConfig.hugePages = mShouldUseHugepages;
+
         return vsConfig;
     }
 
@@ -872,8 +857,8 @@
         private boolean mConnectVmConsole = false;
         @Nullable private File mVendorDiskImage;
         @NonNull @OsName private String mOs = DEFAULT_OS;
-        private boolean mNetworkSupported;
         private boolean mShouldBoostUclamp = false;
+        private boolean mShouldUseHugepages = false;
 
         /**
          * Creates a builder for the given context.
@@ -950,10 +935,6 @@
                         "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,
@@ -972,8 +953,8 @@
                     mConnectVmConsole,
                     mVendorDiskImage,
                     mOs,
-                    mNetworkSupported,
-                    mShouldBoostUclamp);
+                    mShouldBoostUclamp,
+                    mShouldUseHugepages);
         }
 
         /**
@@ -1279,26 +1260,15 @@
             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;
+        /** @hide */
+        public Builder setShouldBoostUclamp(boolean shouldBoostUclamp) {
+            mShouldBoostUclamp = shouldBoostUclamp;
             return this;
         }
 
         /** @hide */
-        public Builder setShouldBoostUclamp(boolean shouldBoostUclamp) {
-            mShouldBoostUclamp = shouldBoostUclamp;
+        public Builder setShouldUseHugepages(boolean shouldUseHugepages) {
+            mShouldUseHugepages = shouldUseHugepages;
             return this;
         }
     }
diff --git a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
index c0ff11a..8d4886a 100644
--- a/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
+++ b/java/framework/src/android/system/virtualmachine/VirtualMachineCustomImageConfig.java
@@ -36,6 +36,7 @@
     private static final String KEY_TOUCH = "touch";
     private static final String KEY_KEYBOARD = "keyboard";
     private static final String KEY_MOUSE = "mouse";
+    private static final String KEY_NETWORK = "network";
     private static final String KEY_GPU = "gpu";
 
     @Nullable private final String name;
@@ -48,6 +49,7 @@
     private final boolean touch;
     private final boolean keyboard;
     private final boolean mouse;
+    private final boolean network;
     @Nullable private final GpuConfig gpuConfig;
 
     @Nullable
@@ -92,6 +94,10 @@
         return mouse;
     }
 
+    public boolean useNetwork() {
+        return network;
+    }
+
     /** @hide */
     public VirtualMachineCustomImageConfig(
             String name,
@@ -104,6 +110,7 @@
             boolean touch,
             boolean keyboard,
             boolean mouse,
+            boolean network,
             GpuConfig gpuConfig) {
         this.name = name;
         this.kernelPath = kernelPath;
@@ -115,6 +122,7 @@
         this.touch = touch;
         this.keyboard = keyboard;
         this.mouse = mouse;
+        this.network = network;
         this.gpuConfig = gpuConfig;
     }
 
@@ -146,6 +154,7 @@
         builder.useTouch(customImageConfigBundle.getBoolean(KEY_TOUCH));
         builder.useKeyboard(customImageConfigBundle.getBoolean(KEY_KEYBOARD));
         builder.useMouse(customImageConfigBundle.getBoolean(KEY_MOUSE));
+        builder.useNetwork(customImageConfigBundle.getBoolean(KEY_NETWORK));
         builder.setGpuConfig(GpuConfig.from(customImageConfigBundle.getPersistableBundle(KEY_GPU)));
         return builder.build();
     }
@@ -178,6 +187,7 @@
         pb.putBoolean(KEY_TOUCH, touch);
         pb.putBoolean(KEY_KEYBOARD, keyboard);
         pb.putBoolean(KEY_MOUSE, mouse);
+        pb.putBoolean(KEY_NETWORK, network);
         pb.putPersistableBundle(
                 KEY_GPU,
                 Optional.ofNullable(gpuConfig).map(gc -> gc.toPersistableBundle()).orElse(null));
@@ -237,6 +247,7 @@
         private boolean touch;
         private boolean keyboard;
         private boolean mouse;
+        private boolean network;
         private GpuConfig gpuConfig;
 
         /** @hide */
@@ -309,6 +320,12 @@
         }
 
         /** @hide */
+        public Builder useNetwork(boolean network) {
+            this.network = network;
+            return this;
+        }
+
+        /** @hide */
         public VirtualMachineCustomImageConfig build() {
             return new VirtualMachineCustomImageConfig(
                     this.name,
@@ -321,6 +338,7 @@
                     touch,
                     keyboard,
                     mouse,
+                    network,
                     gpuConfig);
         }
     }
diff --git a/java/service/Android.bp b/java/service/Android.bp
index 8bac7be..814445c 100644
--- a/java/service/Android.bp
+++ b/java/service/Android.bp
@@ -31,6 +31,7 @@
     ],
     static_libs: [
         "android.system.virtualizationmaintenance-java",
+        "android.system.vmtethering-java",
     ],
     sdk_version: "core_platform",
     apex_available: ["com.android.virt"],
diff --git a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
index 2461755..241eef4 100644
--- a/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
+++ b/java/service/src/com/android/system/virtualmachine/VirtualizationSystemService.java
@@ -21,16 +21,23 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.net.TetheringManager;
+import android.net.TetheringManager.StartTetheringCallback;
+import android.net.TetheringManager.TetheringRequest;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.ServiceManager;
 import android.os.UserHandle;
 import android.system.virtualizationmaintenance.IVirtualizationMaintenance;
+import android.system.vmtethering.IVmTethering;
 import android.util.Log;
 
 import com.android.internal.os.BackgroundThread;
 import com.android.server.SystemService;
 
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
 /**
  * This class exists to notify virtualization service of relevant things happening in the Android
  * framework.
@@ -40,17 +47,25 @@
  */
 public class VirtualizationSystemService extends SystemService {
     private static final String TAG = VirtualizationSystemService.class.getName();
-    private static final String SERVICE_NAME = "android.system.virtualizationmaintenance";
+    private static final String MAINTENANCE_SERVICE_NAME =
+            "android.system.virtualizationmaintenance";
     private Handler mHandler;
+    private final TetheringService mTetheringService;
 
     public VirtualizationSystemService(Context context) {
         super(context);
+        if (Files.exists(Paths.get("/apex/com.android.virt/bin/vmnic"))) {
+            mTetheringService = new TetheringService();
+        } else {
+            mTetheringService = null;
+        }
     }
 
     @Override
     public void onStart() {
-        // Nothing needed here - we don't expose any binder service. The binder service we use is
-        // exposed as a lazy service by the virtualizationservice native binary.
+        if (mTetheringService != null) {
+            publishBinderService(IVmTethering.DESCRIPTOR, mTetheringService);
+        }
     }
 
     @Override
@@ -82,11 +97,11 @@
     }
 
     static IVirtualizationMaintenance connectToMaintenanceService() {
-        IBinder binder = ServiceManager.waitForService(SERVICE_NAME);
+        IBinder binder = ServiceManager.waitForService(MAINTENANCE_SERVICE_NAME);
         IVirtualizationMaintenance maintenance =
                 IVirtualizationMaintenance.Stub.asInterface(binder);
         if (maintenance == null) {
-            throw new IllegalStateException("Failed to connect to " + SERVICE_NAME);
+            throw new IllegalStateException("Failed to connect to " + MAINTENANCE_SERVICE_NAME);
         }
         return maintenance;
     }
@@ -136,4 +151,38 @@
             }
         }
     }
+
+    private final class TetheringService extends IVmTethering.Stub {
+        private final TetheringManager tm = getContext().getSystemService(TetheringManager.class);
+
+        @Override
+        public void enableVmTethering() {
+            final TetheringRequest tr =
+                    new TetheringRequest.Builder(TetheringManager.TETHERING_VIRTUAL)
+                            .setConnectivityScope(TetheringManager.CONNECTIVITY_SCOPE_GLOBAL)
+                            .build();
+
+            StartTetheringCallback startTetheringCallback =
+                    new StartTetheringCallback() {
+                        @Override
+                        public void onTetheringStarted() {
+                            Log.i(TAG, "VM tethering started successfully");
+                        }
+
+                        @Override
+                        public void onTetheringFailed(int resultCode) {
+                            Log.e(
+                                    TAG,
+                                    "VM tethering failed. Result Code: "
+                                            + Integer.toString(resultCode));
+                        }
+                    };
+            tm.startTethering(tr, c -> c.run() /* executor */, startTetheringCallback);
+        }
+
+        @Override
+        public void disableVmTethering() {
+            tm.stopTethering(TetheringManager.TETHERING_VIRTUAL);
+        }
+    }
 }
diff --git a/service_vm/demo_apk/Android.bp b/service_vm/demo_apk/Android.bp
index 3750fe6..c64b70a 100644
--- a/service_vm/demo_apk/Android.bp
+++ b/service_vm/demo_apk/Android.bp
@@ -23,7 +23,7 @@
         "libandroid_logger",
         "libanyhow",
         "liblog_rust",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
 }
 
diff --git a/service_vm/demo_apk/src/main.rs b/service_vm/demo_apk/src/main.rs
index 8ea4e65..26df52c 100644
--- a/service_vm/demo_apk/src/main.rs
+++ b/service_vm/demo_apk/src/main.rs
@@ -14,25 +14,15 @@
 
 //! Main executable of Service VM client for manual testing.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::{ensure, Context, Result};
 use log::{error, info};
-use std::{
-    ffi::{c_void, CStr},
-    panic,
-    ptr::{self, NonNull},
-    result,
-};
-use vm_payload_bindgen::{
-    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
-    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
-    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
-    AVmPayload_requestAttestation,
-};
+use std::panic;
+use vm_payload::AttestationError;
+
+vm_payload::main!(main);
 
 /// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("service_vm_client")
@@ -52,15 +42,11 @@
     info!("Welcome to Service VM Client!");
 
     let too_big_challenge = &[0u8; 66];
-    let res = AttestationResult::request_attestation(too_big_challenge);
+    let res = vm_payload::request_attestation(too_big_challenge);
     ensure!(res.is_err());
-    let status = res.unwrap_err();
-    ensure!(
-        status == AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
-        "Unexpected status: {:?}",
-        status
-    );
-    info!("Status: {:?}", status_to_cstr(status));
+    let error = res.unwrap_err();
+    ensure!(error == AttestationError::InvalidChallenge, "Unexpected error: {error:?}");
+    info!("Error: {error}");
 
     // The data below is only a placeholder generated randomly with urandom
     let challenge = &[
@@ -68,162 +54,18 @@
         0x67, 0xc3, 0x3e, 0x73, 0x9b, 0x30, 0xbd, 0x04, 0x20, 0x2e, 0xde, 0x3b, 0x1d, 0xc8, 0x07,
         0x11, 0x7b,
     ];
-    let res = AttestationResult::request_attestation(challenge)
-        .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))?;
+    let res = vm_payload::request_attestation(challenge).context("Unexpected attestation error")?;
 
-    let cert_chain = res.certificate_chain()?;
+    let cert_chain: Vec<_> = res.certificate_chain().collect();
     info!("Attestation result certificateChain = {:?}", cert_chain);
 
-    let private_key = res.private_key()?;
+    let private_key = res.private_key();
     info!("Attestation result privateKey = {:?}", private_key);
 
     let message = b"Hello from Service VM client";
     info!("Signing message: {:?}", message);
-    let signature = res.sign(message)?;
+    let signature = res.sign_message(message);
     info!("Signature: {:?}", signature);
 
     Ok(())
 }
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-impl AttestationResult {
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestation(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn certificate_chain(&self) -> Result<Vec<Box<[u8]>>> {
-        let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::with_capacity(num_certs);
-        for i in 0..num_certs {
-            certs.push(get_certificate_at(self.as_ref(), i)?);
-        }
-        Ok(certs)
-    }
-
-    fn private_key(&self) -> Result<Box<[u8]>> {
-        get_private_key(self.as_ref())
-    }
-
-    fn sign(&self, message: &[u8]) -> Result<Box<[u8]>> {
-        sign_with_attested_key(self.as_ref(), message)
-    }
-}
-
-impl AsRef<AVmAttestationResult> for AttestationResult {
-    fn as_ref(&self) -> &AVmAttestationResult {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`.
-        unsafe { self.0.as_ref() }
-    }
-}
-
-impl Drop for AttestationResult {
-    fn drop(&mut self) {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`, and not freed elsewhere.
-        unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
-    }
-}
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
-    let mut cert = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `cert`.
-    // And `cert` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getCertificateAt(
-            res,
-            index,
-            cert.as_mut_ptr() as *mut c_void,
-            cert.len(),
-        )
-    };
-    ensure!(size == cert.len());
-    Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
-    let mut private_key = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `private_key`.
-    // And `private_key` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getPrivateKey(
-            res,
-            private_key.as_mut_ptr() as *mut c_void,
-            private_key.len(),
-        )
-    };
-    ensure!(size == private_key.len());
-    Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Box<[u8]>> {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            ptr::null_mut(),
-            0,
-        )
-    };
-    let mut signature = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `signature`.
-    // And `signature` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            signature.as_mut_ptr() as *mut c_void,
-            signature.len(),
-        )
-    };
-    ensure!(size == signature.len());
-    Ok(signature.into_boxed_slice())
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
-    // SAFETY: The function only reads the given enum status and returns a pointer to a
-    // static string.
-    let message = unsafe { AVmAttestationStatus_toString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
-    // point to a valid C String that lives forever.
-    unsafe { CStr::from_ptr(message) }
-}
diff --git a/service_vm/test_apk/Android.bp b/service_vm/test_apk/Android.bp
index 1ba156f..58b394a 100644
--- a/service_vm/test_apk/Android.bp
+++ b/service_vm/test_apk/Android.bp
@@ -39,7 +39,7 @@
         "libanyhow",
         "libavflog",
         "liblog_rust",
-        "libvm_payload_bindgen",
+        "libvm_payload_rs",
     ],
 }
 
diff --git a/service_vm/test_apk/src/native/main.rs b/service_vm/test_apk/src/native/main.rs
index 00ddff8..52635ad 100644
--- a/service_vm/test_apk/src/native/main.rs
+++ b/service_vm/test_apk/src/native/main.rs
@@ -14,35 +14,26 @@
 
 //! Main executable of VM attestation for end-to-end testing.
 
-use anyhow::{anyhow, ensure, Result};
+use anyhow::Result;
 use avflog::LogResult;
 use com_android_virt_vm_attestation_testservice::{
     aidl::com::android::virt::vm_attestation::testservice::IAttestationService::{
         AttestationStatus::AttestationStatus, BnAttestationService, IAttestationService,
         SigningResult::SigningResult, PORT,
     },
-    binder::{self, unstable_api::AsNative, BinderFeatures, Interface, IntoBinderResult, Strong},
+    binder::{self, BinderFeatures, Interface, IntoBinderResult, Strong},
 };
 use log::{error, info};
 use std::{
-    ffi::{c_void, CStr},
     panic,
-    ptr::{self, NonNull},
-    result,
     sync::{Arc, Mutex},
 };
-use vm_payload_bindgen::{
-    AIBinder, AVmAttestationResult, AVmAttestationResult_free,
-    AVmAttestationResult_getCertificateAt, AVmAttestationResult_getCertificateCount,
-    AVmAttestationResult_getPrivateKey, AVmAttestationResult_sign, AVmAttestationStatus,
-    AVmAttestationStatus_toString, AVmPayload_notifyPayloadReady, AVmPayload_requestAttestation,
-    AVmPayload_requestAttestationForTesting, AVmPayload_runVsockRpcServer,
-};
+use vm_payload::{AttestationError, AttestationResult};
 
-/// Entry point of the Service VM client.
-#[allow(non_snake_case)]
-#[no_mangle]
-pub extern "C" fn AVmPayload_main() {
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
     android_logger::init_once(
         android_logger::Config::default()
             .with_tag("service_vm_client")
@@ -61,18 +52,7 @@
 fn try_main() -> Result<()> {
     info!("Welcome to Service VM Client!");
 
-    let mut service = AttestationService::new_binder().as_binder();
-    let service = service.as_native_mut() as *mut AIBinder;
-    let param = ptr::null_mut();
-    // SAFETY: We hold a strong pointer, so the raw pointer remains valid. The bindgen AIBinder
-    // is the same type as `sys::AIBinder`. It is safe for `on_ready` to be invoked at any time,
-    // with any parameter.
-    unsafe { AVmPayload_runVsockRpcServer(service, PORT.try_into()?, Some(on_ready), param) };
-}
-
-extern "C" fn on_ready(_param: *mut c_void) {
-    // SAFETY: It is safe to call `AVmPayload_notifyPayloadReady` at any time.
-    unsafe { AVmPayload_notifyPayloadReady() };
+    vm_payload::run_single_vsock_service(AttestationService::new_binder(), PORT.try_into()?)
 }
 
 struct AttestationService {
@@ -88,11 +68,11 @@
     }
 }
 
+#[allow(non_snake_case)]
 impl IAttestationService for AttestationService {
     fn requestAttestationForTesting(&self) -> binder::Result<()> {
         const CHALLENGE: &[u8] = &[0xaa; 32];
-        let res = AttestationResult::request_attestation_for_testing(CHALLENGE)
-            .map_err(|e| anyhow!("Unexpected status: {:?}", status_to_cstr(e)))
+        let res = vm_payload::restricted::request_attestation_for_testing(CHALLENGE)
             .with_log()
             .or_service_specific_exception(-1)?;
         *self.res.lock().unwrap() = Some(res);
@@ -104,218 +84,46 @@
         challenge: &[u8],
         message: &[u8],
     ) -> binder::Result<SigningResult> {
-        let res = match AttestationResult::request_attestation(challenge) {
+        let res: AttestationResult = match vm_payload::request_attestation(challenge) {
             Ok(res) => res,
-            Err(status) => {
-                let status = to_attestation_status(status);
+            Err(e) => {
+                let status = to_attestation_status(e);
                 return Ok(SigningResult { certificateChain: vec![], signature: vec![], status });
             }
         };
-        let certificate_chain =
-            res.certificate_chain().with_log().or_service_specific_exception(-1)?;
+
+        let certificate_chain: Vec<u8> = res.certificate_chain().flatten().collect();
         let status = AttestationStatus::OK;
-        let signature = res.sign(message).with_log().or_service_specific_exception(-1)?;
+        let signature = res.sign_message(message);
+
         Ok(SigningResult { certificateChain: certificate_chain, signature, status })
     }
 
     fn validateAttestationResult(&self) -> binder::Result<()> {
         // TODO(b/191073073): Returns the attestation result to the host for validation.
-        self.res.lock().unwrap().as_ref().unwrap().log().or_service_specific_exception(-1)
-    }
-}
-
-fn to_attestation_status(status: AVmAttestationStatus) -> AttestationStatus {
-    match status {
-        AVmAttestationStatus::ATTESTATION_OK => AttestationStatus::OK,
-        AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
-            AttestationStatus::ERROR_INVALID_CHALLENGE
-        }
-        AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
-            AttestationStatus::ERROR_ATTESTATION_FAILED
-        }
-        AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => AttestationStatus::ERROR_UNSUPPORTED,
-    }
-}
-
-#[derive(Debug)]
-struct AttestationResult(NonNull<AVmAttestationResult>);
-
-// Safety: `AttestationResult` is not `Send` because it contains a raw pointer to a C struct.
-unsafe impl Send for AttestationResult {}
-
-impl AttestationResult {
-    fn request_attestation_for_testing(
-        challenge: &[u8],
-    ) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestationForTesting(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn request_attestation(challenge: &[u8]) -> result::Result<Self, AVmAttestationStatus> {
-        let mut res: *mut AVmAttestationResult = ptr::null_mut();
-        // SAFETY: It is safe as we only read the challenge within its bounds and the
-        // function does not retain any reference to it.
-        let status = unsafe {
-            AVmPayload_requestAttestation(
-                challenge.as_ptr() as *const c_void,
-                challenge.len(),
-                &mut res,
-            )
-        };
-        if status == AVmAttestationStatus::ATTESTATION_OK {
-            info!("Attestation succeeds. Status: {:?}", status_to_cstr(status));
-            let res = NonNull::new(res).expect("The attestation result is null");
-            Ok(Self(res))
-        } else {
-            Err(status)
-        }
-    }
-
-    fn certificate_chain(&self) -> Result<Vec<u8>> {
-        let num_certs = get_certificate_count(self.as_ref());
-        let mut certs = Vec::new();
-        for i in 0..num_certs {
-            certs.extend(get_certificate_at(self.as_ref(), i)?.iter());
-        }
-        Ok(certs)
-    }
-
-    fn private_key(&self) -> Result<Box<[u8]>> {
-        get_private_key(self.as_ref())
-    }
-
-    fn sign(&self, message: &[u8]) -> Result<Vec<u8>> {
-        sign_with_attested_key(self.as_ref(), message)
-    }
-
-    fn log(&self) -> Result<()> {
-        let cert_chain = self.certificate_chain()?;
-        info!("Attestation result certificateChain = {:?}", cert_chain);
-
-        let private_key = self.private_key()?;
-        info!("Attestation result privateKey = {:?}", private_key);
-
-        let message = b"Hello from Service VM client";
-        info!("Signing message: {:?}", message);
-        let signature = self.sign(message)?;
-        info!("Signature: {:?}", signature);
+        log(self.res.lock().unwrap().as_ref().unwrap());
         Ok(())
     }
 }
 
-impl AsRef<AVmAttestationResult> for AttestationResult {
-    fn as_ref(&self) -> &AVmAttestationResult {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`.
-        unsafe { self.0.as_ref() }
+fn log(res: &AttestationResult) {
+    for (i, cert) in res.certificate_chain().enumerate() {
+        info!("Attestation result certificate {i} = {cert:?}");
     }
+
+    let private_key = res.private_key();
+    info!("Attestation result privateKey = {private_key:?}");
+
+    let message = b"Hello from Service VM client";
+    info!("Signing message: {message:?}");
+    let signature = res.sign_message(message);
+    info!("Signature: {signature:?}");
 }
 
-impl Drop for AttestationResult {
-    fn drop(&mut self) {
-        // SAFETY: This field is private, and only populated with a successful call to
-        // `AVmPayload_requestAttestation`, and not freed elsewhere.
-        unsafe { AVmAttestationResult_free(self.0.as_ptr()) };
+fn to_attestation_status(e: AttestationError) -> AttestationStatus {
+    match e {
+        AttestationError::InvalidChallenge => AttestationStatus::ERROR_INVALID_CHALLENGE,
+        AttestationError::AttestationFailed => AttestationStatus::ERROR_ATTESTATION_FAILED,
+        AttestationError::AttestationUnsupported => AttestationStatus::ERROR_UNSUPPORTED,
     }
 }
-
-fn get_certificate_count(res: &AVmAttestationResult) -> usize {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    unsafe { AVmAttestationResult_getCertificateCount(res) }
-}
-
-fn get_certificate_at(res: &AVmAttestationResult, index: usize) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getCertificateAt(res, index, ptr::null_mut(), 0) };
-    let mut cert = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `cert`.
-    // And `cert` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getCertificateAt(
-            res,
-            index,
-            cert.as_mut_ptr() as *mut c_void,
-            cert.len(),
-        )
-    };
-    ensure!(size == cert.len());
-    Ok(cert.into_boxed_slice())
-}
-
-fn get_private_key(res: &AVmAttestationResult) -> Result<Box<[u8]>> {
-    let size =
-        // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-        // before getting freed.
-        unsafe { AVmAttestationResult_getPrivateKey(res, ptr::null_mut(), 0) };
-    let mut private_key = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `private_key`.
-    // And `private_key` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_getPrivateKey(
-            res,
-            private_key.as_mut_ptr() as *mut c_void,
-            private_key.len(),
-        )
-    };
-    ensure!(size == private_key.len());
-    Ok(private_key.into_boxed_slice())
-}
-
-fn sign_with_attested_key(res: &AVmAttestationResult, message: &[u8]) -> Result<Vec<u8>> {
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            ptr::null_mut(),
-            0,
-        )
-    };
-    let mut signature = vec![0u8; size];
-    // SAFETY: The result is returned by `AVmPayload_requestAttestation` and should be valid
-    // before getting freed. This function only writes within the bounds of `signature`.
-    // And `signature` cannot overlap `res` because we just allocated it.
-    let size = unsafe {
-        AVmAttestationResult_sign(
-            res,
-            message.as_ptr() as *const c_void,
-            message.len(),
-            signature.as_mut_ptr() as *mut c_void,
-            signature.len(),
-        )
-    };
-    ensure!(size <= signature.len());
-    signature.truncate(size);
-    Ok(signature)
-}
-
-fn status_to_cstr(status: AVmAttestationStatus) -> &'static CStr {
-    // SAFETY: The function only reads the given enum status and returns a pointer to a
-    // static string.
-    let message = unsafe { AVmAttestationStatus_toString(status) };
-    // SAFETY: The pointer returned by `AVmAttestationStatus_toString` is guaranteed to
-    // point to a valid C String that lives forever.
-    unsafe { CStr::from_ptr(message) }
-}
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index 639de06..c33d3f5 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,8 +16,8 @@
 
 package com.android.microdroid.benchmark;
 
-import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
 import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
 
@@ -34,10 +34,10 @@
 import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
 import android.os.Process;
 import android.os.RemoteException;
+import android.system.Os;
 import android.system.virtualmachine.VirtualMachine;
 import android.system.virtualmachine.VirtualMachineConfig;
 import android.system.virtualmachine.VirtualMachineException;
-import android.system.Os;
 import android.system.virtualmachine.VirtualMachineManager;
 import android.util.Log;
 
@@ -158,6 +158,7 @@
                 newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .setMemoryBytes(mem * ONE_MEBI)
+                        .setShouldUseHugepages(true)
                         .build();
 
         // returns true if succeeded at least once.
@@ -234,6 +235,7 @@
             VirtualMachineConfig.Builder builder =
                     newVmConfigBuilderWithPayloadBinary("MicrodroidIdleNativeLib.so")
                             .setShouldBoostUclamp(true)
+                            .setShouldUseHugepages(true)
                             .setMemoryBytes(256 * ONE_MEBI)
                             .setDebugLevel(DEBUG_LEVEL_NONE);
             if (fullDebug) {
@@ -347,6 +349,7 @@
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
                         .setShouldBoostUclamp(true)
+                        .setShouldUseHugepages(true)
                         .build();
         List<Double> transferRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -373,6 +376,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .build();
         List<Double> readRates = new ArrayList<>(IO_TEST_TRIAL_COUNT);
 
@@ -524,6 +528,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .setMemoryBytes(256 * ONE_MEBI)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
@@ -610,6 +615,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .setMemoryBytes(256 * ONE_MEBI)
                         .build();
         VirtualMachine vm = forceCreateNewVirtualMachine(vmName, config);
@@ -729,6 +735,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .setShouldBoostUclamp(true)
                         .build();
 
@@ -778,6 +785,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadBinary("MicrodroidTestNativeLib.so")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .build();
 
         List<Double> requestLatencies = new ArrayList<>(IO_TEST_TRIAL_COUNT * NUM_REQUESTS);
@@ -836,6 +844,7 @@
         VirtualMachineConfig config =
                 newVmConfigBuilderWithPayloadConfig("assets/vm_config_io.json")
                         .setDebugLevel(DEBUG_LEVEL_NONE)
+                        .setShouldUseHugepages(true)
                         .build();
         List<Double> vmKillTime = new ArrayList<>(TEST_TRIAL_COUNT);
 
diff --git a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
index 6474965..026cf3f 100644
--- a/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
+++ b/tests/helper/src/java/com/android/microdroid/test/device/MicrodroidDeviceTestBase.java
@@ -191,6 +191,9 @@
             assume().withMessage("Skip where protected VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_PROTECTED_VM)
                     .isNotEqualTo(0);
+            assume().withMessage("Testing protected VMs on GSI isn't supported. b/272443823")
+                    .that(isGsi())
+                    .isFalse();
         } else {
             assume().withMessage("Skip where VMs aren't supported")
                     .that(capabilities & VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM)
@@ -212,13 +215,17 @@
                 .that(mCtx.getPackageManager().hasSystemFeature(FEATURE_VIRTUALIZATION_FRAMEWORK))
                 .isTrue();
         int vendorApiLevel = getVendorApiLevel();
-        boolean isGsi = new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+        boolean isGsi = isGsi();
         Log.i(TAG, "isGsi = " + isGsi + ", vendor api level = " + vendorApiLevel);
         assume().withMessage("GSI with vendor API level < 202404 may not support AVF")
                 .that(isGsi && vendorApiLevel < 202404)
                 .isFalse();
     }
 
+    protected boolean isGsi() {
+        return new File("/system/system_ext/etc/init/init.gsi.rc").exists();
+    }
+
     protected static int getVendorApiLevel() {
         return SystemProperties.getInt("ro.board.api_level", 0);
     }
@@ -549,6 +556,7 @@
         public int mFileMode;
         public int mMountFlags;
         public String mConsoleInput;
+        public byte[] mInstanceSecret;
 
         public void assertNoException() {
             if (mException != null) {
diff --git a/tests/testapk/Android.bp b/tests/testapk/Android.bp
index 471aea7..e32ff88 100644
--- a/tests/testapk/Android.bp
+++ b/tests/testapk/Android.bp
@@ -55,6 +55,7 @@
         "MicrodroidExitNativeLib",
         "MicrodroidPrivateLinkingNativeLib",
         "MicrodroidCrashNativeLib",
+        "libmicrodroid_testlib_rust",
         "libvm_attestation_test_payload",
     ],
     min_sdk_version: "33",
@@ -166,3 +167,22 @@
     header_libs: ["vm_payload_headers"],
     stl: "libc++_static",
 }
+
+// A payload written in Rust, using the Rust wrapper for the VM payload API.
+rust_ffi_shared {
+    name: "libmicrodroid_testlib_rust",
+    crate_name: "microdroid_testlib_rust",
+    defaults: ["avf_build_flags_rust"],
+    prefer_rlib: true,
+    srcs: ["src/native/testbinary.rs"],
+    compile_multilib: "both",
+    rustlibs: [
+        "com.android.microdroid.testservice-rust",
+        "libandroid_logger",
+        "libanyhow",
+        "libavflog",
+        "libcstr",
+        "liblog_rust",
+        "libvm_payload_rs",
+    ],
+}
diff --git a/tests/testapk/AndroidManifestV5.xml b/tests/testapk/AndroidManifestV5.xml
index b869586..7d97680 100644
--- a/tests/testapk/AndroidManifestV5.xml
+++ b/tests/testapk/AndroidManifestV5.xml
@@ -18,7 +18,6 @@
       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 c55da85..19d5674 100644
--- a/tests/testapk/AndroidManifestV6.xml
+++ b/tests/testapk/AndroidManifestV6.xml
@@ -18,7 +18,6 @@
       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 4141903..55badcc 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -197,6 +197,7 @@
                             tr.mSublibRunProp = ts.readProperty("debug.microdroid.app.sublib.run");
                             tr.mApkContentsPath = ts.getApkContentsPath();
                             tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
                         });
         testResults.assertNoException();
         assertThat(testResults.mAddInteger).isEqualTo(123 + 456);
@@ -204,6 +205,7 @@
         assertThat(testResults.mSublibRunProp).isEqualTo("true");
         assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
         assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
     }
 
     @Test
@@ -574,7 +576,6 @@
         assertThat(minimal.getEncryptedStorageBytes()).isEqualTo(0);
         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.)
@@ -591,9 +592,6 @@
                         .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");
@@ -610,9 +608,6 @@
         assertThat(maximal.getEncryptedStorageBytes()).isEqualTo(1_000_000);
         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();
@@ -667,18 +662,6 @@
                         .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
@@ -2320,47 +2303,61 @@
         }
     }
 
-    private VirtualMachineConfig buildVmConfigWithNetworkSupported() throws Exception {
-        return buildVmConfigWithNetworkSupported("MicrodroidTestNativeLib.so");
-    }
+    @Test
+    public void createAndRunRustVm() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
 
-    private VirtualMachineConfig buildVmConfigWithNetworkSupported(String binaryPath)
-            throws Exception {
         assumeSupportedDevice();
-        assumeNonProtectedVM();
-        assumeFeatureEnabled(VirtualMachineManager.FEATURE_NETWORK);
+
         VirtualMachineConfig config =
-                newVmConfigBuilderWithPayloadBinary(binaryPath)
-                        .setNetworkSupported(true)
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .build();
-        grantPermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
-        return config;
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
+
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> {
+                            tr.mAddInteger = ts.addInteger(37, 73);
+                            tr.mApkContentsPath = ts.getApkContentsPath();
+                            tr.mEncryptedStoragePath = ts.getEncryptedStoragePath();
+                            tr.mInstanceSecret = ts.insecurelyExposeVmInstanceSecret();
+                        });
+        testResults.assertNoException();
+        assertThat(testResults.mAddInteger).isEqualTo(37 + 73);
+        assertThat(testResults.mApkContentsPath).isEqualTo("/mnt/apk");
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("");
+        assertThat(testResults.mInstanceSecret).hasLength(32);
     }
 
     @Test
-    public void configuringNetworkSupportedRequiresCustomPermission() throws Exception {
-        VirtualMachineConfig config = buildVmConfigWithNetworkSupported();
-        revokePermission(VirtualMachine.USE_CUSTOM_VIRTUAL_MACHINE_PERMISSION);
+    public void createAndRunRustVmWithEncryptedStorage() throws Exception {
+        // This test is here mostly to exercise the Rust wrapper around the VM Payload API.
+        // We're testing the same functionality as in other tests, the only difference is
+        // that the payload is written in Rust.
 
-        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");
-    }
+        assumeSupportedDevice();
 
-    @Test
-    public void bootsWithNetworkSupported() throws Exception {
-        VirtualMachineConfig config = buildVmConfigWithNetworkSupported();
+        VirtualMachineConfig config =
+                newVmConfigBuilderWithPayloadBinary("libmicrodroid_testlib_rust.so")
+                        .setMemoryBytes(minMemoryRequired())
+                        .setDebugLevel(DEBUG_LEVEL_FULL)
+                        .setEncryptedStorageBytes(ENCRYPTED_STORAGE_BYTES)
+                        .build();
+        VirtualMachine vm = forceCreateNewVirtualMachine("rust_vm", config);
 
-        VirtualMachine vm =
-                forceCreateNewVirtualMachine("test_boot_with_network_supported", config);
-        runVmTestService(TAG, vm, (ts, tr) -> {}).assertNoException();
+        TestResults testResults =
+                runVmTestService(
+                        TAG,
+                        vm,
+                        (ts, tr) -> tr.mEncryptedStoragePath = ts.getEncryptedStoragePath());
+        testResults.assertNoException();
+        assertThat(testResults.mEncryptedStoragePath).isEqualTo("/mnt/encryptedstore");
     }
 
     private VirtualMachineConfig buildVmConfigWithVendor(File vendorDiskImage) throws Exception {
diff --git a/tests/testapk/src/native/testbinary.rs b/tests/testapk/src/native/testbinary.rs
new file mode 100644
index 0000000..85b411e
--- /dev/null
+++ b/tests/testapk/src/native/testbinary.rs
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+//! A VM payload that exists to allow testing of the Rust wrapper for the VM payload APIs.
+
+use anyhow::Result;
+use com_android_microdroid_testservice::{
+    aidl::com::android::microdroid::testservice::{
+        IAppCallback::IAppCallback,
+        ITestService::{BnTestService, ITestService, PORT},
+    },
+    binder::{BinderFeatures, ExceptionCode, Interface, Result as BinderResult, Status, Strong},
+};
+use cstr::cstr;
+use log::{error, info};
+use std::panic;
+use std::process::exit;
+use std::string::String;
+use std::vec::Vec;
+
+vm_payload::main!(main);
+
+// Entry point of the Service VM client.
+fn main() {
+    android_logger::init_once(
+        android_logger::Config::default()
+            .with_tag("microdroid_testlib_rust")
+            .with_max_level(log::LevelFilter::Debug),
+    );
+    // Redirect panic messages to logcat.
+    panic::set_hook(Box::new(|panic_info| {
+        error!("{panic_info}");
+    }));
+    if let Err(e) = try_main() {
+        error!("failed with {:?}", e);
+        exit(1);
+    }
+}
+
+fn try_main() -> Result<()> {
+    info!("Welcome to the Rust test binary");
+
+    vm_payload::run_single_vsock_service(TestService::new_binder(), PORT.try_into()?)
+}
+
+struct TestService {}
+
+impl Interface for TestService {}
+
+impl TestService {
+    fn new_binder() -> Strong<dyn ITestService> {
+        BnTestService::new_binder(TestService {}, BinderFeatures::default())
+    }
+}
+
+impl ITestService for TestService {
+    fn quit(&self) -> BinderResult<()> {
+        exit(0)
+    }
+
+    fn addInteger(&self, a: i32, b: i32) -> BinderResult<i32> {
+        a.checked_add(b).ok_or_else(|| Status::new_exception(ExceptionCode::ILLEGAL_ARGUMENT, None))
+    }
+
+    fn getApkContentsPath(&self) -> BinderResult<String> {
+        Ok(vm_payload::apk_contents_path().to_string_lossy().to_string())
+    }
+
+    fn getEncryptedStoragePath(&self) -> BinderResult<String> {
+        Ok(vm_payload::encrypted_storage_path()
+            .map(|p| p.to_string_lossy().to_string())
+            .unwrap_or("".to_string()))
+    }
+
+    fn insecurelyExposeVmInstanceSecret(&self) -> BinderResult<Vec<u8>> {
+        let mut secret = vec![0u8; 32];
+        vm_payload::get_vm_instance_secret(b"identifier", secret.as_mut_slice());
+        Ok(secret)
+    }
+
+    // Everything below here is unimplemented. Implementations may be added as needed.
+
+    fn readProperty(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn insecurelyExposeAttestationCdi(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn getBcc(&self) -> BinderResult<Vec<u8>> {
+        unimplemented()
+    }
+    fn runEchoReverseServer(&self) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn getEffectiveCapabilities(&self) -> BinderResult<Vec<String>> {
+        unimplemented()
+    }
+    fn getUid(&self) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn writeToFile(&self, _: &str, _: &str) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readFromFile(&self, _: &str) -> BinderResult<String> {
+        unimplemented()
+    }
+    fn getFilePermissions(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn getMountFlags(&self, _: &str) -> BinderResult<i32> {
+        unimplemented()
+    }
+    fn requestCallback(&self, _: &Strong<dyn IAppCallback + 'static>) -> BinderResult<()> {
+        unimplemented()
+    }
+    fn readLineFromConsole(&self) -> BinderResult<String> {
+        unimplemented()
+    }
+}
+
+fn unimplemented<T>() -> BinderResult<T> {
+    let message = cstr!("Got a call to an unimplemented ITestService method in testbinary.rs");
+    error!("{message:?}");
+    Err(Status::new_exception(ExceptionCode::UNSUPPORTED_OPERATION, Some(message)))
+}
diff --git a/tests/testapk_no_internet_perm/Android.bp b/tests/testapk_no_internet_perm/Android.bp
deleted file mode 100644
index d23081f..0000000
--- a/tests/testapk_no_internet_perm/Android.bp
+++ /dev/null
@@ -1,26 +0,0 @@
-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
deleted file mode 100644
index 87b302a..0000000
--- a/tests/testapk_no_internet_perm/AndroidManifest.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-<?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
deleted file mode 100644
index 61f8b8c..0000000
--- a/tests/testapk_no_internet_perm/AndroidTest.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?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
deleted file mode 100644
index 767f745..0000000
--- a/tests/testapk_no_internet_perm/src/java/com/android/microdroid/test/MicrodroidTestAppNoInternetPerm.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index ee5f5cd..47ef91a 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -1087,15 +1087,6 @@
         }
     }
 
-    if cfg!(paravirtualized_devices) {
-        // TODO(b/340376951): Remove this after tap in CrosvmConfig is connected to tethering.
-        if rustutils::system_properties::read_bool("ro.crosvm.network.setup.done", false)
-            .unwrap_or(false)
-        {
-            command.arg("--net").arg("tap-name=crosvm_tap");
-        }
-    }
-
     if cfg!(network) {
         if let Some(tap) = &config.tap {
             let tap_fd = tap.as_raw_fd();
diff --git a/virtualizationservice/Android.bp b/virtualizationservice/Android.bp
index 0c39501..f9034af 100644
--- a/virtualizationservice/Android.bp
+++ b/virtualizationservice/Android.bp
@@ -30,6 +30,7 @@
         "android.system.virtualizationservice-rust",
         "android.system.virtualizationservice_internal-rust",
         "android.system.virtualmachineservice-rust",
+        "android.system.vmtethering-rust",
         "android.os.permissions_aidl-rust",
         "libandroid_logger",
         "libanyhow",
diff --git a/virtualizationservice/aidl/Android.bp b/virtualizationservice/aidl/Android.bp
index fb89772..bca4512 100644
--- a/virtualizationservice/aidl/Android.bp
+++ b/virtualizationservice/aidl/Android.bp
@@ -86,6 +86,26 @@
 }
 
 aidl_interface {
+    name: "android.system.vmtethering",
+    srcs: ["android/system/vmtethering/**/*.aidl"],
+    unstable: true,
+    backend: {
+        java: {
+            sdk_version: "module_current",
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+        rust: {
+            enabled: true,
+            apex_available: [
+                "com.android.virt",
+            ],
+        },
+    },
+}
+
+aidl_interface {
     name: "android.system.virtualmachineservice",
     srcs: ["android/system/virtualmachineservice/**/*.aidl"],
     imports: [
diff --git a/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl b/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
new file mode 100644
index 0000000..0743ffa
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/vmtethering/IVmTethering.aidl
@@ -0,0 +1,29 @@
+/*
+ * 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 android.system.vmtethering;
+
+interface IVmTethering {
+    /**
+     * Start VM tethering to provide external network to VM.
+     */
+    void enableVmTethering();
+
+    /**
+     * Terminate VM tethering that providing external network to VM.
+     */
+    void disableVmTethering();
+}
diff --git a/virtualizationservice/src/aidl.rs b/virtualizationservice/src/aidl.rs
index 70da37b..acdb53a 100644
--- a/virtualizationservice/src/aidl.rs
+++ b/virtualizationservice/src/aidl.rs
@@ -25,6 +25,7 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use android_system_virtualizationservice_internal as android_vs_internal;
 use android_system_virtualmachineservice::aidl::android::system::virtualmachineservice;
+use android_system_vmtethering::aidl::android::system::vmtethering;
 use android_vs_internal::aidl::android::system::virtualizationservice_internal;
 use anyhow::{anyhow, ensure, Context, Result};
 use avflog::LogResult;
@@ -33,7 +34,7 @@
     LazyServiceGuard, ParcelFileDescriptor, Status, Strong,
 };
 use lazy_static::lazy_static;
-use libc::VMADDR_CID_HOST;
+use libc::{VMADDR_CID_HOST, VMADDR_CID_HYPERVISOR, VMADDR_CID_LOCAL};
 use log::{error, info, warn};
 use nix::unistd::{chown, Uid};
 use openssl::x509::X509;
@@ -73,6 +74,7 @@
     IVmnic::{BpVmnic, IVmnic},
 };
 use virtualmachineservice::IVirtualMachineService::VM_TOMBSTONES_SERVICE_PORT;
+use vmtethering::IVmTethering::{BpVmTethering, IVmTethering};
 use vsock::{VsockListener, VsockStream};
 
 /// The unique ID of a VM used (together with a port number) for vsock communication.
@@ -163,6 +165,9 @@
     static ref NETWORK_SERVICE: Strong<dyn IVmnic> =
         wait_for_interface(<BpVmnic as IVmnic>::get_descriptor())
             .expect("Could not connect to Vmnic");
+    static ref TETHERING_SERVICE: Strong<dyn IVmTethering> =
+        wait_for_interface(<BpVmTethering as IVmTethering>::get_descriptor())
+            .expect("Could not connect to VmTethering");
 }
 
 fn is_valid_guest_cid(cid: Cid) -> bool {
@@ -513,7 +518,7 @@
         Ok(())
     }
 
-    fn createTapInterface(&self, iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
+    fn createTapInterface(&self, _iface_name_suffix: &str) -> binder::Result<ParcelFileDescriptor> {
         check_internet_permission()?;
         check_use_custom_virtual_machine()?;
         if !cfg!(network) {
@@ -523,7 +528,16 @@
             ))
             .with_log();
         }
-        NETWORK_SERVICE.createTapInterface(iface_name_suffix)
+        // TODO(340377643): Use iface_name_suffix after introducing bridge interface, not fixed
+        // value.
+        let tap_fd = NETWORK_SERVICE.createTapInterface("fixed")?;
+
+        // TODO(340377643): Due to lack of implementation of creating bridge interface, tethering is
+        // enabled for TAP interface instead of bridge interface. After introducing creation of
+        // bridge interface in AVF, we should modify it.
+        TETHERING_SERVICE.enableVmTethering()?;
+
+        Ok(tap_fd)
     }
 
     fn deleteTapInterface(&self, tap_fd: &ParcelFileDescriptor) -> binder::Result<()> {
@@ -536,6 +550,10 @@
             ))
             .with_log();
         }
+
+        // TODO(340377643): Disabling tethering should be for bridge interface, not TAP interface.
+        TETHERING_SERVICE.disableVmTethering()?;
+
         NETWORK_SERVICE.deleteTapInterface(tap_fd)
     }
 }
@@ -860,11 +878,21 @@
     for incoming_stream in listener.incoming() {
         let mut incoming_stream = match incoming_stream {
             Err(e) => {
-                warn!("invalid incoming connection: {:?}", e);
+                warn!("invalid incoming connection: {e:?}");
                 continue;
             }
             Ok(s) => s,
         };
+        if let Ok(addr) = incoming_stream.peer_addr() {
+            let cid = addr.cid();
+            match cid {
+                VMADDR_CID_LOCAL | VMADDR_CID_HOST | VMADDR_CID_HYPERVISOR => {
+                    warn!("Rejecting non-guest tombstone vsock connection from cid={cid}");
+                    continue;
+                }
+                _ => info!("Vsock Stream connected to cid={cid} for tombstones"),
+            }
+        }
         std::thread::spawn(move || {
             if let Err(e) = handle_tombstone(&mut incoming_stream) {
                 error!("Failed to write tombstone- {:?}", e);
@@ -875,9 +903,6 @@
 }
 
 fn handle_tombstone(stream: &mut VsockStream) -> Result<()> {
-    if let Ok(addr) = stream.peer_addr() {
-        info!("Vsock Stream connected to cid={} for tombstones", addr.cid());
-    }
     let tb_connection =
         TombstonedConnection::connect(std::process::id() as i32, DebuggerdDumpType::Tombstone)
             .context("Failed to connect to tombstoned")?;
diff --git a/vm_payload/Android.bp b/vm_payload/Android.bp
index 229f533..cf2a002 100644
--- a/vm_payload/Android.bp
+++ b/vm_payload/Android.bp
@@ -39,8 +39,8 @@
     visibility: [":__subpackages__"],
 }
 
-// Rust wrappers round the C API for Rust clients.
-// (Yes, this involves going Rust -> C -> Rust.)
+// Access to the C API for Rust code.
+// This shouldn't be used directly - prefer libvm_payload_rs (below)
 rust_bindgen {
     name: "libvm_payload_bindgen",
     wrapper_src: "include-restricted/vm_payload_restricted.h",
@@ -51,15 +51,31 @@
     bindgen_flags: [
         "--default-enum-style rust",
     ],
-    visibility: [
-        "//packages/modules/Virtualization/compos",
-        "//packages/modules/Virtualization/service_vm:__subpackages__",
-    ],
     shared_libs: [
         "libvm_payload#current",
     ],
 }
 
+// Wrapper library for the raw C API for use by Rust clients.
+// (Yes, this involves going Rust -> C -> Rust.)
+// This is not a stable API - we may change it in subsequent versions.
+// But it is made available as an rlib so it is linked into any
+// code using it, leaving only dependencies on stable APIs.
+// So code built with it should run unchanged on future versions.
+rust_library_rlib {
+    name: "libvm_payload_rs",
+    crate_name: "vm_payload",
+    defaults: ["avf_build_flags_rust"],
+    srcs: ["wrapper/lib.rs"],
+    rustlibs: [
+        "libbinder_rs",
+        "libstatic_assertions",
+        "libvm_payload_bindgen",
+    ],
+    apex_available: ["com.android.compos"],
+    visibility: ["//visibility:public"],
+}
+
 // Shared library for clients to link against.
 cc_library_shared {
     name: "libvm_payload",
diff --git a/vm_payload/README.md b/vm_payload/README.md
index 4b1e6f3..66fd532 100644
--- a/vm_payload/README.md
+++ b/vm_payload/README.md
@@ -70,3 +70,16 @@
 See [AIDL
 backends](https://source.android.com/docs/core/architecture/aidl/aidl-backends)
 for information on using AIDL with the NDK Binder from C++.
+
+## Rust
+
+A Rust wrapper library for the VM Payload API is available (as an rlib) for VM
+payloads written in Rust.
+
+This wrapper is not guaranteed to be stable; we may change it in future
+versions. But payload code built using it will depend only on the C VM Payload
+API and the NDK APIs that are available to the payload, so should run unchanged
+on future versions.
+
+See [wrapper/lib.rs](wrapper/lib.rs) and `libvm_payload_rs` in
+[Android.bp](Android.bp).
diff --git a/vm_payload/wrapper/attestation.rs b/vm_payload/wrapper/attestation.rs
new file mode 100644
index 0000000..e0055d5
--- /dev/null
+++ b/vm_payload/wrapper/attestation.rs
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ */
+
+use std::error::Error;
+use std::ffi::{c_void, CStr};
+use std::fmt::{self, Display};
+use std::iter::FusedIterator;
+use std::ptr::{self, NonNull};
+
+use vm_payload_bindgen::{
+    AVmAttestationResult, AVmAttestationResult_free, AVmAttestationResult_getCertificateAt,
+    AVmAttestationResult_getCertificateCount, AVmAttestationResult_getPrivateKey,
+    AVmAttestationResult_sign, AVmAttestationStatus, AVmAttestationStatus_toString,
+    AVmPayload_requestAttestation, AVmPayload_requestAttestationForTesting,
+};
+
+/// Holds the result of a successful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Debug)]
+pub struct AttestationResult {
+    result: NonNull<AVmAttestationResult>,
+}
+
+/// Error type that can be returned from an unsuccessful Virtual Machine attestation request.
+/// See [`request_attestation`].
+#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
+pub enum AttestationError {
+    /// The challenge size was not between 0 and 64 bytes (inclusive).
+    InvalidChallenge,
+    /// The attempt to attest the VM failed. A subsequent request may succeed.
+    AttestationFailed,
+    /// VM attestation is not supported in the current environment.
+    AttestationUnsupported,
+}
+
+impl Error for AttestationError {}
+
+impl Display for AttestationError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
+        let status = match self {
+            Self::InvalidChallenge => AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE,
+            Self::AttestationFailed => AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED,
+            Self::AttestationUnsupported => AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED,
+        };
+        // SAFETY: AVmAttestationStatus_toString always returns a non-null pointer to a
+        // nul-terminated C string with static lifetime (which is valid UTF-8).
+        let c_str = unsafe { CStr::from_ptr(AVmAttestationStatus_toString(status)) };
+        let str = c_str.to_str().expect("Invalid UTF-8 for AVmAttestationStatus");
+        f.write_str(str)
+    }
+}
+
+impl Drop for AttestationResult {
+    fn drop(&mut self) {
+        let ptr = self.result.as_ptr();
+
+        // SAFETY: The `result` field is private, and only populated with a successful call to
+        // `AVmPayload_requestAttestation`, and not freed elsewhere.
+        unsafe { AVmAttestationResult_free(ptr) };
+    }
+}
+
+// SAFETY: The API functions that accept the `AVmAttestationResult` pointer are all safe to call
+// from any thread, including `AVmAttestationResult_free` which is called only on drop.
+unsafe impl Send for AttestationResult {}
+
+// SAFETY: There is no interior mutation here; any future functions that might mutate the data would
+// require a non-const pointer and hence need `&mut self` here. The only existing such function is
+// `AVmAttestationResult_free` where we take a mutable reference guaranteeing no other references
+// exist. The raw API functions are safe to call from any thread.
+unsafe impl Sync for AttestationResult {}
+
+/// Requests the remote attestation of this VM.
+///
+/// On success the supplied [`challenge`] will be included in the certificate chain accessible from
+/// the [`AttestationResult`]; this can be used as proof of the freshness of the attestation.
+///
+/// The challenge should be no more than 64 bytes long or the request will fail.
+pub fn request_attestation(challenge: &[u8]) -> Result<AttestationResult, AttestationError> {
+    let mut result: *mut AVmAttestationResult = ptr::null_mut();
+    // SAFETY: We only read the challenge within its bounds and the function does not retain any
+    // reference to it.
+    let status = unsafe {
+        AVmPayload_requestAttestation(
+            challenge.as_ptr() as *const c_void,
+            challenge.len(),
+            &mut result,
+        )
+    };
+    AttestationResult::new(status, result)
+}
+
+/// A variant of [`request_attestation`] used for testing purposes. This should not be used by
+/// normal VMs, and is not available to app owned VMs.
+pub fn request_attestation_for_testing(
+    challenge: &[u8],
+) -> Result<AttestationResult, AttestationError> {
+    let mut result: *mut AVmAttestationResult = ptr::null_mut();
+    // SAFETY: We only read the challenge within its bounds and the function does not retain any
+    // reference to it.
+    let status = unsafe {
+        AVmPayload_requestAttestationForTesting(
+            challenge.as_ptr() as *const c_void,
+            challenge.len(),
+            &mut result,
+        )
+    };
+    AttestationResult::new(status, result)
+}
+
+impl AttestationResult {
+    fn new(
+        status: AVmAttestationStatus,
+        result: *mut AVmAttestationResult,
+    ) -> Result<AttestationResult, AttestationError> {
+        match status {
+            AVmAttestationStatus::ATTESTATION_ERROR_INVALID_CHALLENGE => {
+                Err(AttestationError::InvalidChallenge)
+            }
+            AVmAttestationStatus::ATTESTATION_ERROR_ATTESTATION_FAILED => {
+                Err(AttestationError::AttestationFailed)
+            }
+            AVmAttestationStatus::ATTESTATION_ERROR_UNSUPPORTED => {
+                Err(AttestationError::AttestationUnsupported)
+            }
+            AVmAttestationStatus::ATTESTATION_OK => {
+                let result = NonNull::new(result)
+                    .expect("Attestation succeeded but the attestation result is null");
+                Ok(AttestationResult { result })
+            }
+        }
+    }
+
+    fn as_const_ptr(&self) -> *const AVmAttestationResult {
+        self.result.as_ptr().cast_const()
+    }
+
+    /// Returns the attested private key. This is the ECDSA P-256 private key corresponding to the
+    /// public key described by the leaf certificate in the attested
+    /// [certificate chain](AttestationResult::certificate_chain). It is a DER-encoded
+    /// `ECPrivateKey` structure as specified in
+    /// [RFC 5915 s3](https://datatracker.ietf.org/doc/html/rfc5915#section-3).
+    ///
+    /// Note: The [`sign_message`](AttestationResult::sign_message) method allows signing with the
+    /// key without retrieving it.
+    pub fn private_key(&self) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        let size =
+            // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+            // writes no data since we pass a zero size, and null is explicitly allowed for the
+            // destination in that case.
+            unsafe { AVmAttestationResult_getPrivateKey(ptr, ptr::null_mut(), 0) };
+
+        let mut private_key = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `private_key`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_getPrivateKey(
+                ptr,
+                private_key.as_mut_ptr() as *mut c_void,
+                private_key.len(),
+            )
+        };
+        assert_eq!(size, private_key.len());
+        private_key
+    }
+
+    /// Signs the given message using the attested private key. The signature uses ECDSA P-256; the
+    /// message is first hashed with SHA-256 and then it is signed with the attested EC P-256
+    /// [private key](AttestationResult::private_key).
+    ///
+    /// The signature is a DER-encoded `ECDSASignature`` structure as described in
+    /// [RFC 6979](https://datatracker.ietf.org/doc/html/rfc6979).
+    pub fn sign_message(&self, message: &[u8]) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+        // writes no data since we pass a zero size, and null is explicitly allowed for the
+        // destination in that case.
+        let size = unsafe {
+            AVmAttestationResult_sign(
+                ptr,
+                message.as_ptr() as *const c_void,
+                message.len(),
+                ptr::null_mut(),
+                0,
+            )
+        };
+
+        let mut signature = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `signature`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_sign(
+                ptr,
+                message.as_ptr() as *const c_void,
+                message.len(),
+                signature.as_mut_ptr() as *mut c_void,
+                signature.len(),
+            )
+        };
+        assert!(size <= signature.len());
+        signature.truncate(size);
+        signature
+    }
+
+    /// Returns an iterator over the certificates forming the certificate chain for the VM, and its
+    /// public key, obtained by the attestation process.
+    ///
+    /// The certificate chain consists of a sequence of DER-encoded X.509 certificates that form
+    /// the attestation key's certificate chain. It starts with the leaf certificate covering the
+    /// attested public key and ends with the root certificate.
+    pub fn certificate_chain(&self) -> CertIterator {
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid.
+        let count = unsafe { AVmAttestationResult_getCertificateCount(self.as_const_ptr()) };
+
+        CertIterator { result: self, count, current: 0 }
+    }
+
+    fn certificate(&self, index: usize) -> Vec<u8> {
+        let ptr = self.as_const_ptr();
+
+        let size =
+            // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function
+            // writes no data since we pass a zero size, and null is explicitly allowed for the
+            // destination in that case. The function will panic if `index` is out of range (which
+            // is safe).
+            unsafe { AVmAttestationResult_getCertificateAt(ptr, index, ptr::null_mut(), 0) };
+
+        let mut cert = vec![0u8; size];
+        // SAFETY: We own the `AVmAttestationResult` pointer, so it is valid. The function only
+        // writes within the bounds of `cert`, which we just allocated so cannot be aliased.
+        let size = unsafe {
+            AVmAttestationResult_getCertificateAt(
+                ptr,
+                index,
+                cert.as_mut_ptr() as *mut c_void,
+                cert.len(),
+            )
+        };
+        assert_eq!(size, cert.len());
+        cert
+    }
+}
+
+/// An iterator over the DER-encoded X.509 certificates containin in an [`AttestationResult`].
+/// See [`certificate_chain`](AttestationResult::certificate_chain) for more details.
+pub struct CertIterator<'a> {
+    result: &'a AttestationResult,
+    count: usize,
+    current: usize, // Invariant: current <= count
+}
+
+impl<'a> Iterator for CertIterator<'a> {
+    type Item = Vec<u8>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.current < self.count {
+            let cert = self.result.certificate(self.current);
+            self.current += 1;
+            Some(cert)
+        } else {
+            None
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        let size = self.count - self.current;
+        (size, Some(size))
+    }
+}
+
+impl<'a> ExactSizeIterator for CertIterator<'a> {}
+impl<'a> FusedIterator for CertIterator<'a> {}
diff --git a/vm_payload/wrapper/lib.rs b/vm_payload/wrapper/lib.rs
new file mode 100644
index 0000000..d3f03d7
--- /dev/null
+++ b/vm_payload/wrapper/lib.rs
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+
+//! Rust wrapper for the VM Payload API, allowing virtual machine payload code to be written in
+//! Rust. This wraps the raw C API, accessed via bindgen, into a more idiomatic Rust interface.
+//!
+//! See `https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Virtualization/vm_payload/README.md`
+//! for more information on the VM Payload API.
+
+mod attestation;
+
+pub use attestation::{request_attestation, AttestationError, AttestationResult};
+use binder::unstable_api::AsNative;
+use binder::{FromIBinder, Strong};
+use std::ffi::{c_void, CStr, OsStr};
+use std::os::unix::ffi::OsStrExt;
+use std::path::Path;
+use std::ptr;
+use vm_payload_bindgen::{
+    AIBinder, AVmPayload_getApkContentsPath, AVmPayload_getEncryptedStoragePath,
+    AVmPayload_getVmInstanceSecret, AVmPayload_notifyPayloadReady, AVmPayload_runVsockRpcServer,
+};
+
+/// The functions declared here are restricted to VMs created with a config file;
+/// they will fail, or panic, if called in other VMs. The ability to create such VMs
+/// requires the android.permission.USE_CUSTOM_VIRTUAL_MACHINE permission, and is
+/// therefore not available to privileged or third party apps.
+///
+/// These functions can be used by tests, if the permission is granted via shell.
+pub mod restricted {
+    pub use crate::attestation::request_attestation_for_testing;
+}
+
+/// Marks the main function of the VM payload.
+///
+/// When the VM is run, this function is called. If it returns, the VM ends normally with a 0 exit
+/// code.
+///
+/// Example:
+///
+/// ```rust
+/// use log::info;
+///
+/// vm_payload::main!(vm_main);
+///
+/// fn vm_main() {
+///     android_logger::init_once(
+///          android_logger::Config::default()
+///             .with_tag("example_vm_payload")
+///             .with_max_level(log::LevelFilter::Info),
+///     );
+///     info!("Hello world");
+/// }
+/// ```
+#[macro_export]
+macro_rules! main {
+    ($name:path) => {
+        // Export a symbol with a name matching the extern declaration below.
+        #[export_name = "rust_main"]
+        fn __main() {
+            // Ensure that the main function provided by the application has the correct type.
+            $name()
+        }
+    };
+}
+
+// This is the real C entry point for the VM; we just forward to the Rust entry point.
+#[allow(non_snake_case)]
+#[no_mangle]
+extern "C" fn AVmPayload_main() {
+    extern "Rust" {
+        fn rust_main();
+    }
+
+    // SAFETY: rust_main is provided by the application using the `main!` macro above, which makes
+    // sure it has the right type.
+    unsafe { rust_main() }
+}
+
+/// Notifies the host that the payload is ready.
+///
+/// If the host app has set a `VirtualMachineCallback` for the VM, its
+/// `onPayloadReady` method will be called.
+///
+/// Note that subsequent calls to this function after the first have no effect;
+/// `onPayloadReady` is never called more than once.
+pub fn notify_payload_ready() {
+    // SAFETY: Invokes a method from the bindgen library `vm_payload_bindgen` which is safe to
+    // call at any time.
+    unsafe { AVmPayload_notifyPayloadReady() };
+}
+
+/// Runs a binder RPC server, serving the supplied binder service implementation on the given vsock
+/// port.
+///
+/// If and when the server is ready for connections (i.e. it is listening on the port),
+/// [`notify_payload_ready`] is called to notify the host that the server is ready. This is
+/// appropriate for VM payloads that serve a single binder service - which is common.
+///
+/// Note that this function does not return. The calling thread joins the binder
+/// thread pool to handle incoming messages.
+pub fn run_single_vsock_service<T>(service: Strong<T>, port: u32) -> !
+where
+    T: FromIBinder + ?Sized,
+{
+    extern "C" fn on_ready(_param: *mut c_void) {
+        notify_payload_ready();
+    }
+
+    let mut service = service.as_binder();
+    // The cast here is needed because the compiler doesn't know that our vm_payload_bindgen
+    // AIBinder is the same type as binder_ndk_sys::AIBinder.
+    let service = service.as_native_mut() as *mut AIBinder;
+    let param = ptr::null_mut();
+    // SAFETY: We have a strong reference to the service, so the raw pointer remains valid. It is
+    // safe for on_ready to be invoked at any time, with any parameter.
+    unsafe { AVmPayload_runVsockRpcServer(service, port, Some(on_ready), param) }
+}
+
+/// Gets the path to the contents of the APK containing the VM payload. It is a directory, under
+/// which are the unzipped contents of the APK containing the payload, all read-only
+/// but accessible to the payload.
+pub fn apk_contents_path() -> &'static Path {
+    // SAFETY: AVmPayload_getApkContentsPath always returns a non-null pointer to a
+    // nul-terminated C string with static lifetime.
+    let c_str = unsafe { CStr::from_ptr(AVmPayload_getApkContentsPath()) };
+    Path::new(OsStr::from_bytes(c_str.to_bytes()))
+}
+
+/// Gets the path to the encrypted persistent storage for the VM, if any. This is
+/// a directory under which any files or directories created will be stored on
+/// behalf of the VM by the host app. All data is encrypted using a key known
+/// only to the VM, so the host cannot decrypt it, but may delete it.
+///
+/// Returns `None` if no encrypted storage was requested in the VM configuration.
+pub fn encrypted_storage_path() -> Option<&'static Path> {
+    // SAFETY: AVmPayload_getEncryptedStoragePath returns either null or a pointer to a
+    // nul-terminated C string with static lifetime.
+    let ptr = unsafe { AVmPayload_getEncryptedStoragePath() };
+    if ptr.is_null() {
+        None
+    } else {
+        // SAFETY: We know the pointer is not null, and so it is a valid C string.
+        let c_str = unsafe { CStr::from_ptr(ptr) };
+        Some(Path::new(OsStr::from_bytes(c_str.to_bytes())))
+    }
+}
+
+/// Retrieves all or part of a 32-byte secret that is bound to this unique VM
+/// instance and the supplied identifier. The secret can be used e.g. as an
+/// encryption key.
+///
+/// Every VM has a secret that is derived from a device-specific value known to
+/// the hypervisor, the code that runs in the VM and its non-modifiable
+/// configuration; it is not made available to the host OS.
+///
+/// This function performs a further derivation from the VM secret and the
+/// supplied identifier. As long as the VM identity doesn't change the same value
+/// will be returned for the same identifier, even if the VM is stopped &
+/// restarted or the device rebooted.
+///
+/// If multiple secrets are required for different purposes, a different
+/// identifier should be used for each. The identifiers otherwise are arbitrary
+/// byte sequences and do not need to be kept secret; typically they are
+/// hardcoded in the calling code.
+///
+/// The secret is returned in [`secret`], truncated to its size, which must be between
+/// 1 and 32 bytes (inclusive) or the function will panic.
+pub fn get_vm_instance_secret(identifier: &[u8], secret: &mut [u8]) {
+    let secret_size = secret.len();
+    assert!((1..=32).contains(&secret_size), "VM instance secrets can be up to 32 bytes long");
+
+    // SAFETY: The function only reads from `[identifier]` within its bounds, and only writes to
+    // `[secret]` within its bounds. Neither reference is retained, and we know neither is null.
+    unsafe {
+        AVmPayload_getVmInstanceSecret(
+            identifier.as_ptr() as *const c_void,
+            identifier.len(),
+            secret.as_mut_ptr() as *mut c_void,
+            secret_size,
+        )
+    }
+}
diff --git a/vmlauncher_app/AndroidManifest.xml b/vmlauncher_app/AndroidManifest.xml
index d800ec7..4f95086 100644
--- a/vmlauncher_app/AndroidManifest.xml
+++ b/vmlauncher_app/AndroidManifest.xml
@@ -7,19 +7,22 @@
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-feature android:name="android.software.virtualization_framework" android:required="true" />
     <application
-        android:label="VmLauncherApp"
-        android:networkSecurityConfig="@xml/network_security_config">
+        android:label="VmLauncherApp">
         <activity android:name=".MainActivity"
-                  android:enabled="false"
                   android:screenOrientation="landscape"
                   android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
                   android:theme="@style/MyTheme"
                   android:exported="true">
+        </activity>
+        <activity-alias android:name=".MainActivityAlias"
+                android:targetActivity="com.android.virtualization.vmlauncher.MainActivity"
+                android:exported="true"
+                android:enabled="false">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
-        </activity>
+        </activity-alias>
     </application>
 
 </manifest>
diff --git a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
index 33e4755..c2f218a 100644
--- a/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/vmlauncher_app/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -188,6 +188,7 @@
             customImageConfigBuilder.useTouch(true);
             customImageConfigBuilder.useKeyboard(true);
             customImageConfigBuilder.useMouse(true);
+            customImageConfigBuilder.useNetwork(true);
 
             configBuilder.setCustomImageConfig(customImageConfigBuilder.build());
 
diff --git a/vmlauncher_app/res/xml/network_security_config.xml b/vmlauncher_app/res/xml/network_security_config.xml
deleted file mode 100644
index f27fa56..0000000
--- a/vmlauncher_app/res/xml/network_security_config.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?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.
-  -->
-
-<network-security-config>
-    <domain-config cleartextTrafficPermitted="true">
-        <domain includeSubdomains="true">localhost</domain>
-    </domain-config>
-</network-security-config>