Snap for 6533464 from f1aef0d73ab38e5304694d9aefde87acdf77753f to sdk-release

Change-Id: If77bfde4bb3516ca6c36c899d702a854d224d1f0
diff --git a/METADATA b/METADATA
new file mode 100644
index 0000000..d97975c
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,3 @@
+third_party {
+  license_type: NOTICE
+}
diff --git a/common/libs/device_config/host_device_config.cpp b/common/libs/device_config/host_device_config.cpp
index f1399b2..ba34128 100644
--- a/common/libs/device_config/host_device_config.cpp
+++ b/common/libs/device_config/host_device_config.cpp
@@ -41,22 +41,25 @@
   uint8_t ril_prefixlen = -1;
   std::string ril_ipaddr;
   std::string ril_gateway;
-  std::string ril_dns = "8.8.8.8";
+  std::string ril_dns;
   std::string ril_broadcast;
 
-  bool ObtainConfig(const std::string& interface) {
-    bool ret = ParseIntefaceAttributes(interface);
-    LOG(INFO) << "Network config:";
-    LOG(INFO) << "ipaddr = " << ril_ipaddr;
-    LOG(INFO) << "gateway = " << ril_gateway;
-    LOG(INFO) << "dns = " << ril_dns;
-    LOG(INFO) << "broadcast = " << ril_broadcast;
-    LOG(INFO) << "prefix length = " << static_cast<int>(ril_prefixlen);
+  bool ObtainConfig(const std::string& interface, const std::string& dns) {
+    bool ret = ParseInterfaceAttributes(interface);
+    if (ret) {
+      ril_dns = dns;
+      LOG(INFO) << "Network config:";
+      LOG(INFO) << "ipaddr = " << ril_ipaddr;
+      LOG(INFO) << "gateway = " << ril_gateway;
+      LOG(INFO) << "dns = " << ril_dns;
+      LOG(INFO) << "broadcast = " << ril_broadcast;
+      LOG(INFO) << "prefix length = " << static_cast<int>(ril_prefixlen);
+    }
     return ret;
   }
 
  private:
-  bool ParseIntefaceAttributes(struct ifaddrs* ifa) {
+  bool ParseInterfaceAttributes(struct ifaddrs* ifa) {
     struct sockaddr_in* sa;
     char* addr_str;
 
@@ -72,6 +75,16 @@
     this->ril_broadcast = strtok(addr_str, "\n");
     auto broadcast_s_addr = ntohl(sa->sin_addr.s_addr);
 
+    // Detect misconfigured network interfaces. All network interfaces must
+    // have a valid broadcast address set; if there is none set, glibc may
+    // return the interface address in the broadcast field. This causes
+    // no packets to be routed correctly from the guest.
+    if (this->ril_gateway == this->ril_broadcast) {
+      LOG(ERROR) << "Gateway and Broadcast addresses are the same on "
+                 << ifa->ifa_name << ", which is invalid.";
+      return false;
+    }
+
     // Netmask
     sa = reinterpret_cast<sockaddr_in*>(ifa->ifa_netmask);
     this->ril_prefixlen = number_of_ones(sa->sin_addr.s_addr);
@@ -99,14 +112,14 @@
     return true;
   }
 
-  bool ParseIntefaceAttributes(const std::string& interface) {
+  bool ParseInterfaceAttributes(const std::string& interface) {
     struct ifaddrs *ifa_list{}, *ifa{};
     bool ret = false;
     getifaddrs(&ifa_list);
     for (ifa = ifa_list; ifa; ifa = ifa->ifa_next) {
       if (strcmp(ifa->ifa_name, interface.c_str()) == 0 &&
           ifa->ifa_addr->sa_family == AF_INET) {
-        ret = ParseIntefaceAttributes(ifa);
+        ret = ParseInterfaceAttributes(ifa);
         break;
       }
     }
@@ -140,9 +153,16 @@
     const vsoc::CuttlefishConfig& config) {
   auto instance = config.ForDefaultInstance();
   NetConfig netconfig;
-  if (!netconfig.ObtainConfig(instance.mobile_bridge_name())) {
-    LOG(ERROR) << "Unable to obtain the network configuration";
-    return false;
+  // Check the mobile bridge first; this was the traditional way we configured
+  // the mobile interface. If that fails, it probably means we are using a
+  // newer version of cuttlefish-common, and we can use the tap device
+  // directly instead.
+  if (!netconfig.ObtainConfig(instance.mobile_bridge_name(),
+                              config.ril_dns())) {
+    if (!netconfig.ObtainConfig(instance.mobile_tap_name(), config.ril_dns())) {
+      LOG(ERROR) << "Unable to obtain the network configuration";
+      return false;
+    }
   }
 
   auto res = snprintf(data_.ril.ipaddr, sizeof(data_.ril.ipaddr), "%s",
diff --git a/common/libs/fs/shared_buf.cc b/common/libs/fs/shared_buf.cc
index dbf33ba..5ae1aa2 100644
--- a/common/libs/fs/shared_buf.cc
+++ b/common/libs/fs/shared_buf.cc
@@ -22,13 +22,15 @@
 #include "common/libs/fs/shared_buf.h"
 #include "common/libs/fs/shared_fd.h"
 
-namespace cvd {
-
 namespace {
 
 const size_t BUFF_SIZE = 1 << 14;
 
-static ssize_t WriteAll(SharedFD fd, const char* buf, size_t size) {
+} // namespace
+
+namespace cvd {
+
+ssize_t WriteAll(SharedFD fd, const char* buf, size_t size) {
   size_t total_written = 0;
   ssize_t written = 0;
   while ((written = fd->Write((void*)&(buf[total_written]), size - total_written)) > 0) {
@@ -60,8 +62,6 @@
   return total_read;
 }
 
-} // namespace
-
 ssize_t ReadAll(SharedFD fd, std::string* buf) {
   char buff[BUFF_SIZE];
   std::stringstream ss;
diff --git a/common/libs/fs/shared_buf.h b/common/libs/fs/shared_buf.h
index c6e7590..f8f86b7 100644
--- a/common/libs/fs/shared_buf.h
+++ b/common/libs/fs/shared_buf.h
@@ -53,6 +53,29 @@
 ssize_t ReadExact(SharedFD fd, std::vector<char>* buf);
 
 /**
+ * Reads from fd until reading `size` bytes or errors.
+ *
+ * On a successful read, returns buf->size().
+ *
+ * If a read error is encountered, returns -1. buf will contain any data read
+ * up until that point and errno will be set.
+ */
+ssize_t ReadExact(SharedFD fd, char* buf, size_t size);
+
+/*
+ * Reads from fd until reading `sizeof(T)` bytes or errors.
+ *
+ * On a successful read, returns `sizeof(T)`.
+ *
+ * If a read error is encountered, returns -1. buf will contain any data read
+ * up until that point and errno will be set.
+ */
+template<typename T>
+ssize_t ReadExactBinary(SharedFD fd, T* binary_data) {
+  return ReadExact(fd, (char*) binary_data, sizeof(*binary_data));
+}
+
+/**
  * Writes to fd until writing all bytes in buf.
  *
  * On a successful write, returns buf.size().
@@ -72,4 +95,27 @@
  */
 ssize_t WriteAll(SharedFD fd, const std::vector<char>& buf);
 
+/**
+ * Writes to fd until `size` bytes are written from `buf`.
+ *
+ * On a successful write, returns `size`.
+ *
+ * If a write error is encountered, returns -1. Some data may have already been
+ * written to fd at that point.
+ */
+ssize_t WriteAll(SharedFD fd, const char* buf, size_t size);
+
+/**
+ * Writes to fd until `sizeof(T)` bytes are written from binary_data.
+ *
+ * On a successful write, returns `sizeof(T)`.
+ *
+ * If a write error is encountered, returns -1. Some data may have already been
+ * written to fd at that point.
+ */
+template<typename T>
+ssize_t WriteAllBinary(SharedFD fd, const T* binary_data) {
+  return WriteAll(fd, (const char*) binary_data, sizeof(*binary_data));
+}
+
 } // namespace cvd
diff --git a/common/libs/security/Android.bp b/common/libs/security/Android.bp
new file mode 100644
index 0000000..c007c35
--- /dev/null
+++ b/common/libs/security/Android.bp
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2020 The Android Open-Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_library {
+    name: "libcuttlefish_security",
+    defaults: ["hidl_defaults", "cuttlefish_host_and_guest"],
+    srcs: [
+        "keymaster_channel.cpp",
+    ],
+    header_libs: [
+        "libhardware_headers",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcuttlefish_fs",
+        "libkeymaster_messages",
+        "liblog",
+    ],
+}
diff --git a/common/libs/security/keymaster_channel.cpp b/common/libs/security/keymaster_channel.cpp
new file mode 100644
index 0000000..34c7da6
--- /dev/null
+++ b/common/libs/security/keymaster_channel.cpp
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "keymaster_channel.h"
+
+#include <android-base/logging.h>
+#include "keymaster/android_keymaster_utils.h"
+
+#include "common/libs/fs/shared_buf.h"
+
+namespace cvd {
+
+ManagedKeymasterMessage CreateKeymasterMessage(
+    AndroidKeymasterCommand command, bool is_response, size_t payload_size) {
+  auto memory = new uint8_t[payload_size + sizeof(keymaster_message)];
+  auto message = reinterpret_cast<keymaster_message*>(memory);
+  message->cmd = command;
+  message->is_response = is_response;
+  message->payload_size = payload_size;
+  return ManagedKeymasterMessage(message);
+}
+
+void KeymasterCommandDestroyer::operator()(keymaster_message* ptr) {
+  {
+    keymaster::Eraser(ptr, sizeof(keymaster_message) + ptr->payload_size);
+  }
+  delete reinterpret_cast<uint8_t*>(ptr);
+}
+
+KeymasterChannel::KeymasterChannel(SharedFD channel) : channel_(channel) {
+}
+
+bool KeymasterChannel::SendRequest(
+    AndroidKeymasterCommand command, const keymaster::Serializable& message) {
+  return SendMessage(command, false, message);
+}
+
+bool KeymasterChannel::SendResponse(
+    AndroidKeymasterCommand command, const keymaster::Serializable& message) {
+  return SendMessage(command, true, message);
+}
+
+bool KeymasterChannel::SendMessage(
+    AndroidKeymasterCommand command,
+    bool is_response,
+    const keymaster::Serializable& message) {
+  LOG(DEBUG) << "Sending message with id: " << command;
+  auto payload_size = message.SerializedSize();
+  auto to_send = CreateKeymasterMessage(command, is_response, payload_size);
+  message.Serialize(to_send->payload, to_send->payload + payload_size);
+  auto write_size = payload_size + sizeof(keymaster_message);
+  auto to_send_bytes = reinterpret_cast<const char*>(to_send.get());
+  auto written = cvd::WriteAll(channel_, to_send_bytes, write_size);
+  if (written == -1) {
+    LOG(ERROR) << "Could not write Keymaster Message: " << channel_->StrError();
+  }
+  return written == write_size;
+}
+
+ManagedKeymasterMessage KeymasterChannel::ReceiveMessage() {
+  struct keymaster_message message_header;
+  auto read = cvd::ReadExactBinary(channel_, &message_header);
+  if (read != sizeof(keymaster_message)) {
+    LOG(ERROR) << "Expected " << sizeof(keymaster_message) << ", received "
+               << read;
+    LOG(ERROR) << "Could not read Keymaster Message: " << channel_->StrError();
+    return {};
+  }
+  LOG(DEBUG) << "Received message with id: " << message_header.cmd;
+  auto message = CreateKeymasterMessage(message_header.cmd,
+                                        message_header.is_response,
+                                        message_header.payload_size);
+  auto message_bytes = reinterpret_cast<char*>(message->payload);
+  read = cvd::ReadExact(channel_, message_bytes, message->payload_size);
+  if (read != message->payload_size) {
+    LOG(ERROR) << "Could not read Keymaster Message: " << channel_->StrError();
+    return {};
+  }
+  return message;
+}
+
+}
diff --git a/common/libs/security/keymaster_channel.h b/common/libs/security/keymaster_channel.h
new file mode 100644
index 0000000..529325a
--- /dev/null
+++ b/common/libs/security/keymaster_channel.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "keymaster/android_keymaster_messages.h"
+#include "keymaster/serializable.h"
+
+#include "common/libs/fs/shared_fd.h"
+
+#include <memory>
+
+namespace keymaster {
+
+/**
+ * keymaster_message - Serial header for communicating with KM server
+ * @cmd: the command, one of AndroidKeymasterCommand.
+ * @payload: start of the serialized command specific payload
+ */
+struct keymaster_message {
+    AndroidKeymasterCommand cmd : 31;
+    bool is_response : 1;
+    uint32_t payload_size;
+    uint8_t payload[0];
+};
+
+} // namespace keymaster
+
+namespace cvd {
+
+using keymaster::AndroidKeymasterCommand;
+using keymaster::keymaster_message;
+
+/**
+ * A destroyer for keymaster_message instances created with
+ * CreateKeymasterMessage. Wipes memory from the keymaster_message instances.
+ */
+class KeymasterCommandDestroyer {
+public:
+  void operator()(keymaster_message* ptr);
+};
+
+/** An owning pointer for a keymaster_message instance. */
+using ManagedKeymasterMessage =
+    std::unique_ptr<keymaster_message, KeymasterCommandDestroyer>;
+
+/**
+ * Allocates memory for a keymaster_message carrying a message of size
+ * `payload_size`.
+ */
+ManagedKeymasterMessage CreateKeymasterMessage(
+    AndroidKeymasterCommand command, bool is_response, size_t payload_size);
+
+/*
+ * Interface for communication channels that synchronously communicate Keymaster
+ * IPC/RPC calls. Sends messages over a file descriptor.
+ */
+class KeymasterChannel {
+private:
+  SharedFD channel_;
+  bool SendMessage(AndroidKeymasterCommand command, bool response,
+                   const keymaster::Serializable& message);
+public:
+  KeymasterChannel(SharedFD channel);
+
+  bool SendRequest(AndroidKeymasterCommand command,
+                   const keymaster::Serializable& message);
+  bool SendResponse(AndroidKeymasterCommand command,
+                    const keymaster::Serializable& message);
+  ManagedKeymasterMessage ReceiveMessage();
+};
+
+} // namespace cvd
diff --git a/common/libs/utils/Android.bp b/common/libs/utils/Android.bp
index a3fc45e..830a9e9 100644
--- a/common/libs/utils/Android.bp
+++ b/common/libs/utils/Android.bp
@@ -23,11 +23,13 @@
         "files.cpp",
         "users.cpp",
         "network.cpp",
+        "base64.cpp",
     ],
     shared: {
         shared_libs: [
             "libbase",
             "libcuttlefish_fs",
+            "libcrypto",
         ],
     },
     static: {
@@ -35,6 +37,9 @@
             "libbase",
             "libcuttlefish_fs",
         ],
+        shared_libs: [
+          "libcrypto", // libcrypto_static is not accessible from all targets
+        ],
     },
     defaults: ["cuttlefish_host_and_guest"],
 }
diff --git a/common/libs/utils/base64.cpp b/common/libs/utils/base64.cpp
new file mode 100644
index 0000000..cd4fbb7
--- /dev/null
+++ b/common/libs/utils/base64.cpp
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "common/libs/utils/base64.h"
+
+#include <openssl/base64.h>
+
+namespace cvd {
+
+bool EncodeBase64(const void *data, size_t size, std::string *out) {
+  size_t enc_len = 0;
+  auto len_res = EVP_EncodedLength(&enc_len, size);
+  if (!len_res) {
+    return false;
+  }
+  out->resize(enc_len);
+  auto enc_res = EVP_EncodeBlock(reinterpret_cast<uint8_t *>(out->data()),
+                                 reinterpret_cast<const uint8_t *>(data), size);
+  if (enc_res < 0) {
+    return false;
+  }
+  out->resize(enc_res);  // Don't count the terminating \0 character
+  return true;
+}
+
+bool DecodeBase64(const std::string &data, std::vector<uint8_t> *buffer) {
+  size_t out_len;
+  auto len_res = EVP_DecodedLength(&out_len, data.size());
+  if (!len_res) {
+    return false;
+  }
+  buffer->resize(out_len);
+  return EVP_DecodeBase64(buffer->data(), &out_len, out_len,
+                          reinterpret_cast<const uint8_t *>(data.data()),
+                          data.size());
+}
+
+}  // namespace cvd
diff --git a/common/libs/utils/base64.h b/common/libs/utils/base64.h
new file mode 100644
index 0000000..15ad268
--- /dev/null
+++ b/common/libs/utils/base64.h
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <cinttypes>
+#include <string>
+#include <vector>
+
+namespace cvd {
+
+bool EncodeBase64(const void* _data, size_t size, std::string* out);
+
+bool DecodeBase64(const std::string& data, std::vector<uint8_t>* buffer);
+
+}  // namespace cvd
diff --git a/common/libs/utils/files.cpp b/common/libs/utils/files.cpp
index 24d0223..ac81346 100644
--- a/common/libs/utils/files.cpp
+++ b/common/libs/utils/files.cpp
@@ -22,6 +22,7 @@
 #include <climits>
 #include <cstdio>
 #include <cstdlib>
+#include <fstream>
 #include <sys/types.h>
 #include <sys/stat.h>
 #include <unistd.h>
@@ -107,11 +108,33 @@
   return std::chrono::system_clock::time_point(seconds);
 }
 
+bool RenameFile(const std::string& old_name, const std::string& new_name) {
+  LOG(INFO) << "Renaming " << old_name << " to " << new_name;
+  if(rename(old_name.c_str(), new_name.c_str())) {
+    LOG(ERROR) << "File rename failed due to " << strerror(errno);
+    return false;
+  }
+
+  return true;
+}
+
 bool RemoveFile(const std::string& file) {
   LOG(INFO) << "Removing " << file;
   return remove(file.c_str()) == 0;
 }
 
+
+std::string ReadFile(const std::string& file) {
+  std::string contents;
+  std::ifstream in(file, std::ios::in | std::ios::binary);
+  in.seekg(0, std::ios::end);
+  contents.resize(in.tellg());
+  in.seekg(0, std::ios::beg);
+  in.read(&contents[0], contents.size());
+  in.close();
+  return(contents);
+}
+
 std::string CurrentDirectory() {
   char* path = getcwd(nullptr, 0);
   std::string ret(path);
diff --git a/common/libs/utils/files.h b/common/libs/utils/files.h
index 13e6892..0541344 100644
--- a/common/libs/utils/files.h
+++ b/common/libs/utils/files.h
@@ -27,6 +27,8 @@
 bool IsDirectoryEmpty(const std::string& path);
 off_t FileSize(const std::string& path);
 bool RemoveFile(const std::string& file);
+bool RenameFile(const std::string& old_name, const std::string& new_name);
+std::string ReadFile(const std::string& file);
 std::chrono::system_clock::time_point FileModificationTime(const std::string& path);
 
 // The returned value may contain .. or . if these are present in the path
diff --git a/common/libs/utils/network.cpp b/common/libs/utils/network.cpp
index 033430e..33a1ea9 100644
--- a/common/libs/utils/network.cpp
+++ b/common/libs/utils/network.cpp
@@ -30,10 +30,19 @@
 #include "android-base/logging.h"
 
 #include "common/libs/fs/shared_buf.h"
+#include "common/libs/utils/environment.h"
 #include "common/libs/utils/subprocess.h"
 
 namespace cvd {
 namespace {
+
+static std::string DefaultHostArtifactsPath(const std::string& file_name) {
+  return (cvd::StringFromEnv("ANDROID_HOST_OUT",
+                             cvd::StringFromEnv("HOME", ".")) +
+          "/") +
+         file_name;
+}
+
 // This should be the size of virtio_net_hdr_v1, from linux/virtio_net.h, but
 // the version of that header that ships with android in Pie does not include
 // that struct (it was added in Q).
@@ -88,28 +97,41 @@
     return tap_fd;
   }
 
-  struct ifreq ifr;
-  memset(&ifr, 0, sizeof(ifr));
-  ifr.ifr_flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR;
-  strncpy(ifr.ifr_name, interface_name.c_str(), IFNAMSIZ);
+  if (cvd::HostArch() == "aarch64") {
+    auto tapsetiff_path = DefaultHostArtifactsPath("bin/tapsetiff");
+    cvd::Command cmd(tapsetiff_path);
+    cmd.AddParameter(tap_fd);
+    cmd.AddParameter(interface_name.c_str());
+    int ret = cmd.Start().Wait();
+    if (ret != 0) {
+      LOG(ERROR) << "Unable to run tapsetiff.py. Exited with status " << ret;
+      tap_fd->Close();
+      return cvd::SharedFD();
+    }
+  } else {
+    struct ifreq ifr;
+    memset(&ifr, 0, sizeof(ifr));
+    ifr.ifr_flags = IFF_TAP | IFF_NO_PI | IFF_VNET_HDR;
+    strncpy(ifr.ifr_name, interface_name.c_str(), IFNAMSIZ);
 
-  int err = tap_fd->Ioctl(TUNSETIFF, &ifr);
-  if (err < 0) {
-    LOG(ERROR) << "Unable to connect to " << interface_name
-               << " tap interface: " << tap_fd->StrError();
-    tap_fd->Close();
-    return cvd::SharedFD();
-  }
+    int err = tap_fd->Ioctl(TUNSETIFF, &ifr);
+    if (err < 0) {
+      LOG(ERROR) << "Unable to connect to " << interface_name
+                 << " tap interface: " << tap_fd->StrError();
+      tap_fd->Close();
+      return cvd::SharedFD();
+    }
 
-  // The interface's configuration may have been modified or just not set
-  // correctly on creation. While qemu checks this and enforces the right
-  // configuration, crosvm does not, so it needs to be set before it's passed to
-  // it.
-  tap_fd->Ioctl(TUNSETOFFLOAD,
-                reinterpret_cast<void*>(TUN_F_CSUM | TUN_F_UFO | TUN_F_TSO4 |
+    // The interface's configuration may have been modified or just not set
+    // correctly on creation. While qemu checks this and enforces the right
+    // configuration, crosvm does not, so it needs to be set before it's passed to
+    // it.
+    tap_fd->Ioctl(TUNSETOFFLOAD,
+                  reinterpret_cast<void*>(TUN_F_CSUM | TUN_F_UFO | TUN_F_TSO4 |
                                         TUN_F_TSO6));
-  int len = SIZE_OF_VIRTIO_NET_HDR_V1;
-  tap_fd->Ioctl(TUNSETVNETHDRSZ, &len);
+    int len = SIZE_OF_VIRTIO_NET_HDR_V1;
+    tap_fd->Ioctl(TUNSETVNETHDRSZ, &len);
+  }
 
   return tap_fd;
 }
diff --git a/common/libs/utils/users.cpp b/common/libs/utils/users.cpp
index cd38982..0b59069 100644
--- a/common/libs/utils/users.cpp
+++ b/common/libs/utils/users.cpp
@@ -45,7 +45,7 @@
     if (grp_p != nullptr) {
       return grp.gr_gid;
     } else {
-      LOG(ERROR) << "Group " << group_name << " does not exist";
+      // Caller may be checking with non-existent group name
       return -1;
     }
   } else {
@@ -89,4 +89,4 @@
     return true;
   }
   return false;
-}
\ No newline at end of file
+}
diff --git a/guest/commands/vtpm_manager/main.cpp b/guest/commands/vtpm_manager/main.cpp
index f2df360..d2b3584 100644
--- a/guest/commands/vtpm_manager/main.cpp
+++ b/guest/commands/vtpm_manager/main.cpp
@@ -44,32 +44,39 @@
 bool ReadResponseLoop(cvd::SharedFD in_fd, cvd::SharedFD out_fd) {
   std::vector<char> message;
   while (true) {
-    std::vector<char> response_size_bytes(4, 0);
-    CHECK(cvd::ReadExact(in_fd, &response_size_bytes) == 4) << "Could not read response size";
+    std::uint32_t response_size;
+    CHECK(cvd::ReadExactBinary(in_fd, &response_size) == 4)
+        << "Could not read response size";
     // the tpm simulator writes 4 extra bytes at the end of the message.
-    std::uint32_t response_size = be32toh(*reinterpret_cast<std::uint32_t*>(response_size_bytes.data()));
+    response_size = be32toh(response_size);
     message.resize(response_size, '\0');
-    CHECK(cvd::ReadExact(in_fd, &message) == response_size) << "Could not read response message";
+    CHECK(cvd::ReadExact(in_fd, &message) == response_size)
+        << "Could not read response message";
     auto header = reinterpret_cast<tpm_message_header*>(message.data());
     auto host_rc = betoh32(header->ordinal);
-    LOG(DEBUG) << "TPM response was: \"" << Tss2_RC_Decode(host_rc) << "\" (" << host_rc << ")";
+    LOG(DEBUG) << "TPM response was: \"" << Tss2_RC_Decode(host_rc) << "\" ("
+               << host_rc << ")";
     std::vector<char> response_bytes(4, 0);
-    CHECK(cvd::ReadExact(in_fd, &response_bytes) == 4) << "Could not read parity response";
-    CHECK(cvd::WriteAll(out_fd, message) == message.size()) << "Could not forward message to vTPM";
+    CHECK(cvd::ReadExact(in_fd, &response_bytes) == 4)
+        << "Could not read parity response";
+    CHECK(cvd::WriteAll(out_fd, message) == message.size())
+        << "Could not forward message to vTPM";
   }
 }
 
 void SendCommand(cvd::SharedFD out_fd, std::vector<char> command) {
   // TODO(schuffelen): Implement this logic on the host.
   // TPM2 simulator command protocol.
-  std::vector<char> command_bytes(4, 0);
-  *reinterpret_cast<std::uint32_t*>(command_bytes.data()) = htobe32(8); // TPM_SEND_COMMAND
-  CHECK(cvd::WriteAll(out_fd, command_bytes) == 4) << "Could not send TPM_SEND_COMMAND";
-  CHECK(cvd::WriteAll(out_fd, std::vector<char>{(char)locality}) == 1) << "Could not send locality";
-  std::vector<char> length_bytes(4, 0);
-  *reinterpret_cast<std::uint32_t*>(length_bytes.data()) = htobe32(command.size());
-  CHECK(cvd::WriteAll(out_fd, length_bytes) == 4) << "Could not send command length";
-  CHECK(cvd::WriteAll(out_fd, command) == command.size()) << "Could not write TPM message";
+  std::uint32_t command_num = htobe32(8); // TPM_SEND_COMMAND
+  CHECK(cvd::WriteAllBinary(out_fd, &command_num) == 4)
+      << "Could not send TPM_SEND_COMMAND";
+  CHECK(cvd::WriteAllBinary(out_fd, (char*)&locality) == 1)
+      << "Could not send locality";
+  std::uint32_t length = htobe32(command.size());
+  CHECK(cvd::WriteAllBinary(out_fd, &length) == 4)
+      << "Could not send command length";
+  CHECK(cvd::WriteAll(out_fd, command) == command.size())
+      << "Could not write TPM message";
 }
 
 bool SendCommandLoop(cvd::SharedFD in_fd, cvd::SharedFD out_fd) {
@@ -80,19 +87,21 @@
     // is not large enough.
     // https://github.com/torvalds/linux/blob/407e9ef72476e64937ebec44cc835e03a25fb408/drivers/char/tpm/tpm_vtpm_proxy.c#L98
     while ((data_length = in_fd->Read(message.data(), message.size())) < 0) {
-      CHECK(in_fd->GetErrno() == EIO) << "Error in reading TPM command from kernel: "
-                                      << in_fd->StrError();
+      CHECK(in_fd->GetErrno() == EIO) << "Error in reading TPM command from "
+                                      << "kernel: " << in_fd->StrError();
       message.resize((message.size() + 1) * 2, '\0');
     }
     message.resize(data_length, 0);
     auto header = reinterpret_cast<tpm_message_header*>(message.data());
-    LOG(DEBUG) << "Received TPM command " << TpmCommandName(betoh32(header->ordinal));
+    LOG(DEBUG) << "Received TPM command "
+               << TpmCommandName(betoh32(header->ordinal));
     if (header->ordinal == htobe32(TPM2_CC_SET_LOCALITY)) { // "Driver command"
       locality = *reinterpret_cast<unsigned char*>(header + 1);
       header->ordinal = htobe32(locality);
       header->length = htobe32(sizeof(tpm_message_header));
       message.resize(sizeof(tpm_message_header), '\0');
-      CHECK(cvd::WriteAll(in_fd, message) == message.size()) << "Could not write TPM message";
+      CHECK(cvd::WriteAll(in_fd, message) == message.size())
+          << "Could not write TPM message";
     } else {
       SendCommand(out_fd, message);
     }
diff --git a/guest/hals/keymaster/remote/Android.bp b/guest/hals/keymaster/remote/Android.bp
new file mode 100644
index 0000000..99df093
--- /dev/null
+++ b/guest/hals/keymaster/remote/Android.bp
@@ -0,0 +1,49 @@
+//
+// Copyright (C) 2020 The Android Open-Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_binary {
+    name: "android.hardware.keymaster@4.0-service.remote",
+    defaults: ["hidl_defaults", "cuttlefish_guest_only"],
+    relative_install_path: "hw",
+    vendor: true,
+    init_rc: ["android.hardware.keymaster@4.0-service.remote.rc"],
+    srcs: [
+        "service4.cpp",
+        "remote_keymaster4_device.cpp",
+        "remote_keymaster.cpp",
+    ],
+
+    static_libs: [
+        "libgflags",
+    ],
+
+    shared_libs: [
+        "android.hardware.keymaster@4.0",
+        "libbase",
+        "libcutils",
+        "libcuttlefish_fs",
+        "libcuttlefish_security",
+        "libdl",
+        "libhardware",
+        "libhidlbase",
+        "libkeymaster4",
+        "libkeymaster_messages",
+        "liblog",
+        "libtrusty",
+        "libutils",
+    ],
+
+    vintf_fragments: ["android.hardware.keymaster@4.0-service.remote.xml"],
+}
diff --git a/guest/hals/keymaster/remote/android.hardware.keymaster@4.0-service.remote.rc b/guest/hals/keymaster/remote/android.hardware.keymaster@4.0-service.remote.rc
new file mode 100644
index 0000000..422d9b6
--- /dev/null
+++ b/guest/hals/keymaster/remote/android.hardware.keymaster@4.0-service.remote.rc
@@ -0,0 +1,2 @@
+service vendor.keymaster-4-0 /vendor/bin/hw/android.hardware.keymaster@4.0-service.remote
+    class early_hal
diff --git a/guest/hals/keymaster/remote/android.hardware.keymaster@4.0-service.remote.xml b/guest/hals/keymaster/remote/android.hardware.keymaster@4.0-service.remote.xml
new file mode 100644
index 0000000..65eb854
--- /dev/null
+++ b/guest/hals/keymaster/remote/android.hardware.keymaster@4.0-service.remote.xml
@@ -0,0 +1,7 @@
+<manifest version="1.0" type="device">
+    <hal format="hidl">
+        <name>android.hardware.keymaster</name>
+        <transport>hwbinder</transport>
+        <fqname>@4.0::IKeymasterDevice/default</fqname>
+    </hal>
+</manifest>
diff --git a/guest/hals/keymaster/remote/remote_keymaster.cpp b/guest/hals/keymaster/remote/remote_keymaster.cpp
new file mode 100644
index 0000000..2b3973c
--- /dev/null
+++ b/guest/hals/keymaster/remote/remote_keymaster.cpp
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#include "remote_keymaster.h"
+
+#include <android-base/logging.h>
+#include <keymaster/android_keymaster_messages.h>
+#include <keymaster/keymaster_configuration.h>
+
+namespace keymaster {
+
+RemoteKeymaster::RemoteKeymaster(cvd::KeymasterChannel* channel)
+    : channel_(channel) {}
+
+RemoteKeymaster::~RemoteKeymaster() {
+}
+
+void RemoteKeymaster::ForwardCommand(AndroidKeymasterCommand command, const Serializable& req,
+                                     KeymasterResponse* rsp) {
+    if (!channel_->SendRequest(command, req)) {
+        LOG(ERROR) << "Failed to send keymaster message: " << command;
+        rsp->error = KM_ERROR_UNKNOWN_ERROR;
+        return;
+    }
+    auto response = channel_->ReceiveMessage();
+    if (!response) {
+        LOG(ERROR) << "Failed to receive keymaster response: " << command;
+        rsp->error = KM_ERROR_UNKNOWN_ERROR;
+        return;
+    }
+    const uint8_t* buffer = response->payload;
+    const uint8_t* buffer_end = response->payload + response->payload_size;
+    if (!rsp->Deserialize(&buffer, buffer_end)) {
+        LOG(ERROR) << "Failed to deserialize keymaster response: " << command;
+        rsp->error = KM_ERROR_UNKNOWN_ERROR;
+        return;
+    }
+}
+
+bool RemoteKeymaster::Initialize() {
+
+    ConfigureRequest req;
+    req.os_version = GetOsVersion();
+    req.os_patchlevel = GetOsPatchlevel();
+
+    ConfigureResponse rsp;
+    Configure(req, &rsp);
+
+    if (rsp.error != KM_ERROR_OK) {
+        LOG(ERROR) << "Failed to configure keymaster: " << rsp.error;
+        return false;
+    }
+
+    return true;
+}
+
+void RemoteKeymaster::GetVersion(const GetVersionRequest& request, GetVersionResponse* response) {
+    ForwardCommand(GET_VERSION, request, response);
+}
+
+void RemoteKeymaster::SupportedAlgorithms(const SupportedAlgorithmsRequest& request,
+                                          SupportedAlgorithmsResponse* response) {
+    ForwardCommand(GET_SUPPORTED_ALGORITHMS, request, response);
+}
+
+void RemoteKeymaster::SupportedBlockModes(const SupportedBlockModesRequest& request,
+                                          SupportedBlockModesResponse* response) {
+    ForwardCommand(GET_SUPPORTED_BLOCK_MODES, request, response);
+}
+
+void RemoteKeymaster::SupportedPaddingModes(const SupportedPaddingModesRequest& request,
+                                            SupportedPaddingModesResponse* response) {
+    ForwardCommand(GET_SUPPORTED_PADDING_MODES, request, response);
+}
+
+void RemoteKeymaster::SupportedDigests(const SupportedDigestsRequest& request,
+                                       SupportedDigestsResponse* response) {
+    ForwardCommand(GET_SUPPORTED_DIGESTS, request, response);
+}
+
+void RemoteKeymaster::SupportedImportFormats(const SupportedImportFormatsRequest& request,
+                                             SupportedImportFormatsResponse* response) {
+    ForwardCommand(GET_SUPPORTED_IMPORT_FORMATS, request, response);
+}
+
+void RemoteKeymaster::SupportedExportFormats(const SupportedExportFormatsRequest& request,
+                                             SupportedExportFormatsResponse* response) {
+    ForwardCommand(GET_SUPPORTED_EXPORT_FORMATS, request, response);
+}
+
+void RemoteKeymaster::AddRngEntropy(const AddEntropyRequest& request,
+                                    AddEntropyResponse* response) {
+    ForwardCommand(ADD_RNG_ENTROPY, request, response);
+}
+
+void RemoteKeymaster::Configure(const ConfigureRequest& request, ConfigureResponse* response) {
+    ForwardCommand(CONFIGURE, request, response);
+}
+
+void RemoteKeymaster::GenerateKey(const GenerateKeyRequest& request,
+                                  GenerateKeyResponse* response) {
+    GenerateKeyRequest datedRequest(request.message_version);
+    datedRequest.key_description = request.key_description;
+
+    if (!request.key_description.Contains(TAG_CREATION_DATETIME)) {
+        datedRequest.key_description.push_back(TAG_CREATION_DATETIME, java_time(time(NULL)));
+    }
+
+    ForwardCommand(GENERATE_KEY, datedRequest, response);
+}
+
+void RemoteKeymaster::GetKeyCharacteristics(const GetKeyCharacteristicsRequest& request,
+                                            GetKeyCharacteristicsResponse* response) {
+    ForwardCommand(GET_KEY_CHARACTERISTICS, request, response);
+}
+
+void RemoteKeymaster::ImportKey(const ImportKeyRequest& request, ImportKeyResponse* response) {
+    ForwardCommand(IMPORT_KEY, request, response);
+}
+
+void RemoteKeymaster::ImportWrappedKey(const ImportWrappedKeyRequest& request,
+                                       ImportWrappedKeyResponse* response) {
+    ForwardCommand(IMPORT_WRAPPED_KEY, request, response);
+}
+
+void RemoteKeymaster::ExportKey(const ExportKeyRequest& request, ExportKeyResponse* response) {
+    ForwardCommand(EXPORT_KEY, request, response);
+}
+
+void RemoteKeymaster::AttestKey(const AttestKeyRequest& request, AttestKeyResponse* response) {
+    ForwardCommand(ATTEST_KEY, request, response);
+}
+
+void RemoteKeymaster::UpgradeKey(const UpgradeKeyRequest& request, UpgradeKeyResponse* response) {
+    ForwardCommand(UPGRADE_KEY, request, response);
+}
+
+void RemoteKeymaster::DeleteKey(const DeleteKeyRequest& request, DeleteKeyResponse* response) {
+    ForwardCommand(DELETE_KEY, request, response);
+}
+
+void RemoteKeymaster::DeleteAllKeys(const DeleteAllKeysRequest& request,
+                                    DeleteAllKeysResponse* response) {
+    ForwardCommand(DELETE_ALL_KEYS, request, response);
+}
+
+void RemoteKeymaster::BeginOperation(const BeginOperationRequest& request,
+                                     BeginOperationResponse* response) {
+    ForwardCommand(BEGIN_OPERATION, request, response);
+}
+
+void RemoteKeymaster::UpdateOperation(const UpdateOperationRequest& request,
+                                      UpdateOperationResponse* response) {
+    ForwardCommand(UPDATE_OPERATION, request, response);
+}
+
+void RemoteKeymaster::FinishOperation(const FinishOperationRequest& request,
+                                      FinishOperationResponse* response) {
+    ForwardCommand(FINISH_OPERATION, request, response);
+}
+
+void RemoteKeymaster::AbortOperation(const AbortOperationRequest& request,
+                                     AbortOperationResponse* response) {
+    ForwardCommand(ABORT_OPERATION, request, response);
+}
+
+GetHmacSharingParametersResponse RemoteKeymaster::GetHmacSharingParameters() {
+    // Dummy empty buffer to allow ForwardCommand to have something to serialize
+    Buffer request;
+    GetHmacSharingParametersResponse response;
+    ForwardCommand(GET_HMAC_SHARING_PARAMETERS, request, &response);
+    return response;
+}
+
+ComputeSharedHmacResponse RemoteKeymaster::ComputeSharedHmac(
+        const ComputeSharedHmacRequest& request) {
+    ComputeSharedHmacResponse response;
+    ForwardCommand(COMPUTE_SHARED_HMAC, request, &response);
+    return response;
+}
+
+VerifyAuthorizationResponse RemoteKeymaster::VerifyAuthorization(
+        const VerifyAuthorizationRequest& request) {
+    VerifyAuthorizationResponse response;
+    ForwardCommand(VERIFY_AUTHORIZATION, request, &response);
+    return response;
+}
+
+}  // namespace keymaster
diff --git a/guest/hals/keymaster/remote/remote_keymaster.h b/guest/hals/keymaster/remote/remote_keymaster.h
new file mode 100644
index 0000000..d3f9698
--- /dev/null
+++ b/guest/hals/keymaster/remote/remote_keymaster.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#ifndef REMOTE_KEYMASTER_H_
+#define REMOTE_KEYMASTER_H_
+
+#include <keymaster/android_keymaster_messages.h>
+
+#include "common/libs/security/keymaster_channel.h"
+
+namespace keymaster {
+
+class RemoteKeymaster {
+  private:
+    cvd::KeymasterChannel* channel_;
+
+    void ForwardCommand(
+        AndroidKeymasterCommand command, const Serializable& req, KeymasterResponse* rsp);
+  public:
+    RemoteKeymaster(cvd::KeymasterChannel*);
+    ~RemoteKeymaster();
+    bool Initialize();
+    void GetVersion(const GetVersionRequest& request, GetVersionResponse* response);
+    void SupportedAlgorithms(const SupportedAlgorithmsRequest& request,
+                             SupportedAlgorithmsResponse* response);
+    void SupportedBlockModes(const SupportedBlockModesRequest& request,
+                             SupportedBlockModesResponse* response);
+    void SupportedPaddingModes(const SupportedPaddingModesRequest& request,
+                               SupportedPaddingModesResponse* response);
+    void SupportedDigests(const SupportedDigestsRequest& request,
+                          SupportedDigestsResponse* response);
+    void SupportedImportFormats(const SupportedImportFormatsRequest& request,
+                                SupportedImportFormatsResponse* response);
+    void SupportedExportFormats(const SupportedExportFormatsRequest& request,
+                                SupportedExportFormatsResponse* response);
+    void AddRngEntropy(const AddEntropyRequest& request, AddEntropyResponse* response);
+    void Configure(const ConfigureRequest& request, ConfigureResponse* response);
+    void GenerateKey(const GenerateKeyRequest& request, GenerateKeyResponse* response);
+    void GetKeyCharacteristics(const GetKeyCharacteristicsRequest& request,
+                               GetKeyCharacteristicsResponse* response);
+    void ImportKey(const ImportKeyRequest& request, ImportKeyResponse* response);
+    void ImportWrappedKey(const ImportWrappedKeyRequest& request,
+                          ImportWrappedKeyResponse* response);
+    void ExportKey(const ExportKeyRequest& request, ExportKeyResponse* response);
+    void AttestKey(const AttestKeyRequest& request, AttestKeyResponse* response);
+    void UpgradeKey(const UpgradeKeyRequest& request, UpgradeKeyResponse* response);
+    void DeleteKey(const DeleteKeyRequest& request, DeleteKeyResponse* response);
+    void DeleteAllKeys(const DeleteAllKeysRequest& request, DeleteAllKeysResponse* response);
+    void BeginOperation(const BeginOperationRequest& request, BeginOperationResponse* response);
+    void UpdateOperation(const UpdateOperationRequest& request, UpdateOperationResponse* response);
+    void FinishOperation(const FinishOperationRequest& request, FinishOperationResponse* response);
+    void AbortOperation(const AbortOperationRequest& request, AbortOperationResponse* response);
+    GetHmacSharingParametersResponse GetHmacSharingParameters();
+    ComputeSharedHmacResponse ComputeSharedHmac(const ComputeSharedHmacRequest& request);
+    VerifyAuthorizationResponse VerifyAuthorization(const VerifyAuthorizationRequest& request);
+};
+
+}  // namespace keymaster
+
+#endif  // REMOTE_KEYMASTER_H_
diff --git a/guest/hals/keymaster/remote/remote_keymaster4_device.cpp b/guest/hals/keymaster/remote/remote_keymaster4_device.cpp
new file mode 100644
index 0000000..ceb5231
--- /dev/null
+++ b/guest/hals/keymaster/remote/remote_keymaster4_device.cpp
@@ -0,0 +1,605 @@
+/*
+ **
+ ** Copyright 2018, The Android Open Source Project
+ **
+ ** Licensed under the Apache License, Version 2.0 (the "License");
+ ** you may not use this file except in compliance with the License.
+ ** You may obtain a copy of the License at
+ **
+ **     http://www.apache.org/licenses/LICENSE-2.0
+ **
+ ** Unless required by applicable law or agreed to in writing, software
+ ** distributed under the License is distributed on an "AS IS" BASIS,
+ ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ** See the License for the specific language governing permissions and
+ ** limitations under the License.
+ */
+
+#define LOG_TAG "android.hardware.keymaster@4.0-impl.remote"
+
+#include "remote_keymaster4_device.h"
+
+#include <android/hardware/keymaster/3.0/IKeymasterDevice.h>
+#include <authorization_set.h>
+#include <cutils/log.h>
+#include <keymaster/android_keymaster_messages.h>
+
+using ::keymaster::AbortOperationRequest;
+using ::keymaster::AbortOperationResponse;
+using ::keymaster::AddEntropyRequest;
+using ::keymaster::AddEntropyResponse;
+using ::keymaster::AttestKeyRequest;
+using ::keymaster::AttestKeyResponse;
+using ::keymaster::AuthorizationSet;
+using ::keymaster::BeginOperationRequest;
+using ::keymaster::BeginOperationResponse;
+using ::keymaster::ExportKeyRequest;
+using ::keymaster::ExportKeyResponse;
+using ::keymaster::FinishOperationRequest;
+using ::keymaster::FinishOperationResponse;
+using ::keymaster::GenerateKeyRequest;
+using ::keymaster::GenerateKeyResponse;
+using ::keymaster::GetKeyCharacteristicsRequest;
+using ::keymaster::GetKeyCharacteristicsResponse;
+using ::keymaster::ImportKeyRequest;
+using ::keymaster::ImportKeyResponse;
+using ::keymaster::UpdateOperationRequest;
+using ::keymaster::UpdateOperationResponse;
+using ::keymaster::ng::Tag;
+
+typedef ::android::hardware::keymaster::V3_0::Tag Tag3;
+using ::android::hardware::keymaster::V4_0::Constants;
+
+namespace keymaster {
+namespace V4_0 {
+namespace {
+
+inline keymaster_tag_t legacy_enum_conversion(const Tag value) {
+    return keymaster_tag_t(value);
+}
+inline Tag legacy_enum_conversion(const keymaster_tag_t value) {
+    return Tag(value);
+}
+inline keymaster_purpose_t legacy_enum_conversion(const KeyPurpose value) {
+    return keymaster_purpose_t(value);
+}
+inline keymaster_key_format_t legacy_enum_conversion(const KeyFormat value) {
+    return keymaster_key_format_t(value);
+}
+
+inline SecurityLevel legacy_enum_conversion(const keymaster_security_level_t value) {
+    return static_cast<SecurityLevel>(value);
+}
+
+inline hw_authenticator_type_t legacy_enum_conversion(const HardwareAuthenticatorType value) {
+    return static_cast<hw_authenticator_type_t>(value);
+}
+
+inline ErrorCode legacy_enum_conversion(const keymaster_error_t value) {
+    return ErrorCode(value);
+}
+
+inline keymaster_tag_type_t typeFromTag(const keymaster_tag_t tag) {
+    return keymaster_tag_get_type(tag);
+}
+
+/*
+ * injectAuthToken translates a KM4 authToken into a legacy AUTH_TOKEN tag
+ *
+ * Currently, system/keymaster's reference implementation only accepts this
+ * method for passing an auth token, so until that changes we need to
+ * translate to the old format.
+ */
+inline hidl_vec<KeyParameter> injectAuthToken(const hidl_vec<KeyParameter>& keyParamsBase,
+                                              const HardwareAuthToken& authToken) {
+    std::vector<KeyParameter> keyParams(keyParamsBase);
+    const size_t mac_len = static_cast<size_t>(Constants::AUTH_TOKEN_MAC_LENGTH);
+    /*
+     * mac.size() == 0 indicates no token provided, so we should not copy.
+     * mac.size() != mac_len means it is incompatible with the old
+     *   hw_auth_token_t structure. This is forbidden by spec, but to be safe
+     *   we only copy if mac.size() == mac_len, e.g. there is an authToken
+     *   with a hw_auth_token_t compatible MAC.
+     */
+    if (authToken.mac.size() == mac_len) {
+        KeyParameter p;
+        p.tag = static_cast<Tag>(Tag3::AUTH_TOKEN);
+        p.blob.resize(sizeof(hw_auth_token_t));
+
+        hw_auth_token_t* auth_token = reinterpret_cast<hw_auth_token_t*>(p.blob.data());
+        auth_token->version = 0;
+        auth_token->challenge = authToken.challenge;
+        auth_token->user_id = authToken.userId;
+        auth_token->authenticator_id = authToken.authenticatorId;
+        auth_token->authenticator_type =
+                htobe32(static_cast<uint32_t>(authToken.authenticatorType));
+        auth_token->timestamp = htobe64(authToken.timestamp);
+        static_assert(mac_len == sizeof(auth_token->hmac));
+        memcpy(auth_token->hmac, authToken.mac.data(), mac_len);
+        keyParams.push_back(p);
+    }
+
+    return hidl_vec<KeyParameter>(std::move(keyParams));
+}
+
+class KmParamSet : public keymaster_key_param_set_t {
+  public:
+    KmParamSet(const hidl_vec<KeyParameter>& keyParams) {
+        params = new keymaster_key_param_t[keyParams.size()];
+        length = keyParams.size();
+        for (size_t i = 0; i < keyParams.size(); ++i) {
+            auto tag = legacy_enum_conversion(keyParams[i].tag);
+            switch (typeFromTag(tag)) {
+                case KM_ENUM:
+                case KM_ENUM_REP:
+                    params[i] = keymaster_param_enum(tag, keyParams[i].f.integer);
+                    break;
+                case KM_UINT:
+                case KM_UINT_REP:
+                    params[i] = keymaster_param_int(tag, keyParams[i].f.integer);
+                    break;
+                case KM_ULONG:
+                case KM_ULONG_REP:
+                    params[i] = keymaster_param_long(tag, keyParams[i].f.longInteger);
+                    break;
+                case KM_DATE:
+                    params[i] = keymaster_param_date(tag, keyParams[i].f.dateTime);
+                    break;
+                case KM_BOOL:
+                    if (keyParams[i].f.boolValue)
+                        params[i] = keymaster_param_bool(tag);
+                    else
+                        params[i].tag = KM_TAG_INVALID;
+                    break;
+                case KM_BIGNUM:
+                case KM_BYTES:
+                    params[i] = keymaster_param_blob(tag, &keyParams[i].blob[0],
+                                                     keyParams[i].blob.size());
+                    break;
+                case KM_INVALID:
+                default:
+                    params[i].tag = KM_TAG_INVALID;
+                    /* just skip */
+                    break;
+            }
+        }
+    }
+    KmParamSet(KmParamSet&& other) noexcept
+        : keymaster_key_param_set_t{other.params, other.length} {
+        other.length = 0;
+        other.params = nullptr;
+    }
+    KmParamSet(const KmParamSet&) = delete;
+    ~KmParamSet() { delete[] params; }
+};
+
+inline hidl_vec<uint8_t> kmBlob2hidlVec(const keymaster_key_blob_t& blob) {
+    hidl_vec<uint8_t> result;
+    result.setToExternal(const_cast<unsigned char*>(blob.key_material), blob.key_material_size);
+    return result;
+}
+
+inline hidl_vec<uint8_t> kmBlob2hidlVec(const keymaster_blob_t& blob) {
+    hidl_vec<uint8_t> result;
+    result.setToExternal(const_cast<unsigned char*>(blob.data), blob.data_length);
+    return result;
+}
+
+inline hidl_vec<uint8_t> kmBuffer2hidlVec(const ::keymaster::Buffer& buf) {
+    hidl_vec<uint8_t> result;
+    result.setToExternal(const_cast<unsigned char*>(buf.peek_read()), buf.available_read());
+    return result;
+}
+
+inline static hidl_vec<hidl_vec<uint8_t>> kmCertChain2Hidl(
+        const keymaster_cert_chain_t& cert_chain) {
+    hidl_vec<hidl_vec<uint8_t>> result;
+    if (!cert_chain.entry_count || !cert_chain.entries) return result;
+
+    result.resize(cert_chain.entry_count);
+    for (size_t i = 0; i < cert_chain.entry_count; ++i) {
+        result[i] = kmBlob2hidlVec(cert_chain.entries[i]);
+    }
+
+    return result;
+}
+
+static inline hidl_vec<KeyParameter> kmParamSet2Hidl(const keymaster_key_param_set_t& set) {
+    hidl_vec<KeyParameter> result;
+    if (set.length == 0 || set.params == nullptr) return result;
+
+    result.resize(set.length);
+    keymaster_key_param_t* params = set.params;
+    for (size_t i = 0; i < set.length; ++i) {
+        auto tag = params[i].tag;
+        result[i].tag = legacy_enum_conversion(tag);
+        switch (typeFromTag(tag)) {
+            case KM_ENUM:
+            case KM_ENUM_REP:
+                result[i].f.integer = params[i].enumerated;
+                break;
+            case KM_UINT:
+            case KM_UINT_REP:
+                result[i].f.integer = params[i].integer;
+                break;
+            case KM_ULONG:
+            case KM_ULONG_REP:
+                result[i].f.longInteger = params[i].long_integer;
+                break;
+            case KM_DATE:
+                result[i].f.dateTime = params[i].date_time;
+                break;
+            case KM_BOOL:
+                result[i].f.boolValue = params[i].boolean;
+                break;
+            case KM_BIGNUM:
+            case KM_BYTES:
+                result[i].blob.setToExternal(const_cast<unsigned char*>(params[i].blob.data),
+                                             params[i].blob.data_length);
+                break;
+            case KM_INVALID:
+            default:
+                params[i].tag = KM_TAG_INVALID;
+                /* just skip */
+                break;
+        }
+    }
+    return result;
+}
+
+void addClientAndAppData(const hidl_vec<uint8_t>& clientId, const hidl_vec<uint8_t>& appData,
+                         ::keymaster::AuthorizationSet* params) {
+    params->Clear();
+    if (clientId.size()) {
+        params->push_back(::keymaster::TAG_APPLICATION_ID, clientId.data(), clientId.size());
+    }
+    if (appData.size()) {
+        params->push_back(::keymaster::TAG_APPLICATION_DATA, appData.data(), appData.size());
+    }
+}
+
+}  // anonymous namespace
+
+RemoteKeymaster4Device::RemoteKeymaster4Device(RemoteKeymaster* impl) : impl_(impl) {}
+
+RemoteKeymaster4Device::~RemoteKeymaster4Device() {}
+
+Return<void> RemoteKeymaster4Device::getHardwareInfo(getHardwareInfo_cb _hidl_cb) {
+    _hidl_cb(SecurityLevel::TRUSTED_ENVIRONMENT, "RemoteKeymaster", "Google");
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::getHmacSharingParameters(
+        getHmacSharingParameters_cb _hidl_cb) {
+    const GetHmacSharingParametersResponse response = impl_->GetHmacSharingParameters();
+    // response.params is not the same as the HIDL structure, we need to convert it
+    V4_0::HmacSharingParameters params;
+    params.seed.setToExternal(const_cast<uint8_t*>(response.params.seed.data),
+                              response.params.seed.data_length);
+    static_assert(sizeof(response.params.nonce) == params.nonce.size(), "Nonce sizes don't match");
+    memcpy(params.nonce.data(), response.params.nonce, params.nonce.size());
+    _hidl_cb(legacy_enum_conversion(response.error), params);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::computeSharedHmac(
+        const hidl_vec<HmacSharingParameters>& params, computeSharedHmac_cb _hidl_cb) {
+    ComputeSharedHmacRequest request;
+    request.params_array.params_array = new keymaster::HmacSharingParameters[params.size()];
+    request.params_array.num_params = params.size();
+    for (size_t i = 0; i < params.size(); ++i) {
+        request.params_array.params_array[i].seed = {params[i].seed.data(), params[i].seed.size()};
+        static_assert(sizeof(request.params_array.params_array[i].nonce) ==
+                              decltype(params[i].nonce)::size(),
+                      "Nonce sizes don't match");
+        memcpy(request.params_array.params_array[i].nonce, params[i].nonce.data(),
+               params[i].nonce.size());
+    }
+
+    auto response = impl_->ComputeSharedHmac(request);
+    hidl_vec<uint8_t> sharing_check;
+    if (response.error == KM_ERROR_OK) {
+        sharing_check = kmBlob2hidlVec(response.sharing_check);
+    }
+
+    _hidl_cb(legacy_enum_conversion(response.error), sharing_check);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::verifyAuthorization(
+        uint64_t challenge, const hidl_vec<KeyParameter>& parametersToVerify,
+        const HardwareAuthToken& authToken, verifyAuthorization_cb _hidl_cb) {
+    VerifyAuthorizationRequest request;
+    request.challenge = challenge;
+    request.parameters_to_verify.Reinitialize(KmParamSet(parametersToVerify));
+    request.auth_token.challenge = authToken.challenge;
+    request.auth_token.user_id = authToken.userId;
+    request.auth_token.authenticator_id = authToken.authenticatorId;
+    request.auth_token.authenticator_type = legacy_enum_conversion(authToken.authenticatorType);
+    request.auth_token.timestamp = authToken.timestamp;
+    KeymasterBlob mac(authToken.mac.data(), authToken.mac.size());
+    request.auth_token.mac = mac;
+
+    auto response = impl_->VerifyAuthorization(request);
+
+    ::android::hardware::keymaster::V4_0::VerificationToken token;
+    token.challenge = response.token.challenge;
+    token.timestamp = response.token.timestamp;
+    token.parametersVerified = kmParamSet2Hidl(response.token.parameters_verified);
+    token.securityLevel = legacy_enum_conversion(response.token.security_level);
+    token.mac = kmBlob2hidlVec(response.token.mac);
+
+    _hidl_cb(legacy_enum_conversion(response.error), token);
+
+    return Void();
+}
+
+Return<ErrorCode> RemoteKeymaster4Device::addRngEntropy(const hidl_vec<uint8_t>& data) {
+    if (data.size() == 0) return ErrorCode::OK;
+    AddEntropyRequest request;
+    request.random_data.Reinitialize(data.data(), data.size());
+
+    AddEntropyResponse response;
+    impl_->AddRngEntropy(request, &response);
+
+    return legacy_enum_conversion(response.error);
+}
+
+Return<void> RemoteKeymaster4Device::generateKey(const hidl_vec<KeyParameter>& keyParams,
+                                                 generateKey_cb _hidl_cb) {
+    GenerateKeyRequest request;
+    request.key_description.Reinitialize(KmParamSet(keyParams));
+
+    GenerateKeyResponse response;
+    impl_->GenerateKey(request, &response);
+
+    KeyCharacteristics resultCharacteristics;
+    hidl_vec<uint8_t> resultKeyBlob;
+    if (response.error == KM_ERROR_OK) {
+        resultKeyBlob = kmBlob2hidlVec(response.key_blob);
+        resultCharacteristics.hardwareEnforced = kmParamSet2Hidl(response.enforced);
+        resultCharacteristics.softwareEnforced = kmParamSet2Hidl(response.unenforced);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultKeyBlob, resultCharacteristics);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::getKeyCharacteristics(const hidl_vec<uint8_t>& keyBlob,
+                                                           const hidl_vec<uint8_t>& clientId,
+                                                           const hidl_vec<uint8_t>& appData,
+                                                           getKeyCharacteristics_cb _hidl_cb) {
+    GetKeyCharacteristicsRequest request;
+    request.SetKeyMaterial(keyBlob.data(), keyBlob.size());
+    addClientAndAppData(clientId, appData, &request.additional_params);
+
+    GetKeyCharacteristicsResponse response;
+    impl_->GetKeyCharacteristics(request, &response);
+
+    KeyCharacteristics resultCharacteristics;
+    if (response.error == KM_ERROR_OK) {
+        resultCharacteristics.hardwareEnforced = kmParamSet2Hidl(response.enforced);
+        resultCharacteristics.softwareEnforced = kmParamSet2Hidl(response.unenforced);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultCharacteristics);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::importKey(const hidl_vec<KeyParameter>& params,
+                                               KeyFormat keyFormat,
+                                               const hidl_vec<uint8_t>& keyData,
+                                               importKey_cb _hidl_cb) {
+    ImportKeyRequest request;
+    request.key_description.Reinitialize(KmParamSet(params));
+    request.key_format = legacy_enum_conversion(keyFormat);
+    request.SetKeyMaterial(keyData.data(), keyData.size());
+
+    ImportKeyResponse response;
+    impl_->ImportKey(request, &response);
+
+    KeyCharacteristics resultCharacteristics;
+    hidl_vec<uint8_t> resultKeyBlob;
+    if (response.error == KM_ERROR_OK) {
+        resultKeyBlob = kmBlob2hidlVec(response.key_blob);
+        resultCharacteristics.hardwareEnforced = kmParamSet2Hidl(response.enforced);
+        resultCharacteristics.softwareEnforced = kmParamSet2Hidl(response.unenforced);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultKeyBlob, resultCharacteristics);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::importWrappedKey(
+        const hidl_vec<uint8_t>& wrappedKeyData, const hidl_vec<uint8_t>& wrappingKeyBlob,
+        const hidl_vec<uint8_t>& maskingKey, const hidl_vec<KeyParameter>& unwrappingParams,
+        uint64_t passwordSid, uint64_t biometricSid, importWrappedKey_cb _hidl_cb) {
+    ImportWrappedKeyRequest request;
+    request.SetWrappedMaterial(wrappedKeyData.data(), wrappedKeyData.size());
+    request.SetWrappingMaterial(wrappingKeyBlob.data(), wrappingKeyBlob.size());
+    request.SetMaskingKeyMaterial(maskingKey.data(), maskingKey.size());
+    request.additional_params.Reinitialize(KmParamSet(unwrappingParams));
+    request.password_sid = passwordSid;
+    request.biometric_sid = biometricSid;
+
+    ImportWrappedKeyResponse response;
+    impl_->ImportWrappedKey(request, &response);
+
+    KeyCharacteristics resultCharacteristics;
+    hidl_vec<uint8_t> resultKeyBlob;
+    if (response.error == KM_ERROR_OK) {
+        resultKeyBlob = kmBlob2hidlVec(response.key_blob);
+        resultCharacteristics.hardwareEnforced = kmParamSet2Hidl(response.enforced);
+        resultCharacteristics.softwareEnforced = kmParamSet2Hidl(response.unenforced);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultKeyBlob, resultCharacteristics);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::exportKey(KeyFormat exportFormat,
+                                               const hidl_vec<uint8_t>& keyBlob,
+                                               const hidl_vec<uint8_t>& clientId,
+                                               const hidl_vec<uint8_t>& appData,
+                                               exportKey_cb _hidl_cb) {
+    ExportKeyRequest request;
+    request.key_format = legacy_enum_conversion(exportFormat);
+    request.SetKeyMaterial(keyBlob.data(), keyBlob.size());
+    addClientAndAppData(clientId, appData, &request.additional_params);
+
+    ExportKeyResponse response;
+    impl_->ExportKey(request, &response);
+
+    hidl_vec<uint8_t> resultKeyBlob;
+    if (response.error == KM_ERROR_OK) {
+        resultKeyBlob.setToExternal(response.key_data, response.key_data_length);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultKeyBlob);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::attestKey(const hidl_vec<uint8_t>& keyToAttest,
+                                               const hidl_vec<KeyParameter>& attestParams,
+                                               attestKey_cb _hidl_cb) {
+    AttestKeyRequest request;
+    request.SetKeyMaterial(keyToAttest.data(), keyToAttest.size());
+    request.attest_params.Reinitialize(KmParamSet(attestParams));
+
+    AttestKeyResponse response;
+    impl_->AttestKey(request, &response);
+
+    hidl_vec<hidl_vec<uint8_t>> resultCertChain;
+    if (response.error == KM_ERROR_OK) {
+        resultCertChain = kmCertChain2Hidl(response.certificate_chain);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultCertChain);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::upgradeKey(const hidl_vec<uint8_t>& keyBlobToUpgrade,
+                                                const hidl_vec<KeyParameter>& upgradeParams,
+                                                upgradeKey_cb _hidl_cb) {
+    UpgradeKeyRequest request;
+    request.SetKeyMaterial(keyBlobToUpgrade.data(), keyBlobToUpgrade.size());
+    request.upgrade_params.Reinitialize(KmParamSet(upgradeParams));
+
+    UpgradeKeyResponse response;
+    impl_->UpgradeKey(request, &response);
+
+    if (response.error == KM_ERROR_OK) {
+        _hidl_cb(ErrorCode::OK, kmBlob2hidlVec(response.upgraded_key));
+    } else {
+        _hidl_cb(legacy_enum_conversion(response.error), hidl_vec<uint8_t>());
+    }
+    return Void();
+}
+
+Return<ErrorCode> RemoteKeymaster4Device::deleteKey(const hidl_vec<uint8_t>& keyBlob) {
+    DeleteKeyRequest request;
+    request.SetKeyMaterial(keyBlob.data(), keyBlob.size());
+
+    DeleteKeyResponse response;
+    impl_->DeleteKey(request, &response);
+
+    return legacy_enum_conversion(response.error);
+}
+
+Return<ErrorCode> RemoteKeymaster4Device::deleteAllKeys() {
+    DeleteAllKeysRequest request;
+    DeleteAllKeysResponse response;
+    impl_->DeleteAllKeys(request, &response);
+
+    return legacy_enum_conversion(response.error);
+}
+
+Return<ErrorCode> RemoteKeymaster4Device::destroyAttestationIds() {
+    return ErrorCode::UNIMPLEMENTED;
+}
+
+Return<void> RemoteKeymaster4Device::begin(KeyPurpose purpose, const hidl_vec<uint8_t>& key,
+                                           const hidl_vec<KeyParameter>& inParams,
+                                           const HardwareAuthToken& authToken, begin_cb _hidl_cb) {
+    hidl_vec<KeyParameter> extendedParams = injectAuthToken(inParams, authToken);
+    BeginOperationRequest request;
+    request.purpose = legacy_enum_conversion(purpose);
+    request.SetKeyMaterial(key.data(), key.size());
+    request.additional_params.Reinitialize(KmParamSet(extendedParams));
+
+    BeginOperationResponse response;
+    impl_->BeginOperation(request, &response);
+
+    hidl_vec<KeyParameter> resultParams;
+    if (response.error == KM_ERROR_OK) {
+        resultParams = kmParamSet2Hidl(response.output_params);
+    }
+
+    _hidl_cb(legacy_enum_conversion(response.error), resultParams, response.op_handle);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::update(uint64_t operationHandle,
+                                            const hidl_vec<KeyParameter>& inParams,
+                                            const hidl_vec<uint8_t>& input,
+                                            const HardwareAuthToken& authToken,
+                                            const VerificationToken& verificationToken,
+                                            update_cb _hidl_cb) {
+    (void)verificationToken;
+    UpdateOperationRequest request;
+    UpdateOperationResponse response;
+    hidl_vec<KeyParameter> resultParams;
+    hidl_vec<uint8_t> resultBlob;
+    hidl_vec<KeyParameter> extendedParams = injectAuthToken(inParams, authToken);
+    uint32_t resultConsumed = 0;
+
+    request.op_handle = operationHandle;
+    request.additional_params.Reinitialize(KmParamSet(extendedParams));
+
+    // TODO(schuffelen): Set a buffer size limit.
+    request.input.Reinitialize(input.data(), input.size());
+
+    impl_->UpdateOperation(request, &response);
+
+    if (response.error == KM_ERROR_OK) {
+        resultConsumed = response.input_consumed;
+        resultParams = kmParamSet2Hidl(response.output_params);
+        resultBlob = kmBuffer2hidlVec(response.output);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultConsumed, resultParams, resultBlob);
+    return Void();
+}
+
+Return<void> RemoteKeymaster4Device::finish(uint64_t operationHandle,
+                                            const hidl_vec<KeyParameter>& inParams,
+                                            const hidl_vec<uint8_t>& input,
+                                            const hidl_vec<uint8_t>& signature,
+                                            const HardwareAuthToken& authToken,
+                                            const VerificationToken& verificationToken,
+                                            finish_cb _hidl_cb) {
+    (void)verificationToken;
+    FinishOperationRequest request;
+    hidl_vec<KeyParameter> extendedParams = injectAuthToken(inParams, authToken);
+    request.op_handle = operationHandle;
+    request.input.Reinitialize(input.data(), input.size());
+    request.signature.Reinitialize(signature.data(), signature.size());
+    request.additional_params.Reinitialize(KmParamSet(extendedParams));
+
+    FinishOperationResponse response;
+    impl_->FinishOperation(request, &response);
+
+    hidl_vec<KeyParameter> resultParams;
+    hidl_vec<uint8_t> resultBlob;
+    if (response.error == KM_ERROR_OK) {
+        resultParams = kmParamSet2Hidl(response.output_params);
+        resultBlob = kmBuffer2hidlVec(response.output);
+    }
+    _hidl_cb(legacy_enum_conversion(response.error), resultParams, resultBlob);
+    return Void();
+}
+
+Return<ErrorCode> RemoteKeymaster4Device::abort(uint64_t operationHandle) {
+    AbortOperationRequest request;
+    request.op_handle = operationHandle;
+
+    AbortOperationResponse response;
+    impl_->AbortOperation(request, &response);
+
+    return legacy_enum_conversion(response.error);
+}
+}  // namespace V4_0
+}  // namespace keymaster
diff --git a/guest/hals/keymaster/remote/remote_keymaster4_device.h b/guest/hals/keymaster/remote/remote_keymaster4_device.h
new file mode 100644
index 0000000..5cd12b4
--- /dev/null
+++ b/guest/hals/keymaster/remote/remote_keymaster4_device.h
@@ -0,0 +1,105 @@
+/*
+ **
+ ** Copyright 2017, 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.
+ */
+
+#ifndef keymaster_V4_0_RemoteKeymaster4Device_H_
+#define keymaster_V4_0_RemoteKeymaster4Device_H_
+
+#include <android/hardware/keymaster/4.0/IKeymasterDevice.h>
+#include <hidl/Status.h>
+#include "guest/hals/keymaster/remote/remote_keymaster.h"
+
+namespace keymaster {
+
+namespace V4_0 {
+
+using ::android::sp;
+using ::android::hardware::hidl_vec;
+using ::android::hardware::Return;
+using ::android::hardware::Void;
+using ::android::hardware::keymaster::V4_0::ErrorCode;
+using ::android::hardware::keymaster::V4_0::HardwareAuthenticatorType;
+using ::android::hardware::keymaster::V4_0::HardwareAuthToken;
+using ::android::hardware::keymaster::V4_0::HmacSharingParameters;
+using ::android::hardware::keymaster::V4_0::IKeymasterDevice;
+using ::android::hardware::keymaster::V4_0::KeyCharacteristics;
+using ::android::hardware::keymaster::V4_0::KeyFormat;
+using ::android::hardware::keymaster::V4_0::KeyParameter;
+using ::android::hardware::keymaster::V4_0::KeyPurpose;
+using ::android::hardware::keymaster::V4_0::SecurityLevel;
+using ::android::hardware::keymaster::V4_0::Tag;
+using ::android::hardware::keymaster::V4_0::VerificationToken;
+
+class RemoteKeymaster4Device : public IKeymasterDevice {
+  public:
+    explicit RemoteKeymaster4Device(RemoteKeymaster* impl);
+    virtual ~RemoteKeymaster4Device();
+
+    Return<void> getHardwareInfo(getHardwareInfo_cb _hidl_cb) override;
+    Return<void> getHmacSharingParameters(getHmacSharingParameters_cb _hidl_cb) override;
+    Return<void> computeSharedHmac(const hidl_vec<HmacSharingParameters>& params,
+                                   computeSharedHmac_cb) override;
+    Return<void> verifyAuthorization(uint64_t challenge,
+                                     const hidl_vec<KeyParameter>& parametersToVerify,
+                                     const HardwareAuthToken& authToken,
+                                     verifyAuthorization_cb _hidl_cb) override;
+    Return<ErrorCode> addRngEntropy(const hidl_vec<uint8_t>& data) override;
+    Return<void> generateKey(const hidl_vec<KeyParameter>& keyParams,
+                             generateKey_cb _hidl_cb) override;
+    Return<void> getKeyCharacteristics(const hidl_vec<uint8_t>& keyBlob,
+                                       const hidl_vec<uint8_t>& clientId,
+                                       const hidl_vec<uint8_t>& appData,
+                                       getKeyCharacteristics_cb _hidl_cb) override;
+    Return<void> importKey(const hidl_vec<KeyParameter>& params, KeyFormat keyFormat,
+                           const hidl_vec<uint8_t>& keyData, importKey_cb _hidl_cb) override;
+    Return<void> importWrappedKey(const hidl_vec<uint8_t>& wrappedKeyData,
+                                  const hidl_vec<uint8_t>& wrappingKeyBlob,
+                                  const hidl_vec<uint8_t>& maskingKey,
+                                  const hidl_vec<KeyParameter>& unwrappingParams,
+                                  uint64_t passwordSid, uint64_t biometricSid,
+                                  importWrappedKey_cb _hidl_cb) override;
+    Return<void> exportKey(KeyFormat exportFormat, const hidl_vec<uint8_t>& keyBlob,
+                           const hidl_vec<uint8_t>& clientId, const hidl_vec<uint8_t>& appData,
+                           exportKey_cb _hidl_cb) override;
+    Return<void> attestKey(const hidl_vec<uint8_t>& keyToAttest,
+                           const hidl_vec<KeyParameter>& attestParams,
+                           attestKey_cb _hidl_cb) override;
+    Return<void> upgradeKey(const hidl_vec<uint8_t>& keyBlobToUpgrade,
+                            const hidl_vec<KeyParameter>& upgradeParams,
+                            upgradeKey_cb _hidl_cb) override;
+    Return<ErrorCode> deleteKey(const hidl_vec<uint8_t>& keyBlob) override;
+    Return<ErrorCode> deleteAllKeys() override;
+    Return<ErrorCode> destroyAttestationIds() override;
+    Return<void> begin(KeyPurpose purpose, const hidl_vec<uint8_t>& key,
+                       const hidl_vec<KeyParameter>& inParams, const HardwareAuthToken& authToken,
+                       begin_cb _hidl_cb) override;
+    Return<void> update(uint64_t operationHandle, const hidl_vec<KeyParameter>& inParams,
+                        const hidl_vec<uint8_t>& input, const HardwareAuthToken& authToken,
+                        const VerificationToken& verificationToken, update_cb _hidl_cb) override;
+    Return<void> finish(uint64_t operationHandle, const hidl_vec<KeyParameter>& inParams,
+                        const hidl_vec<uint8_t>& input, const hidl_vec<uint8_t>& signature,
+                        const HardwareAuthToken& authToken,
+                        const VerificationToken& verificationToken, finish_cb _hidl_cb) override;
+    Return<ErrorCode> abort(uint64_t operationHandle) override;
+
+  private:
+    std::unique_ptr<::keymaster::RemoteKeymaster> impl_;
+};
+
+}  // namespace V4_0
+}  // namespace keymaster
+
+#endif  // keymaster_V4_0_RemoteKeymaster4Device_H_
diff --git a/guest/hals/keymaster/remote/service4.cpp b/guest/hals/keymaster/remote/service4.cpp
new file mode 100644
index 0000000..a5281e2
--- /dev/null
+++ b/guest/hals/keymaster/remote/service4.cpp
@@ -0,0 +1,61 @@
+/*
+**
+** Copyright 2018, 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.
+*/
+
+#include <android-base/logging.h>
+#include <android/hardware/keymaster/4.0/IKeymasterDevice.h>
+#include <cutils/properties.h>
+#include <gflags/gflags.h>
+#include <hidl/HidlTransportSupport.h>
+
+#include "common/libs/fs/shared_fd.h"
+#include "common/libs/security/keymaster_channel.h"
+#include <guest/hals/keymaster/remote/remote_keymaster.h>
+#include <guest/hals/keymaster/remote/remote_keymaster4_device.h>
+
+DEFINE_uint32(
+    port,
+    static_cast<uint32_t>(property_get_int64("ro.boot.vsock_keymaster_port", 0)),
+    "virtio socket port to send keymaster commands to");
+
+int main(int argc, char** argv) {
+    ::android::base::InitLogging(argv);
+    gflags::ParseCommandLineFlags(&argc, &argv, true);
+    ::android::hardware::configureRpcThreadpool(1, true);
+
+    auto vsockFd = cvd::SharedFD::VsockClient(2, FLAGS_port, SOCK_STREAM);
+    if (!vsockFd->IsOpen()) {
+        LOG(FATAL) << "Could not connect to keymaster server: "
+                   << vsockFd->StrError();
+    }
+    cvd::KeymasterChannel keymasterChannel(vsockFd);
+    auto remoteKeymaster = new keymaster::RemoteKeymaster(&keymasterChannel);
+
+    if (!remoteKeymaster->Initialize()) {
+      LOG(FATAL) << "Could not initialize keymaster";
+    }
+
+    auto keymaster = new ::keymaster::V4_0::RemoteKeymaster4Device(remoteKeymaster);
+
+    auto status = keymaster->registerAsService();
+    if (status != android::OK) {
+        LOG(FATAL) << "Could not register service for Keymaster 4.0 (" << status << ")";
+        return -1;
+    }
+
+    android::hardware::joinRpcThreadpool();
+    return -1;  // Should never get here.
+}
diff --git a/guest/hals/ril/libril/ril_service.cpp b/guest/hals/ril/libril/ril_service.cpp
index 42e2cae..6e6b755 100644
--- a/guest/hals/ril/libril/ril_service.cpp
+++ b/guest/hals/ril/libril/ril_service.cpp
@@ -3237,12 +3237,13 @@
     return Void();
 }
 
-Return<void> RadioImpl_1_5::deactivateDataCall_1_2(int32_t /* serial */, int32_t /* cid */,
-        ::android::hardware::radio::V1_2::DataRequestReason /* reason */) {
-    // TODO implement
+Return<void> RadioImpl_1_5::deactivateDataCall_1_2(int32_t serial, int32_t cid,
+        ::android::hardware::radio::V1_2::DataRequestReason reason) {
 #if VDBG
-    RLOGE("[%04d]< %s", serial, "Method is not implemented");
+    RLOGD("deactivateDataCall_1_2: serial %d", serial);
 #endif
+    deactivateDataCall(serial, cid,
+            reason == ::android::hardware::radio::V1_2::DataRequestReason::SHUTDOWN);
     return Void();
 }
 
diff --git a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceService.java b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceService.java
index 34f3b1a..8e7bcc5 100644
--- a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceService.java
+++ b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceService.java
@@ -45,12 +45,9 @@
     private static final int NOTIFICATION_ID = 1;
 
     private final JobExecutor mExecutor = new JobExecutor();
-    private final LocationServicesManager mLocationServices = new LocationServicesManager(this);
-    private final PackageVerificationConsentEnforcer mConsentEnforcer = new PackageVerificationConsentEnforcer(this);
     private final BootReporter mBootReporter = new BootReporter();
     private final GceBroadcastReceiver mBroadcastReceiver = new GceBroadcastReceiver();
     private final BluetoothChecker mBluetoothChecker = new BluetoothChecker();
-    private final TombstoneChecker mTombstoneChecker = new TombstoneChecker();
 
     private ConnectivityChecker mConnChecker;
     private GceWifiManager mWifiManager = null;
@@ -69,20 +66,11 @@
             mConnChecker = new ConnectivityChecker(this, mBootReporter);
             mWifiManager = new GceWifiManager(this, mBootReporter, mExecutor);
 
-            mExecutor.schedule(mLocationServices);
-            mExecutor.schedule(mConsentEnforcer);
             mExecutor.schedule(mWifiManager);
             mExecutor.schedule(mBluetoothChecker);
             mExecutor.schedule(mConnChecker);
-            // TODO(ender): TombstoneChecker is disabled, because we no longer have the code that
-            // produces /ts_snap.txt file. We need to rethink how TombstoneChecker should work.
-            // mExecutor.schedule(mTombstoneChecker);
 
-            mExecutor.schedule(mBootReporter,
-                    mLocationServices.getLocationServicesReady(),
-                    mBluetoothChecker.getEnabled()
-                    // mTombstoneChecker.getTombstoneResult()
-                    );
+            mExecutor.schedule(mBootReporter, mBluetoothChecker.getEnabled());
 
             NotificationManager notificationManager =
                     (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
@@ -171,13 +159,9 @@
         }
         pw.println("");
         pw.println("Current system service state:");
-        pw.println("  Location service ready: "
-            + mLocationServices.getLocationServicesReady().isDone());
         pw.println("  Network connected: " + mConnChecker.getConnected().isDone());
         pw.println("  WiFi configured: " + mWifiManager.getWifiReady().isDone());
         pw.println("  Bluetooth enabled: " + mBluetoothChecker.getEnabled().isDone());
-        pw.println("  Tombstone dropped (on boot): "
-            + !mTombstoneChecker.getTombstoneResult().isDone());
         pw.println("");
     }
 }
diff --git a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceWifiManager.java b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceWifiManager.java
index 2747110..1b07c04 100644
--- a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceWifiManager.java
+++ b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/GceWifiManager.java
@@ -77,12 +77,6 @@
         return mMonitorWifiJob.getWifiReady();
     }
 
-
-    /* Modifies Wifi state:
-     * - if wifi disable requested (state == false), simply turns off wifi.
-     * - if wifi enable requested (state == true), turns on wifi and arms the
-     *   connection timeout (see startWifiReconnectionTimeout).
-     */
     private class MonitorWifiJob extends JobBase {
         private final GceFuture<Boolean> mWifiReady =
                 new GceFuture<Boolean>("WIFI Ready");
diff --git a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/LocationServicesManager.java b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/LocationServicesManager.java
deleted file mode 100644
index f9ad388..0000000
--- a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/LocationServicesManager.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright (C) 2017 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.google.gce.gceservice;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.util.Log;
-import com.android.google.gce.gceservice.GceFuture;
-import com.android.google.gce.gceservice.JobBase;
-
-/**
- * Configure Location Services on Android Jellybean.
- * No action on more recent versions of Android.
- */
-class LocationServicesManager extends JobBase {
-    private static final String LOG_TAG = "GceLocationServicesManager";
-    private static final String ACTION_LOCATION_SERVICES_CONSENT_INTENT =
-            "com.google.android.gsf.action.SET_USE_LOCATION_FOR_SERVICES";
-    private static final String EXTRA_LOCATION_SERVICES_CONSENT_DISABLE =
-            "disable";
-    private final Context mContext;
-    private final GceFuture<Boolean> mResult = new GceFuture<Boolean>("Location Services");
-
-
-    LocationServicesManager(Context context) {
-        super(LOG_TAG);
-        mContext = context;
-    }
-
-
-    public int execute() {
-        /* Check if we're running Jellybean.
-         * Sadly, we can't use version name Build.VERSION_CODES.JELLY_BEAN_MR2
-         * because MR1 and MR0 don't know this number.
-         */
-        if (Build.VERSION.SDK_INT <= 18) {
-            Intent intent = new Intent();
-            intent.setAction(ACTION_LOCATION_SERVICES_CONSENT_INTENT);
-            intent.setFlags(intent.getFlags() |
-                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-            intent.putExtra(EXTRA_LOCATION_SERVICES_CONSENT_DISABLE, false);
-            mContext.startActivity(intent);
-        }
-
-        mResult.set(true);
-        return 0;
-    }
-
-
-    public void onDependencyFailed(Exception e) {
-        Log.e(LOG_TAG, "Could not configure LocationServices.", e);
-        mResult.set(e);
-    }
-
-
-    public GceFuture<Boolean> getLocationServicesReady() {
-        return mResult;
-    }
-}
diff --git a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/PackageVerificationConsentEnforcer.java b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/PackageVerificationConsentEnforcer.java
deleted file mode 100644
index e45d656..0000000
--- a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/PackageVerificationConsentEnforcer.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright (C) 2017 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.google.gce.gceservice;
-
-import android.content.ContentResolver;
-import android.content.Context;
-import android.provider.Settings;
-import android.provider.Settings.SettingNotFoundException;
-import android.util.Log;
-
-/**
- * Forces pacakge verification to be off on N and N-MR1 by adjusting package_verifier_user_consent.
- *
- * This is needed because CVDs don't have a touch screen, and the consent
- * dialog will block apk installs.
- *
- * Possible values for consent seem to be:
- *   -1 The user refused
- *    0 Ask the user
- *    1 The user accepted
- *
- * This code polls because Android may overwrite a non-zero value with a 0
- * at some point after boot completes. However, this happens only on some
- * boots, so it can't be a blocker for boot complete.
- */
-class PackageVerificationConsentEnforcer extends JobBase {
-    private static final String LOG_TAG = "GcePVCR";
-    private static final String PACKAGE_VERIFIER_USER_CONSENT = "package_verifier_user_consent";
-    private final Context mContext;
-
-    // Chosen to avoid the possible values (see top comment).
-    private int mLastObservedValue = -2;
-
-
-    public PackageVerificationConsentEnforcer(Context context) {
-        super(LOG_TAG);
-        mContext = context;
-    }
-
-
-    public int execute() {
-        if (android.os.Build.VERSION.SDK_INT < 24) {
-            // Skip older android versions.
-            return 0;
-        }
-
-        try {
-            ContentResolver contentResolver = mContext.getContentResolver();
-            int value = Settings.Secure.getInt(contentResolver, PACKAGE_VERIFIER_USER_CONSENT);
-            if (value != mLastObservedValue) {
-                mLastObservedValue = value;
-            }
-
-            if (value == 0) {
-                Settings.Secure.putInt(mContext.getContentResolver(), PACKAGE_VERIFIER_USER_CONSENT, -1);
-            }
-        } catch (SettingNotFoundException e) {
-        }
-
-        return 1;
-    }
-
-
-    public void onDependencyFailed(Exception e) {
-        Log.e(LOG_TAG, "Could not start Consent Enforcer.", e);
-    }
-}
-
-
diff --git a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/TombstoneChecker.java b/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/TombstoneChecker.java
deleted file mode 100644
index dec0837..0000000
--- a/guest/monitoring/cuttlefish_service/java/com/android/google/gce/gceservice/TombstoneChecker.java
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- * Copyright (C) 2017 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.google.gce.gceservice;
-
-import android.util.Log;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Scanner;
-
-/** A job that checks for any new tombstones before reporting VIRTUAL_DEVICE_BOOT_COMPLETED.
- *
- */
-public class TombstoneChecker extends JobBase {
-    private static final String LOG_TAG = "GceTombstoneChecker";
-    private static final String sSnapshotDir = "/data/tombstones";
-    private static final String sSnapshotFile = "/ts_snap.txt";
-    private static final String sTsExceptionMessage = "GceTombstoneChecker internal error. ";
-    private static final String sTsFilePrefix = "tombstone";
-    private final GceFuture<Boolean> mPassed = new GceFuture<Boolean>("GceTombstoneChecker");
-    private ArrayList<Record> mPreBootRecords = new ArrayList<Record>();
-    private ArrayList<Record> mPostBootRecords = new ArrayList<Record>();
-
-    public TombstoneChecker() {
-        super(LOG_TAG);
-    }
-
-
-    @Override
-    public int execute() {
-        if (mPassed.isDone()) {
-            return 0;
-        }
-
-        try {
-            readPreBootSnapshot();
-            capturePostBootSnapshot();
-            if (seenNewTombstones()) {
-                Log.e(LOG_TAG, "Tombstones created during boot. ");
-                for (int i = 0; i < mPostBootRecords.size(); i++) {
-                    Log.i(LOG_TAG, mPostBootRecords.get(i).getFileName());
-                }
-                mPassed.set(new Exception("Tombstones created. "));
-            } else {
-                mPassed.set(true);
-            }
-        } catch(Exception e) {
-            Log.e(LOG_TAG, sTsExceptionMessage + e);
-            mPassed.set(new Exception(sTsExceptionMessage, e));
-        }
-
-        return 0;
-    }
-
-    @Override
-    public void onDependencyFailed(Exception e) {
-        mPassed.set(e);
-    }
-
-    public GceFuture<Boolean> getTombstoneResult() {
-        return mPassed;
-    }
-
-    private void capturePostBootSnapshot() throws Exception {
-        File dir = new File(sSnapshotDir);
-        File[] files = dir.listFiles();
-
-        // In K & L, /data/tombstones directory is not created during boot. So
-        // dir.listFiles() can return null.
-        if (files == null) {
-            return;
-        }
-
-        for (int i = 0; i < files.length; i++) {
-            if (files[i].isFile() && files[i].getName().startsWith(sTsFilePrefix)) {
-                long ctime = files[i].lastModified() / 1000;
-                mPostBootRecords.add(new Record(files[i].getName(), ctime));
-            }
-        }
-        Collections.sort(mPostBootRecords);
-
-        return;
-    }
-
-    private void readPreBootSnapshot() throws Exception {
-        File file = new File(sSnapshotFile);
-        if (!file.isFile()) {
-            throw new FileNotFoundException(sSnapshotFile);
-        }
-
-        Scanner scanner = new Scanner(file);
-        while (scanner.hasNext()) {
-            String[] fields = scanner.nextLine().split(" ");
-            mPreBootRecords.add(new Record(fields[0], Long.parseLong(fields[1])));
-        }
-        Collections.sort(mPreBootRecords);
-
-        return;
-    }
-
-    private boolean seenNewTombstones() {
-        return !isEqual(mPreBootRecords, mPostBootRecords);
-    }
-
-    private boolean isEqual(ArrayList<Record> preBoot, ArrayList<Record> postBoot) {
-        postBoot.removeAll(preBoot);
-        if (postBoot.size() != 0) {
-            return false;
-        }
-
-        return true;
-    }
-
-    private class Record implements Comparable<Record> {
-        private String mFilename;
-        private long mCtime;
-
-        public Record(String filename, long ctime) {
-            this.mFilename = filename;
-            this.mCtime = ctime;
-        }
-
-        public String getFileName() {
-            return mFilename;
-        }
-
-        public int compareTo(Record r) {
-            if (this == r) {
-                return 0;
-            }
-
-            return (mFilename.compareTo(r.mFilename));
-        }
-
-        public boolean equals(Object o) {
-            if (o == null) {
-                return false;
-            }
-
-            if (this == o) {
-                return true;
-            }
-
-            Record r = (Record) o;
-            return (mFilename.equals(r.mFilename) && (mCtime == r.mCtime));
-        }
-
-        public String toString() {
-            StringBuilder sb = new StringBuilder();
-            sb.append(mFilename).append(" ").append(String.valueOf(mCtime));
-            return sb.toString();
-        }
-    }
-}
diff --git a/host/commands/assemble_cvd/Android.bp b/host/commands/assemble_cvd/Android.bp
index 08bfb78..d19d3e4 100644
--- a/host/commands/assemble_cvd/Android.bp
+++ b/host/commands/assemble_cvd/Android.bp
@@ -32,6 +32,7 @@
     name: "assemble_cvd",
     srcs: [
         "assemble_cvd.cc",
+        "boot_config.cc",
         "boot_image_unpacker.cc",
         "data_image.cc",
         "flags.cc",
diff --git a/host/commands/assemble_cvd/boot_config.cc b/host/commands/assemble_cvd/boot_config.cc
new file mode 100644
index 0000000..181a111
--- /dev/null
+++ b/host/commands/assemble_cvd/boot_config.cc
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include "host/commands/assemble_cvd/boot_config.h"
+
+#include <fstream>
+#include <sstream>
+#include <string>
+
+#include <sys/stat.h>
+
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+
+#include "common/libs/utils/files.h"
+#include "common/libs/utils/subprocess.h"
+#include "host/libs/config/cuttlefish_config.h"
+#include "host/libs/config/kernel_args.h"
+
+namespace {
+
+size_t WriteEnvironment(const vsoc::CuttlefishConfig& config,
+                        const std::string& env_path) {
+  std::ostringstream env;
+  auto kernel_args = KernelCommandLineFromConfig(config);
+  env << "bootargs=" << android::base::Join(kernel_args, " ") << '\0';
+  if (!config.boot_slot().empty()) {
+      env << "android_slot_suffix=_" << config.boot_slot() << '\0';
+  }
+  env << "bootdevice=0:1" << '\0';
+  env << "bootdelay=0" << '\0';
+  env << "bootcmd=boot_android virtio -" << '\0';
+  env << '\0';
+  std::string env_str = env.str();
+  std::ofstream file_out(env_path.c_str(), std::ios::binary);
+  file_out << env_str;
+
+  if(!file_out.good()) {
+    return 0;
+  }
+
+  return env_str.length();
+}
+
+}  // namespace
+
+
+bool InitBootloaderEnvPartition(const vsoc::CuttlefishConfig& config,
+                                const std::string& boot_env_image_path) {
+  auto tmp_boot_env_image_path = boot_env_image_path + ".tmp";
+  auto uboot_env_path = config.AssemblyPath("u-boot.env");
+  if(!WriteEnvironment(config, uboot_env_path)) {
+    LOG(ERROR) << "Unable to write out plaintext env '" << uboot_env_path << ".'";
+    return false;
+  }
+
+  auto mkimage_path = vsoc::DefaultHostArtifactsPath("bin/mkenvimage");
+  cvd::Command cmd(mkimage_path);
+  cmd.AddParameter("-s");
+  cmd.AddParameter("4096");
+  cmd.AddParameter("-o");
+  cmd.AddParameter(tmp_boot_env_image_path);
+  cmd.AddParameter(uboot_env_path);
+  int success = cmd.Start().Wait();
+  if (success != 0) {
+    LOG(ERROR) << "Unable to run mkenvimage. Exited with status " << success;
+    return false;
+  }
+
+  if(!cvd::FileExists(boot_env_image_path) || cvd::ReadFile(boot_env_image_path) != cvd::ReadFile(tmp_boot_env_image_path)) {
+    if(!cvd::RenameFile(tmp_boot_env_image_path, boot_env_image_path)) {
+      LOG(ERROR) << "Unable to delete the old env image.";
+      return false;
+    }
+    LOG(INFO) << "Updated bootloader environment image.";
+  } else {
+    cvd::RemoveFile(tmp_boot_env_image_path);
+  }
+
+  return true;
+}
diff --git a/host/commands/assemble_cvd/boot_config.h b/host/commands/assemble_cvd/boot_config.h
new file mode 100644
index 0000000..a7b6a12
--- /dev/null
+++ b/host/commands/assemble_cvd/boot_config.h
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+#pragma once
+
+#include <string>
+#include <host/libs/config/cuttlefish_config.h>
+
+bool InitBootloaderEnvPartition(const vsoc::CuttlefishConfig& config,
+                                const std::string& boot_env_image_path);
diff --git a/host/commands/assemble_cvd/data_image.cc b/host/commands/assemble_cvd/data_image.cc
index b1c371d..bbda55d 100644
--- a/host/commands/assemble_cvd/data_image.cc
+++ b/host/commands/assemble_cvd/data_image.cc
@@ -2,9 +2,13 @@
 
 #include <android-base/logging.h>
 
+#include "common/libs/fs/shared_buf.h"
+
 #include "common/libs/utils/files.h"
 #include "common/libs/utils/subprocess.h"
 
+#include "host/commands/assemble_cvd/mbr.h"
+
 namespace {
 const std::string kDataPolicyUseExisting = "use_existing";
 const std::string kDataPolicyCreateIfMissing = "create_if_missing";
@@ -36,11 +40,10 @@
     return true;
   } else {
     off_t raw_target = static_cast<off_t>(data_image_mb) << 20;
-    int truncate_status =
-        cvd::SharedFD::Open(data_image, O_RDWR)->Truncate(raw_target);
-    if (truncate_status != 0) {
+    auto fd = cvd::SharedFD::Open(data_image, O_RDWR);
+    if (fd->Truncate(raw_target) != 0) {
       LOG(ERROR) << "`truncate --size=" << data_image_mb << "M "
-                  << data_image << "` failed with code " << truncate_status;
+                  << data_image << "` failed:" << fd->StrError();
       return false;
     }
     bool fsck_success = ForceFsckImage(data_image);
@@ -64,20 +67,58 @@
 } // namespace
 
 void CreateBlankImage(
-    const std::string& image, int block_count, const std::string& image_fmt,
-    const std::string& block_size) {
+    const std::string& image, int num_mb, const std::string& image_fmt) {
   LOG(INFO) << "Creating " << image;
-  std::string of = "of=";
-  of += image;
-  std::string count = "count=";
-  count += std::to_string(block_count);
-  std::string bs = "bs=" + block_size;
-  cvd::execute({"/bin/dd", "if=/dev/zero", of, bs, count});
+
+  off_t image_size_bytes = static_cast<off_t>(num_mb) << 20;
+  // The newfs_msdos tool with the mandatory -C option will do the same
+  // as below to zero the image file, so we don't need to do it here
+  if (image_fmt != "sdcard") {
+    auto fd = cvd::SharedFD::Open(image, O_CREAT | O_TRUNC | O_RDWR, 0666);
+    if (fd->Truncate(image_size_bytes) != 0) {
+      LOG(ERROR) << "`truncate --size=" << num_mb << "M " << image
+                 << "` failed:" << fd->StrError();
+      return;
+    }
+  }
+
   if (image_fmt == "ext4") {
     cvd::execute({"/sbin/mkfs.ext4", image});
-  } else if (image_fmt != "none") {
+  } else if (image_fmt == "f2fs") {
     auto make_f2fs_path = vsoc::DefaultHostArtifactsPath("bin/make_f2fs");
     cvd::execute({make_f2fs_path, "-t", image_fmt, image, "-g", "android"});
+  } else if (image_fmt == "sdcard") {
+    // Reserve 1MB in the image for the MBR and padding, to simulate what
+    // other OSes do by default when partitioning a drive
+    off_t offset_size_bytes = 1 << 20;
+    image_size_bytes -= offset_size_bytes;
+    off_t image_size_sectors = image_size_bytes / 512;
+    auto newfs_msdos_path = vsoc::DefaultHostArtifactsPath("bin/newfs_msdos");
+    cvd::execute({newfs_msdos_path, "-F", "32", "-m", "0xf8", "-a", "4088",
+                                    "-o", "0",  "-c", "8",    "-h", "255",
+                                    "-u", "63", "-S", "512",
+                                    "-s", std::to_string(image_size_sectors),
+                                    "-C", std::to_string(num_mb) + "M",
+                                    "-@", std::to_string(offset_size_bytes),
+                                    image});
+    // Write the MBR after the filesystem is formatted, as the formatting tools
+    // don't consistently preserve the image contents
+    MasterBootRecord mbr = {
+      .partitions = {{
+        .partition_type = 0xC,
+        .first_lba = (std::uint32_t) offset_size_bytes / SECTOR_SIZE,
+        .num_sectors = (std::uint32_t) image_size_bytes / SECTOR_SIZE,
+      }},
+      .boot_signature = { 0x55, 0xAA },
+    };
+    auto fd = cvd::SharedFD::Open(image, O_RDWR);
+    if (cvd::WriteAllBinary(fd, &mbr) != sizeof(MasterBootRecord)) {
+      LOG(ERROR) << "Writing MBR to " << image << " failed:" << fd->StrError();
+      return;
+    }
+  } else if (image_fmt != "none") {
+    LOG(WARNING) << "Unknown image format '" << image_fmt
+                 << "' for " << image << ", treating as 'none'.";
   }
 }
 
diff --git a/host/commands/assemble_cvd/data_image.h b/host/commands/assemble_cvd/data_image.h
index e83b4ee..e4afdf8 100644
--- a/host/commands/assemble_cvd/data_image.h
+++ b/host/commands/assemble_cvd/data_image.h
@@ -14,5 +14,4 @@
                                      const std::string& path);
 bool InitializeMiscImage(const std::string& misc_image);
 void CreateBlankImage(
-    const std::string& image, int block_count, const std::string& image_fmt,
-    const std::string& block_size = "1M");
+    const std::string& image, int num_mb, const std::string& image_fmt);
diff --git a/host/commands/assemble_cvd/flags.cc b/host/commands/assemble_cvd/flags.cc
index 6df46ef..76e97c3 100644
--- a/host/commands/assemble_cvd/flags.cc
+++ b/host/commands/assemble_cvd/flags.cc
@@ -17,6 +17,7 @@
 
 #include "common/libs/utils/environment.h"
 #include "common/libs/utils/files.h"
+#include "host/commands/assemble_cvd/boot_config.h"
 #include "host/commands/assemble_cvd/boot_image_unpacker.h"
 #include "host/commands/assemble_cvd/data_image.h"
 #include "host/commands/assemble_cvd/image_aggregator.h"
@@ -27,6 +28,9 @@
 #include "host/libs/vm_manager/qemu_manager.h"
 #include "host/libs/vm_manager/vm_manager.h"
 
+// Taken from external/avb/libavb/avb_slot_verify.c; this define is not in the headers
+#define VBMETA_MAX_SIZE 65536ul
+
 using vsoc::ForCurrentInstance;
 using cvd::AssemblerExitCodes;
 
@@ -35,6 +39,8 @@
               "to be generated.");
 DEFINE_int32(blank_metadata_image_mb, 16,
              "The size of the blank metadata image to generate, MB.");
+DEFINE_int32(blank_sdcard_image_mb, 2048,
+             "The size of the blank sdcard image to generate, MB.");
 DEFINE_int32(cpus, 2, "Virtual CPU count.");
 DEFINE_string(data_image, "", "Location of the data partition image.");
 DEFINE_string(data_policy, "use_existing", "How to handle userdata partition."
@@ -71,6 +77,12 @@
 DEFINE_string(vendor_boot_image, "",
               "Location of cuttlefish vendor boot image. If empty it is assumed to "
               "be vendor_boot.img in the directory specified by -system_image_dir.");
+DEFINE_string(vbmeta_image, "",
+              "Location of cuttlefish vbmeta image. If empty it is assumed to "
+              "be vbmeta.img in the directory specified by -system_image_dir.");
+DEFINE_string(vbmeta_system_image, "",
+              "Location of cuttlefish vbmeta_system image. If empty it is assumed to "
+              "be vbmeta_system.img in the directory specified by -system_image_dir.");
 DEFINE_int32(memory_mb, 2048,
              "Total amount of memory available for guest, MB.");
 DEFINE_string(serial_number, ForCurrentInstance("CUTTLEFISHCVD"),
@@ -94,6 +106,9 @@
 DEFINE_string(misc_image, "",
               "Location of the misc partition image. If the image does not "
               "exist, a blank new misc partition image is created.");
+DEFINE_string(boot_env_image, "",
+              "Location of the boot environment image. If the image does not "
+              "exist, a default boot environment image is created.");
 
 DEFINE_bool(deprecated_boot_completed, false, "Log boot completed message to"
             " host kernel. This is only used during transition of our clients."
@@ -129,7 +144,7 @@
               vsoc::DefaultHostArtifactsPath(kSeccompDir),
               "With sandbox'ed crosvm, overrieds the security comp policy directory");
 
-DEFINE_bool(start_webrtc, false, "[Experimental] Whether to start the webrtc process.");
+DEFINE_bool(start_webrtc, false, "Whether to start the webrtc process.");
 
 DEFINE_string(
         webrtc_assets_dir,
@@ -151,6 +166,35 @@
         false,
         "[Experimental] If enabled, exposes local adb service through a websocket.");
 
+DEFINE_bool(
+    start_webrtc_sig_server, false,
+    "Whether to start the webrtc signaling server. This option only applies to "
+    "the first instance, if multiple instances are launched they'll share the "
+    "same signaling server, which is owned by the first one.");
+
+DEFINE_string(webrtc_sig_server_addr, "127.0.0.1",
+              "The address of the webrtc signaling server.");
+
+DEFINE_int32(
+    webrtc_sig_server_port, 443,
+    "The port of the signaling server if started outside of this launch. If "
+    "-start_webrtc_sig_server is given it will choose 8443+instance_num1-1 and "
+    "this parameter is ignored.");
+
+DEFINE_string(webrtc_sig_server_path, "/register_device",
+              "The path section of the URL where the device should be "
+              "registered with the signaling server.");
+
+DEFINE_bool(verify_sig_server_certificate, false,
+            "Whether to verify the signaling server's certificate with a "
+            "trusted signing authority (Disallow self signed certificates).");
+
+DEFINE_string(
+    webrtc_device_id, "cvd-{num}",
+    "The for the device to register with the signaling server. Every "
+    "appearance of the substring '{num}' in the device id will be substituted "
+    "with the instance number to support multiple instances");
+
 DEFINE_string(adb_mode, "vsock_half_tunnel",
               "Mode for ADB connection."
               "'vsock_tunnel' for a TCP connection tunneled through vsock, "
@@ -179,8 +223,7 @@
 DEFINE_string(crosvm_binary,
               vsoc::DefaultHostArtifactsPath("bin/crosvm"),
               "The Crosvm binary to use");
-DEFINE_string(tpm_binary,
-              vsoc::DefaultHostArtifactsPath("bin/ms-tpm-20-ref"),
+DEFINE_string(tpm_binary, "",
               "The TPM simulator to use. Disabled if empty.");
 DEFINE_string(tpm_device, "", "A host TPM device to pass through commands to.");
 DEFINE_bool(restart_subprocesses, true, "Restart any crashed host process");
@@ -202,6 +245,7 @@
                           "images have been updated since the first launch.");
 DEFINE_string(report_anonymous_usage_stats, "", "Report anonymous usage "
             "statistics for metrics collection and analysis.");
+DEFINE_string(ril_dns, "8.8.8.8", "DNS address of mobile network (RIL)");
 
 namespace {
 
@@ -240,6 +284,17 @@
   SetCommandLineOptionWithMode("vendor_boot_image",
                                default_vendor_boot_image.c_str(),
                                google::FlagSettingMode::SET_FLAGS_DEFAULT);
+  std::string default_boot_env_image = FLAGS_system_image_dir + "/env.img";
+  SetCommandLineOptionWithMode("boot_env_image", default_boot_env_image.c_str(),
+                               google::FlagSettingMode::SET_FLAGS_DEFAULT);
+  std::string default_vbmeta_image = FLAGS_system_image_dir + "/vbmeta.img";
+  SetCommandLineOptionWithMode("vbmeta_image", default_vbmeta_image.c_str(),
+                               google::FlagSettingMode::SET_FLAGS_DEFAULT);
+  std::string default_vbmeta_system_image = FLAGS_system_image_dir
+                                          + "/vbmeta_system.img";
+  SetCommandLineOptionWithMode("vbmeta_system_image",
+                               default_vbmeta_system_image.c_str(),
+                               google::FlagSettingMode::SET_FLAGS_DEFAULT);
 
   return true;
 }
@@ -308,8 +363,9 @@
         tmp_config_obj.AssemblyPath(kKernelDefaultPath.c_str()));
     tmp_config_obj.set_use_unpacked_kernel(true);
   }
+
   tmp_config_obj.set_decompress_kernel(FLAGS_decompress_kernel);
-  if (FLAGS_decompress_kernel) {
+  if (tmp_config_obj.decompress_kernel()) {
     tmp_config_obj.set_decompressed_kernel_image_path(
         tmp_config_obj.AssemblyPath("vmlinux"));
   }
@@ -375,6 +431,13 @@
   tmp_config_obj.set_webrtc_assets_dir(FLAGS_webrtc_assets_dir);
   tmp_config_obj.set_webrtc_public_ip(FLAGS_webrtc_public_ip);
   tmp_config_obj.set_webrtc_certs_dir(FLAGS_webrtc_certs_dir);
+  tmp_config_obj.set_sig_server_binary(
+      vsoc::DefaultHostArtifactsPath("bin/webrtc_sig_server"));
+  // Note: This will be overridden if the sig server is started by us
+  tmp_config_obj.set_sig_server_port(FLAGS_webrtc_sig_server_port);
+  tmp_config_obj.set_sig_server_address(FLAGS_webrtc_sig_server_addr);
+  tmp_config_obj.set_sig_server_path(FLAGS_webrtc_sig_server_path);
+  tmp_config_obj.set_sig_server_strict(FLAGS_verify_sig_server_certificate);
 
   tmp_config_obj.set_webrtc_enable_adb_websocket(
           FLAGS_webrtc_enable_adb_websocket);
@@ -410,11 +473,14 @@
 
   tmp_config_obj.set_cuttlefish_env_path(GetCuttlefishEnvPath());
 
+  tmp_config_obj.set_ril_dns(FLAGS_ril_dns);
+
   std::vector<int> instance_nums;
   for (int i = 0; i < FLAGS_num_instances; i++) {
     instance_nums.push_back(vsoc::GetInstance() + i);
   }
 
+  bool is_first_instance = true;
   for (const auto& num : instance_nums) {
     auto instance = tmp_config_obj.ForInstance(num);
     auto const_instance = const_cast<const vsoc::CuttlefishConfig&>(tmp_config_obj)
@@ -449,10 +515,37 @@
       instance.set_keyboard_server_port(7000 + num - 1);
       instance.set_touch_server_port(7100 + num - 1);
     }
+    instance.set_keymaster_vsock_port(7200 + num - 1);
 
     instance.set_device_title(FLAGS_device_title);
 
-    instance.set_virtual_disk_paths({const_instance.PerInstancePath("overlay.img")});
+    instance.set_virtual_disk_paths({
+      const_instance.PerInstancePath("overlay.img"),
+      const_instance.sdcard_path(),
+    });
+
+    instance.set_start_webrtc_signaling_server(false);
+
+    if (FLAGS_webrtc_device_id.empty()) {
+      // Use the instance's name as a default
+      instance.set_webrtc_device_id(const_instance.instance_name());
+    } else {
+      std::string device_id = FLAGS_webrtc_device_id;
+      size_t pos;
+      while ((pos = device_id.find("{num}")) != std::string::npos) {
+        device_id.replace(pos, strlen("{num}"), std::to_string(num));
+      }
+      instance.set_webrtc_device_id(device_id);
+    }
+    if (FLAGS_start_webrtc_sig_server && is_first_instance) {
+      auto port = 8443 + num - 1;
+      // Change the signaling server port for all instances
+      tmp_config_obj.set_sig_server_port(port);
+      instance.set_start_webrtc_signaling_server(true);
+    } else {
+      instance.set_start_webrtc_signaling_server(false);
+    }
+    is_first_instance = false;
   }
 
   return tmp_config_obj;
@@ -492,7 +585,7 @@
                                google::FlagSettingMode::SET_FLAGS_DEFAULT);
   // for now, we support only x86_64 by default
   bool default_enable_sandbox = false;
-  std::set<const std::string> supported_archs{std::string("x86_64"), std::string("aarch64")};
+  std::set<const std::string> supported_archs{std::string("x86_64")};
   if (supported_archs.find(cvd::HostArch()) != supported_archs.end()) {
     default_enable_sandbox =
         [](const std::string& var_empty) -> bool {
@@ -515,6 +608,16 @@
   SetCommandLineOptionWithMode("enable_sandbox",
                                (default_enable_sandbox ? "true" : "false"),
                                google::FlagSettingMode::SET_FLAGS_DEFAULT);
+
+  // Crosvm requires a specific setting for kernel decompression; it must be
+  // on for aarch64 and off for x86, no other mode is supported.
+  bool decompress_kernel = false;
+  if (cvd::HostArch() == "aarch64") {
+    decompress_kernel = true;
+  }
+  SetCommandLineOptionWithMode("decompress_kernel",
+                               (decompress_kernel ? "true" : "false"),
+                               google::FlagSettingMode::SET_FLAGS_DEFAULT);
 }
 
 bool ParseCommandLineFlags(int* argc, char*** argv) {
@@ -536,6 +639,17 @@
     SetCommandLineOptionWithMode("start_vnc_server", "true",
                                  google::FlagSettingMode::SET_FLAGS_DEFAULT);
   }
+  // Various temporary workarounds for aarch64
+  if (cvd::HostArch() == "aarch64") {
+    SetCommandLineOptionWithMode("tpm_binary",
+                                 "",
+                                 google::FlagSettingMode::SET_FLAGS_DEFAULT);
+  }
+  // The default for starting signaling server is whether or not webrt is to be
+  // started.
+  SetCommandLineOptionWithMode("start_webrtc_sig_server",
+                               FLAGS_start_webrtc ? "true" : "false",
+                               google::FlagSettingMode::SET_FLAGS_DEFAULT);
   google::HandleCommandLineHelpFlags();
   if (invalid_manager) {
     return false;
@@ -678,6 +792,50 @@
 
 std::vector<ImagePartition> disk_config() {
   std::vector<ImagePartition> partitions;
+
+  // Note that if the positions of env or misc change, the environment for
+  // u-boot must be updated as well (see boot_config.cc and
+  // configs/cf-x86_defconfig in external/u-boot).
+  partitions.push_back(ImagePartition {
+    .label = "env",
+    .image_file_path = FLAGS_boot_env_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "misc",
+    .image_file_path = FLAGS_misc_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "boot_a",
+    .image_file_path = FLAGS_boot_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "boot_b",
+    .image_file_path = FLAGS_boot_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "vendor_boot_a",
+    .image_file_path = FLAGS_vendor_boot_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "vendor_boot_b",
+    .image_file_path = FLAGS_vendor_boot_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "vbmeta_a",
+    .image_file_path = FLAGS_vbmeta_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "vbmeta_b",
+    .image_file_path = FLAGS_vbmeta_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "vbmeta_system_a",
+    .image_file_path = FLAGS_vbmeta_system_image,
+  });
+  partitions.push_back(ImagePartition {
+    .label = "vbmeta_system_b",
+    .image_file_path = FLAGS_vbmeta_system_image,
+  });
   partitions.push_back(ImagePartition {
     .label = "super",
     .image_file_path = FLAGS_super_image,
@@ -694,14 +852,6 @@
     .label = "metadata",
     .image_file_path = FLAGS_metadata_image,
   });
-  partitions.push_back(ImagePartition {
-    .label = "boot",
-    .image_file_path = FLAGS_boot_image,
-  });
-  partitions.push_back(ImagePartition {
-    .label = "misc",
-    .image_file_path = FLAGS_misc_image
-  });
   return partitions;
 }
 
@@ -816,6 +966,7 @@
       preserving.insert("gpt_header.img");
       preserving.insert("gpt_footer.img");
       preserving.insert("composite.img");
+      preserving.insert("sdcard.img");
       preserving.insert("access-kregistry");
       preserving.insert("disk_hole");
       preserving.insert("NVChip");
@@ -952,13 +1103,36 @@
     exit(cvd::kCuttlefishConfigurationInitError);
   }
 
+  // Create boot_config if necessary
+  if (!InitBootloaderEnvPartition(*config, FLAGS_boot_env_image)) {
+    exit(cvd::kCuttlefishConfigurationInitError);
+  }
+
   if (!cvd::FileExists(FLAGS_metadata_image)) {
     CreateBlankImage(FLAGS_metadata_image, FLAGS_blank_metadata_image_mb, "none");
   }
 
   for (const auto& instance : config->Instances()) {
     if (!cvd::FileExists(instance.access_kregistry_path())) {
-      CreateBlankImage(instance.access_kregistry_path(), 2, "none", "1M");
+      CreateBlankImage(instance.access_kregistry_path(), 2 /* mb */, "none");
+    }
+
+    if (!cvd::FileExists(instance.sdcard_path())) {
+      CreateBlankImage(instance.sdcard_path(),
+                       FLAGS_blank_sdcard_image_mb, "sdcard");
+    }
+  }
+
+  // libavb expects to be able to read the maximum vbmeta size, so we must
+  // provide a partition which matches this or the read will fail
+  for (const auto& vbmeta_image : { FLAGS_vbmeta_image, FLAGS_vbmeta_system_image }) {
+    if (cvd::FileSize(vbmeta_image) != VBMETA_MAX_SIZE) {
+      auto fd = cvd::SharedFD::Open(vbmeta_image, O_RDWR);
+      if (fd->Truncate(VBMETA_MAX_SIZE) != 0) {
+        LOG(ERROR) << "`truncate --size=" << VBMETA_MAX_SIZE << " "
+                   << vbmeta_image << "` failed: " << fd->StrError();
+        exit(cvd::kCuttlefishConfigurationInitError);
+      }
     }
   }
 
@@ -988,7 +1162,7 @@
                      << "newer than its underlying composite disk. Wiping the overlay.";
       }
       CreateQcowOverlay(config->crosvm_binary(), config->composite_disk_path(), overlay_path);
-      CreateBlankImage(instance.access_kregistry_path(), 2, "none", "1M");
+      CreateBlankImage(instance.access_kregistry_path(), 2 /* mb */, "none");
     }
   }
 
diff --git a/host/commands/assemble_cvd/image_aggregator.cc b/host/commands/assemble_cvd/image_aggregator.cc
index 04ec6b3..7be4aaa 100644
--- a/host/commands/assemble_cvd/image_aggregator.cc
+++ b/host/commands/assemble_cvd/image_aggregator.cc
@@ -40,31 +40,14 @@
 #include "common/libs/fs/shared_fd.h"
 #include "common/libs/utils/files.h"
 #include "common/libs/utils/subprocess.h"
+#include "host/commands/assemble_cvd/mbr.h"
 #include "host/libs/config/cuttlefish_config.h"
 #include "device/google/cuttlefish/host/commands/assemble_cvd/cdisk_spec.pb.h"
 
 namespace {
 
-constexpr int SECTOR_SIZE = 512;
 constexpr int GPT_NUM_PARTITIONS = 128;
 
-struct __attribute__((packed)) MbrPartitionEntry {
-  std::uint8_t status;
-  std::uint8_t begin_chs[3];
-  std::uint8_t partition_type;
-  std::uint8_t end_chs[3];
-  std::uint32_t first_lba;
-  std::uint32_t num_sectors;
-};
-
-struct __attribute__((packed)) MasterBootRecord {
-  std::uint8_t bootstrap_code[446];
-  MbrPartitionEntry partitions[4];
-  std::uint8_t boot_signature[2];
-};
-
-static_assert(sizeof(MasterBootRecord) == SECTOR_SIZE);
-
 /**
  * Creates a "Protective" Master Boot Record Partition Table header. The GUID
  * Partition Table Specification recommends putting this on the first sector
@@ -195,6 +178,12 @@
     next_disk_offset_ += size;
   }
 
+  std::uint64_t DiskSize() const {
+    std::uint64_t align = 1 << 16; // 64k alignment
+    std::uint64_t val = next_disk_offset_ + sizeof(GptEnd);
+    return ((val + (align - 1)) / align) * align;
+  }
+
   /**
    * Generates a composite disk specification file, assuming that `header_file`
    * and `footer_file` will be populated with the contents of `Beginning()` and
@@ -204,7 +193,7 @@
                                       const std::string& footer_file) const {
     CompositeDisk disk;
     disk.set_version(1);
-    disk.set_length(next_disk_offset_ + sizeof(GptEnd));
+    disk.set_length(DiskSize());
 
     ComponentDisk* header = disk.add_component_disks();
     header->set_file_path(header_file);
@@ -237,13 +226,13 @@
       return {};
     }
     GptBeginning gpt = {
-      .protective_mbr = ProtectiveMbr(next_disk_offset_ + sizeof(GptEnd)),
+      .protective_mbr = ProtectiveMbr(DiskSize()),
       .header = {
         .signature = {'E', 'F', 'I', ' ', 'P', 'A', 'R', 'T'},
         .revision = {0, 0, 1, 0},
         .header_size = sizeof(GptHeader),
         .current_lba = 1,
-        .backup_lba = (next_disk_offset_ + sizeof(GptEnd)) / SECTOR_SIZE,
+        .backup_lba = (next_disk_offset_ + sizeof(GptEnd)) / SECTOR_SIZE - 1,
         .first_usable_lba = sizeof(GptBeginning) / SECTOR_SIZE,
         .last_usable_lba = (next_disk_offset_ - SECTOR_SIZE) / SECTOR_SIZE,
         .partition_entries_lba = 2,
@@ -306,9 +295,10 @@
   return true;
 }
 
-bool WriteEnd(cvd::SharedFD out, const GptEnd& end) {
-  std::string begin_str((const char*) &end, sizeof(GptEnd));
-  if (cvd::WriteAll(out, begin_str) != begin_str.size()) {
+bool WriteEnd(cvd::SharedFD out, const GptEnd& end, std::int64_t padding) {
+  std::string end_str((const char*) &end, sizeof(GptEnd));
+  end_str.resize(end_str.size() + padding, '\0');
+  if (cvd::WriteAll(out, end_str) != end_str.size()) {
     LOG(ERROR) << "Could not write GPT end: " << out->StrError();
     return false;
   }
@@ -386,7 +376,9 @@
                  << "\" to \"" << output_path << "\": " << output->StrError();
     }
   }
-  if (!WriteEnd(output, builder.End(beginning))) {
+  std::uint64_t padding =
+      builder.DiskSize() - ((beginning.header.backup_lba + 1) * SECTOR_SIZE);
+  if (!WriteEnd(output, builder.End(beginning), padding)) {
     LOG(FATAL) << "Could not write GPT end to \"" << output_path
                << "\": " << output->StrError();
   }
@@ -407,7 +399,9 @@
                << "\": " << header->StrError();
   }
   auto footer = cvd::SharedFD::Creat(footer_file, 0600);
-  if (!WriteEnd(footer, builder.End(beginning))) {
+  std::uint64_t padding =
+      builder.DiskSize() - ((beginning.header.backup_lba + 1) * SECTOR_SIZE);
+  if (!WriteEnd(footer, builder.End(beginning), padding)) {
     LOG(FATAL) << "Could not write GPT end to \"" << footer_file
                << "\": " << footer->StrError();
   }
diff --git a/host/commands/assemble_cvd/mbr.h b/host/commands/assemble_cvd/mbr.h
new file mode 100644
index 0000000..b06a5e3
--- /dev/null
+++ b/host/commands/assemble_cvd/mbr.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+constexpr int SECTOR_SIZE = 512;
+
+struct __attribute__((packed)) MbrPartitionEntry {
+  std::uint8_t status;
+  std::uint8_t begin_chs[3];
+  std::uint8_t partition_type;
+  std::uint8_t end_chs[3];
+  std::uint32_t first_lba;
+  std::uint32_t num_sectors;
+};
+
+struct __attribute__((packed)) MasterBootRecord {
+  std::uint8_t bootstrap_code[446];
+  MbrPartitionEntry partitions[4];
+  std::uint8_t boot_signature[2];
+};
+
+static_assert(sizeof(MasterBootRecord) == SECTOR_SIZE);
diff --git a/host/commands/assemble_cvd/super_image_mixer.cc b/host/commands/assemble_cvd/super_image_mixer.cc
index bdcec93..b05f72f 100644
--- a/host/commands/assemble_cvd/super_image_mixer.cc
+++ b/host/commands/assemble_cvd/super_image_mixer.cc
@@ -61,6 +61,7 @@
   "IMAGES/odm.img",
   "IMAGES/recovery.img",
   "IMAGES/userdata.img",
+  "IMAGES/vbmeta.img",
   "IMAGES/vendor.img",
 };
 
diff --git a/host/commands/fetcher/build_api.cc b/host/commands/fetcher/build_api.cc
index 2722de8..a83df58 100644
--- a/host/commands/fetcher/build_api.cc
+++ b/host/commands/fetcher/build_api.cc
@@ -72,6 +72,12 @@
   return out;
 }
 
+DirectoryBuild::DirectoryBuild(const std::vector<std::string>& paths,
+                               const std::string& target)
+    : paths(paths), target(target), id("eng") {
+  product = getenv("TARGET_PRODUCT");
+}
+
 BuildApi::BuildApi(std::unique_ptr<CredentialSource> credential_source)
     : credential_source(std::move(credential_source)) {}
 
@@ -110,6 +116,15 @@
   return response_json["buildAttemptStatus"].asString();
 }
 
+std::string BuildApi::ProductName(const DeviceBuild& build) {
+  std::string url = BUILD_API + "/builds/" + build.id + "/" + build.target;
+  auto response_json = curl.DownloadToJson(url, Headers());
+  CHECK(!response_json.isMember("error")) << "Error fetching the status of "
+      << "build " << build << ". Response was " << response_json;
+  CHECK(response_json.isMember("target")) << "Build was missing target field.";
+  return response_json["target"]["product"].asString();
+}
+
 std::vector<Artifact> BuildApi::Artifacts(const DeviceBuild& build) {
   std::string url = BUILD_API + "/builds/" + build.id + "/" + build.target
       + "/attempts/latest/artifacts?maxResults=1000";
@@ -213,5 +228,6 @@
     status = build_api->BuildStatus(proposed_build);
   }
   LOG(INFO) << "Status for build " << proposed_build << " is " << status;
+  proposed_build.product = build_api->ProductName(proposed_build);
   return proposed_build;
 }
diff --git a/host/commands/fetcher/build_api.h b/host/commands/fetcher/build_api.h
index 54bbd72..8ccf8b2 100644
--- a/host/commands/fetcher/build_api.h
+++ b/host/commands/fetcher/build_api.h
@@ -56,6 +56,7 @@
 
   std::string id;
   std::string target;
+  std::string product;
 };
 
 std::ostream& operator<<(std::ostream&, const DeviceBuild&);
@@ -63,12 +64,12 @@
 struct DirectoryBuild {
   // TODO(schuffelen): Support local builds other than "eng"
   DirectoryBuild(const std::vector<std::string>& paths,
-                 const std::string& target)
-      : paths(paths), target(target), id("eng") {}
+                 const std::string& target);
 
   std::vector<std::string> paths;
   std::string target;
   std::string id;
+  std::string product;
 };
 
 std::ostream& operator<<(std::ostream&, const DirectoryBuild&);
@@ -91,6 +92,8 @@
 
   std::string BuildStatus(const DeviceBuild&);
 
+  std::string ProductName(const DeviceBuild&);
+
   std::vector<Artifact> Artifacts(const DeviceBuild&);
 
   bool ArtifactToFile(const DeviceBuild& build, const std::string& artifact,
diff --git a/host/commands/fetcher/fetch_cvd.cc b/host/commands/fetcher/fetch_cvd.cc
index 42ce875..8b1387a 100644
--- a/host/commands/fetcher/fetch_cvd.cc
+++ b/host/commands/fetcher/fetch_cvd.cc
@@ -72,13 +72,9 @@
 std::string TargetBuildZipFromArtifacts(
     const Build& build, const std::string& name,
     const std::vector<Artifact>& artifacts) {
-  std::string target = std::visit([](auto&& arg) { return arg.target; }, build);
-  size_t dash_pos = target.find('-');
-  if (dash_pos != std::string::npos) {
-    target.replace(dash_pos, target.size() - dash_pos, "");
-  }
+  std::string product = std::visit([](auto&& arg) { return arg.product; }, build);
   auto id = std::visit([](auto&& arg) { return arg.id; }, build);
-  auto match = target + "-" + name + "-" + id;
+  auto match = product + "-" + name + "-" + id;
   for (const auto& artifact : artifacts) {
     if (artifact.Name().find(match) != std::string::npos) {
       return artifact.Name();
@@ -322,11 +318,12 @@
       bool system_in_img_zip = true;
       if (FLAGS_download_img_zip) {
         std::vector<std::string> image_files =
-            download_images(&build_api, system_build, target_dir, {"system.img"});
+            download_images(&build_api, system_build, target_dir,
+                            {"system.img", "product.img"});
         if (image_files.empty()) {
           LOG(INFO) << "Could not find system image for " << system_build
                     << "in the img zip. Assuming a super image build, which will "
-                    << "get the super image from the target zip.";
+                    << "get the system image from the target zip.";
           system_in_img_zip = false;
         } else {
           LOG(INFO) << "Adding img-zip files for system build";
@@ -368,6 +365,28 @@
               << strerror(error_num);
           return -1;
         }
+        if (ExtractImages(target_files[0], target_dir, {"IMAGES/system_ext.img"})
+            != std::vector<std::string>{}) {
+          std::string extracted_system_ext = target_dir + "/IMAGES/system_ext.img";
+          std::string target_system_ext = target_dir + "/system_ext.img";
+          if (rename(extracted_system_ext.c_str(), target_system_ext.c_str())) {
+            int error_num = errno;
+            LOG(FATAL) << "Could not move system_ext.img in target directory: "
+                       << strerror(error_num);
+            return -1;
+          }
+        }
+        if (ExtractImages(target_files[0], target_dir, {"IMAGES/vbmeta_system.img"})
+            != std::vector<std::string>{}) {
+          std::string extracted_vbmeta_system = target_dir + "/IMAGES/vbmeta_system.img";
+          std::string target_vbmeta_system = target_dir + "/vbmeta_system.img";
+          if (rename(extracted_vbmeta_system.c_str(), target_vbmeta_system.c_str())) {
+            int error_num = errno;
+            LOG(FATAL) << "Could not move vbmeta_system.img in target directory: "
+                       << strerror(error_num);
+            return -1;
+          }
+        }
         // This should technically call AddFilesToConfig with the produced files,
         // but it will conflict with the ones produced from the default system image
         // and pie doesn't care about the produced file list anyway.
diff --git a/host/commands/launch/Android.bp b/host/commands/launch/Android.bp
index 53ed2c3..548931e 100644
--- a/host/commands/launch/Android.bp
+++ b/host/commands/launch/Android.bp
@@ -33,5 +33,8 @@
         "libxml2",
         "libjsoncpp",
     ],
+    required: [
+        "mkenvimage",
+    ],
     defaults: ["cuttlefish_host_only", "cuttlefish_libicuuc"],
 }
diff --git a/host/commands/run_cvd/launch.cc b/host/commands/run_cvd/launch.cc
index abf1f6f..e624e7c 100644
--- a/host/commands/run_cvd/launch.cc
+++ b/host/commands/run_cvd/launch.cc
@@ -1,20 +1,20 @@
 #include "host/commands/run_cvd/launch.h"
 
-#include <sys/types.h>
 #include <sys/stat.h>
+#include <sys/types.h>
 
 #include <android-base/logging.h>
 
 #include "common/libs/fs/shared_fd.h"
 #include "common/libs/utils/files.h"
 #include "common/libs/utils/size_utils.h"
-#include "host/commands/run_cvd/runner_defs.h"
 #include "host/commands/run_cvd/pre_launch_initializers.h"
+#include "host/commands/run_cvd/runner_defs.h"
 #include "host/libs/vm_manager/crosvm_manager.h"
 #include "host/libs/vm_manager/qemu_manager.h"
 
-using cvd::RunnerExitCodes;
 using cvd::MonitorEntry;
+using cvd::RunnerExitCodes;
 
 namespace {
 
@@ -25,9 +25,8 @@
 
 std::string GetAdbConnectorVsockArg(const vsoc::CuttlefishConfig& config) {
   auto instance = config.ForDefaultInstance();
-  return std::string{"vsock:"}
-      + std::to_string(instance.vsock_guest_cid())
-      + std::string{":5555"};
+  return std::string{"vsock:"} + std::to_string(instance.vsock_guest_cid()) +
+         std::string{":5555"};
 }
 
 bool AdbModeEnabled(const vsoc::CuttlefishConfig& config, vsoc::AdbMode mode) {
@@ -36,14 +35,14 @@
 
 bool AdbVsockTunnelEnabled(const vsoc::CuttlefishConfig& config) {
   auto instance = config.ForDefaultInstance();
-  return instance.vsock_guest_cid() > 2
-      && AdbModeEnabled(config, vsoc::AdbMode::VsockTunnel);
+  return instance.vsock_guest_cid() > 2 &&
+         AdbModeEnabled(config, vsoc::AdbMode::VsockTunnel);
 }
 
 bool AdbVsockHalfTunnelEnabled(const vsoc::CuttlefishConfig& config) {
   auto instance = config.ForDefaultInstance();
-  return instance.vsock_guest_cid() > 2
-      && AdbModeEnabled(config, vsoc::AdbMode::VsockHalfTunnel);
+  return instance.vsock_guest_cid() > 2 &&
+         AdbModeEnabled(config, vsoc::AdbMode::VsockHalfTunnel);
 }
 
 bool AdbTcpConnectorEnabled(const vsoc::CuttlefishConfig& config) {
@@ -53,8 +52,8 @@
 }
 
 bool AdbVsockConnectorEnabled(const vsoc::CuttlefishConfig& config) {
-  return config.run_adb_connector()
-      && AdbModeEnabled(config, vsoc::AdbMode::NativeVsock);
+  return config.run_adb_connector() &&
+         AdbModeEnabled(config, vsoc::AdbMode::NativeVsock);
 }
 
 cvd::OnSocketReadyCb GetOnSubprocessExitCallback(
@@ -67,10 +66,10 @@
 }
 
 cvd::SharedFD CreateUnixInputServer(const std::string& path) {
-  auto server = cvd::SharedFD::SocketLocalServer(path.c_str(), false, SOCK_STREAM, 0666);
+  auto server =
+      cvd::SharedFD::SocketLocalServer(path.c_str(), false, SOCK_STREAM, 0666);
   if (!server->IsOpen()) {
-    LOG(ERROR) << "Unable to create unix input server: "
-               << server->StrError();
+    LOG(ERROR) << "Unable to create unix input server: " << server->StrError();
     return cvd::SharedFD();
   }
   return server;
@@ -78,8 +77,8 @@
 
 // Creates the frame and input sockets and add the relevant arguments to the vnc
 // server and webrtc commands
-StreamerLaunchResult CreateStreamerServers(cvd::Command* cmd,
-                                           const vsoc::CuttlefishConfig& config) {
+StreamerLaunchResult CreateStreamerServers(
+    cvd::Command* cmd, const vsoc::CuttlefishConfig& config) {
   StreamerLaunchResult server_ret;
   cvd::SharedFD touch_server;
   cvd::SharedFD keyboard_server;
@@ -104,7 +103,8 @@
   cmd->AddParameter("-touch_fd=", touch_server);
 
   if (!keyboard_server->IsOpen()) {
-    LOG(ERROR) << "Could not open keyboard server: " << keyboard_server->StrError();
+    LOG(ERROR) << "Could not open keyboard server: "
+               << keyboard_server->StrError();
     return {};
   }
   cmd->AddParameter("-keyboard_fd=", keyboard_server);
@@ -125,15 +125,14 @@
   return server_ret;
 }
 
-} // namespace
+}  // namespace
 
 bool LogcatReceiverEnabled(const vsoc::CuttlefishConfig& config) {
   return config.logcat_mode() == cvd::kLogcatVsockMode;
 }
 
 std::vector<cvd::SharedFD> LaunchKernelLogMonitor(
-    const vsoc::CuttlefishConfig& config,
-    cvd::ProcessMonitor* process_monitor,
+    const vsoc::CuttlefishConfig& config, cvd::ProcessMonitor* process_monitor,
     unsigned int number_of_event_pipes) {
   auto instance = config.ForDefaultInstance();
   auto log_name = instance.kernel_log_pipe_name();
@@ -251,8 +250,7 @@
 }
 
 StreamerLaunchResult LaunchVNCServer(
-    const vsoc::CuttlefishConfig& config,
-    cvd::ProcessMonitor* process_monitor,
+    const vsoc::CuttlefishConfig& config, cvd::ProcessMonitor* process_monitor,
     std::function<bool(MonitorEntry*)> callback) {
   auto instance = config.ForDefaultInstance();
   // Launch the vnc server, don't wait for it to complete
@@ -295,23 +293,36 @@
 
 StreamerLaunchResult LaunchWebRTC(cvd::ProcessMonitor* process_monitor,
                                   const vsoc::CuttlefishConfig& config) {
-  cvd::Command webrtc(config.webrtc_binary());
-
-  if (!config.webrtc_certs_dir().empty()) {
-      webrtc.AddParameter("--certs_dir=", config.webrtc_certs_dir());
+  if (config.ForDefaultInstance().start_webrtc_sig_server()) {
+    cvd::Command sig_server(config.sig_server_binary());
+    sig_server.AddParameter("-assets_dir=", config.webrtc_assets_dir());
+    if (!config.webrtc_certs_dir().empty()) {
+      sig_server.AddParameter("-certs_dir=", config.webrtc_certs_dir());
+    }
+    sig_server.AddParameter("-http_server_port=", config.sig_server_port());
+    process_monitor->StartSubprocess(std::move(sig_server),
+                                     GetOnSubprocessExitCallback(config));
   }
 
-  webrtc.AddParameter("--http_server_port=", vsoc::ForCurrentInstance(8443));
-  webrtc.AddParameter("--public_ip=", config.webrtc_public_ip());
-  webrtc.AddParameter("--assets_dir=", config.webrtc_assets_dir());
+  // Currently there is no way to ensure the signaling server will already have
+  // bound the socket to the port by the time the webrtc process runs (the
+  // common technique of doing it from the launcher is not possible here as the
+  // server library being used creates its own sockets). However, this issue is
+  // mitigated slightly by doing some retrying and backoff in the webrtc process
+  // when connecting to the websocket, so it shouldn't be an issue most of the
+  // time.
+
+  cvd::Command webrtc(config.webrtc_binary());
+  webrtc.AddParameter("-public_ip=", config.webrtc_public_ip());
 
   auto server_ret = CreateStreamerServers(&webrtc, config);
 
   if (config.webrtc_enable_adb_websocket()) {
-      auto instance = config.ForDefaultInstance();
-      webrtc.AddParameter("--adb=", instance.adb_ip_and_port());
+    auto instance = config.ForDefaultInstance();
+    webrtc.AddParameter("--adb=", instance.adb_ip_and_port());
   }
 
+  // TODO get from launcher params
   process_monitor->StartSubprocess(std::move(webrtc),
                                    GetOnSubprocessExitCallback(config));
   server_ret.launched = true;
@@ -320,14 +331,14 @@
 }
 
 void LaunchSocketVsockProxyIfEnabled(cvd::ProcessMonitor* process_monitor,
-                                 const vsoc::CuttlefishConfig& config) {
+                                     const vsoc::CuttlefishConfig& config) {
   auto instance = config.ForDefaultInstance();
   if (AdbVsockTunnelEnabled(config)) {
     cvd::Command adb_tunnel(config.socket_vsock_proxy_binary());
     adb_tunnel.AddParameter("--server=tcp");
     adb_tunnel.AddParameter("--vsock_port=6520");
-    adb_tunnel.AddParameter(
-        std::string{"--tcp_port="} + std::to_string(instance.host_port()));
+    adb_tunnel.AddParameter(std::string{"--tcp_port="} +
+                            std::to_string(instance.host_port()));
     adb_tunnel.AddParameter(std::string{"--vsock_cid="} +
                             std::to_string(instance.vsock_guest_cid()));
     process_monitor->StartSubprocess(std::move(adb_tunnel),
@@ -337,8 +348,8 @@
     cvd::Command adb_tunnel(config.socket_vsock_proxy_binary());
     adb_tunnel.AddParameter("--server=tcp");
     adb_tunnel.AddParameter("--vsock_port=5555");
-    adb_tunnel.AddParameter(
-        std::string{"--tcp_port="} + std::to_string(instance.host_port()));
+    adb_tunnel.AddParameter(std::string{"--tcp_port="} +
+                            std::to_string(instance.host_port()));
     adb_tunnel.AddParameter(std::string{"--vsock_cid="} +
                             std::to_string(instance.vsock_guest_cid()));
     process_monitor->StartSubprocess(std::move(adb_tunnel),
@@ -366,7 +377,7 @@
 }
 
 void LaunchMetrics(cvd::ProcessMonitor* process_monitor,
-                                  const vsoc::CuttlefishConfig& config) {
+                   const vsoc::CuttlefishConfig& config) {
   cvd::Command metrics(config.metrics_binary());
 
   process_monitor->StartSubprocess(std::move(metrics),
@@ -394,10 +405,21 @@
                const vsoc::CuttlefishConfig& config) {
   if (config.tpm_device() != "") {
     if (config.tpm_binary() != "") {
-      LOG(WARNING) << "Both -tpm_device and -tpm_binary were set. Using -tpm_device.";
+      LOG(WARNING)
+          << "Both -tpm_device and -tpm_binary were set. Using -tpm_device.";
     }
     LaunchTpmPassthrough(process_monitor, config);
   } else if (config.tpm_binary() != "") {
     LaunchTpmSimulator(process_monitor, config);
   }
 }
+
+void LaunchSecureEnvironment(cvd::ProcessMonitor* process_monitor,
+                             const vsoc::CuttlefishConfig& config) {
+  auto port = config.ForDefaultInstance().keymaster_vsock_port();
+  auto server = cvd::SharedFD::VsockServer(port, SOCK_STREAM);
+  cvd::Command command(vsoc::DefaultHostArtifactsPath("bin/secure_env"));
+  command.AddParameter("-keymaster_fd=", server);
+  process_monitor->StartSubprocess(std::move(command),
+                                   GetOnSubprocessExitCallback(config));
+}
diff --git a/host/commands/run_cvd/launch.h b/host/commands/run_cvd/launch.h
index af3ec40..650886b 100644
--- a/host/commands/run_cvd/launch.h
+++ b/host/commands/run_cvd/launch.h
@@ -42,3 +42,6 @@
 
 void LaunchMetrics(cvd::ProcessMonitor* process_monitor,
                                   const vsoc::CuttlefishConfig& config);
+
+void LaunchSecureEnvironment(cvd::ProcessMonitor* process_monitor,
+                             const vsoc::CuttlefishConfig& config);
diff --git a/host/commands/run_cvd/main.cc b/host/commands/run_cvd/main.cc
index c72237e..5cd2b81 100644
--- a/host/commands/run_cvd/main.cc
+++ b/host/commands/run_cvd/main.cc
@@ -424,6 +424,7 @@
   LaunchConfigServer(*config, &process_monitor);
   LaunchTombstoneReceiverIfEnabled(*config, &process_monitor);
   LaunchTpm(&process_monitor, *config);
+  LaunchSecureEnvironment(&process_monitor, *config);
 
   // The streamer needs to launch before the VMM because it serves on several
   // sockets (input devices, vsock frame server) when using crosvm.
diff --git a/host/commands/secure_env/Android.bp b/host/commands/secure_env/Android.bp
new file mode 100644
index 0000000..8d8efe7
--- /dev/null
+++ b/host/commands/secure_env/Android.bp
@@ -0,0 +1,42 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_binary_host {
+    name: "secure_env",
+    srcs: [
+        "keymaster_responder.cpp",
+        "secure_env.cpp",
+    ],
+    shared_libs: [
+        "libbase",
+        "libcuttlefish_fs",
+        "libcuttlefish_security",
+        "libcuttlefish_utils",
+        "libkeymaster_portable",
+        "libkeymaster_messages",
+        "libsoft_attestation_cert",
+        "liblog",
+        "libcrypto",
+        "libcutils",
+        "libpuresoftkeymasterdevice_host",
+    ],
+    static_libs: [
+        "libgflags",
+    ],
+    defaults: ["cuttlefish_host_only"],
+    cflags: [
+        "-fno-rtti", // Required for libkeymaster_portable
+    ],
+}
diff --git a/host/commands/secure_env/keymaster_responder.cpp b/host/commands/secure_env/keymaster_responder.cpp
new file mode 100644
index 0000000..2076866
--- /dev/null
+++ b/host/commands/secure_env/keymaster_responder.cpp
@@ -0,0 +1,104 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "keymaster_responder.h"
+
+#include <android-base/logging.h>
+#include <keymaster/android_keymaster_messages.h>
+
+KeymasterResponder::KeymasterResponder(
+    cvd::KeymasterChannel* channel, keymaster::AndroidKeymaster* keymaster)
+    : channel_(channel), keymaster_(keymaster) {
+}
+
+bool KeymasterResponder::ProcessMessage() {
+  auto request = channel_->ReceiveMessage();
+  if (!request) {
+    LOG(ERROR) << "Could not receive message";
+    return false;
+  }
+  const uint8_t* buffer = request->payload;
+  const uint8_t* end = request->payload + request->payload_size;
+  switch(request->cmd) {
+    using namespace keymaster;
+#define HANDLE_MESSAGE(ENUM_NAME, METHOD_NAME) \
+    case ENUM_NAME: {\
+      METHOD_NAME##Request request; \
+      if (!request.Deserialize(&buffer, end)) { \
+        LOG(ERROR) << "Failed to deserialize " #METHOD_NAME "Request"; \
+        return false; \
+      } \
+      METHOD_NAME##Response response; \
+      keymaster_->METHOD_NAME(request, &response); \
+      return channel_->SendResponse(ENUM_NAME, response); \
+    }
+    HANDLE_MESSAGE(GENERATE_KEY, GenerateKey)
+    HANDLE_MESSAGE(BEGIN_OPERATION, BeginOperation)
+    HANDLE_MESSAGE(UPDATE_OPERATION, UpdateOperation)
+    HANDLE_MESSAGE(FINISH_OPERATION, FinishOperation)
+    HANDLE_MESSAGE(ABORT_OPERATION, AbortOperation)
+    HANDLE_MESSAGE(IMPORT_KEY, ImportKey)
+    HANDLE_MESSAGE(EXPORT_KEY, ExportKey)
+    HANDLE_MESSAGE(GET_VERSION, GetVersion)
+    HANDLE_MESSAGE(GET_SUPPORTED_ALGORITHMS, SupportedAlgorithms)
+    HANDLE_MESSAGE(GET_SUPPORTED_BLOCK_MODES, SupportedBlockModes)
+    HANDLE_MESSAGE(GET_SUPPORTED_PADDING_MODES, SupportedPaddingModes)
+    HANDLE_MESSAGE(GET_SUPPORTED_DIGESTS, SupportedDigests)
+    HANDLE_MESSAGE(GET_SUPPORTED_IMPORT_FORMATS, SupportedImportFormats)
+    HANDLE_MESSAGE(GET_SUPPORTED_EXPORT_FORMATS, SupportedExportFormats)
+    HANDLE_MESSAGE(GET_KEY_CHARACTERISTICS, GetKeyCharacteristics)
+    HANDLE_MESSAGE(ATTEST_KEY, AttestKey)
+    HANDLE_MESSAGE(UPGRADE_KEY, UpgradeKey)
+    HANDLE_MESSAGE(CONFIGURE, Configure)
+    HANDLE_MESSAGE(DELETE_KEY, DeleteKey)
+    HANDLE_MESSAGE(DELETE_ALL_KEYS, DeleteAllKeys)
+    HANDLE_MESSAGE(IMPORT_WRAPPED_KEY, ImportWrappedKey)
+#undef HANDLE_MESSAGE
+#define HANDLE_MESSAGE(ENUM_NAME, METHOD_NAME) \
+    case ENUM_NAME: {\
+      METHOD_NAME##Request request; \
+      if (!request.Deserialize(&buffer, end)) { \
+        LOG(ERROR) << "Failed to deserialize " #METHOD_NAME "Request"; \
+        return false; \
+      } \
+      auto response = keymaster_->METHOD_NAME(request); \
+      return channel_->SendResponse(ENUM_NAME, response); \
+    }
+    HANDLE_MESSAGE(COMPUTE_SHARED_HMAC, ComputeSharedHmac)
+    HANDLE_MESSAGE(VERIFY_AUTHORIZATION, VerifyAuthorization)
+#undef HANDLE_MESSAGE
+#define HANDLE_MESSAGE(ENUM_NAME, METHOD_NAME) \
+    case ENUM_NAME: {\
+      auto response = keymaster_->METHOD_NAME(); \
+      return channel_->SendResponse(ENUM_NAME, response); \
+    }
+    HANDLE_MESSAGE(GET_HMAC_SHARING_PARAMETERS, GetHmacSharingParameters)
+    HANDLE_MESSAGE(EARLY_BOOT_ENDED, EarlyBootEnded)
+    case ADD_RNG_ENTROPY: {
+      AddEntropyRequest request;
+      if (!request.Deserialize(&buffer, end)) {
+        LOG(ERROR) << "Failed to deserialize AddEntropyRequest";
+        return false;
+      }
+      AddEntropyResponse response;
+      keymaster_->AddRngEntropy(request, &response);
+      return channel_->SendResponse(ADD_RNG_ENTROPY, response);
+    }
+    case DESTROY_ATTESTATION_IDS: // Not defined in AndroidKeymaster?
+      break;
+  }
+  LOG(ERROR) << "Unknown request type: " << request->cmd;
+  return false;
+}
diff --git a/host/commands/secure_env/keymaster_responder.h b/host/commands/secure_env/keymaster_responder.h
new file mode 100644
index 0000000..b30c6a4
--- /dev/null
+++ b/host/commands/secure_env/keymaster_responder.h
@@ -0,0 +1,31 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <keymaster/android_keymaster.h>
+
+#include "common/libs/security/keymaster_channel.h"
+
+class KeymasterResponder {
+private:
+  cvd::KeymasterChannel* channel_;
+  keymaster::AndroidKeymaster* keymaster_;
+public:
+  KeymasterResponder(cvd::KeymasterChannel* channel,
+                     keymaster::AndroidKeymaster* keymaster);
+
+  bool ProcessMessage();
+};
diff --git a/host/commands/secure_env/secure_env.cpp b/host/commands/secure_env/secure_env.cpp
new file mode 100644
index 0000000..c9edc6b
--- /dev/null
+++ b/host/commands/secure_env/secure_env.cpp
@@ -0,0 +1,51 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <android-base/logging.h>
+#include <gflags/gflags.h>
+#include <keymaster/android_keymaster.h>
+#include <keymaster/contexts/pure_soft_keymaster_context.h>
+
+#include "common/libs/fs/shared_fd.h"
+#include "common/libs/security/keymaster_channel.h"
+#include "host/commands/secure_env/keymaster_responder.h"
+
+// Copied from AndroidKeymaster4Device
+constexpr size_t kOperationTableSize = 16;
+
+DEFINE_int32(keymaster_fd, -1, "A file descriptor for keymaster communication");
+
+int main(int argc, char** argv) {
+  ::android::base::InitLogging(argv);
+  gflags::ParseCommandLineFlags(&argc, &argv, true);
+  keymaster::PureSoftKeymasterContext keymaster_context{
+      KM_SECURITY_LEVEL_SOFTWARE};
+  keymaster::AndroidKeymaster keymaster{&keymaster_context, kOperationTableSize};
+
+  CHECK(FLAGS_keymaster_fd != -1)
+      << "TODO(schuffelen): Add keymaster_fd alternative";
+  auto server = cvd::SharedFD::Dup(FLAGS_keymaster_fd);
+  CHECK(server->IsOpen()) << "Could not dup server fd: " << server->StrError();
+  close(FLAGS_keymaster_fd);
+  auto conn = cvd::SharedFD::Accept(*server);
+  CHECK(conn->IsOpen()) << "Unable to open connection: " << conn->StrError();
+  cvd::KeymasterChannel keymaster_channel(conn);
+
+  KeymasterResponder keymaster_responder(&keymaster_channel, &keymaster);
+
+  // TODO(schuffelen): Do this in a thread when adding other HALs
+  while (keymaster_responder.ProcessMessage()) {
+  }
+}
diff --git a/host/commands/tapsetiff/Android.bp b/host/commands/tapsetiff/Android.bp
new file mode 100644
index 0000000..c860241
--- /dev/null
+++ b/host/commands/tapsetiff/Android.bp
@@ -0,0 +1,19 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+sh_binary_host {
+    name: "tapsetiff",
+    src: "tapsetiff.py",
+}
diff --git a/host/commands/tapsetiff/tapsetiff.py b/host/commands/tapsetiff/tapsetiff.py
new file mode 100755
index 0000000..3e7c9e4
--- /dev/null
+++ b/host/commands/tapsetiff/tapsetiff.py
@@ -0,0 +1,30 @@
+#!/usr/bin/python
+
+# Copyright (C) 2020 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http:#www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import fcntl
+import struct
+import sys
+
+TUNSETIFF = 0x400454ca
+IFF_TAP = 0x0002
+IFF_NO_PI = 0x1000
+IFF_VNET_HDR = 0x4000
+
+tun_fd = int(sys.argv[1])
+tap_name = sys.argv[2]
+
+ifr = struct.pack('16sH', tap_name, IFF_TAP | IFF_NO_PI | IFF_VNET_HDR)
+fcntl.ioctl(tun_fd, TUNSETIFF, ifr)
diff --git a/host/commands/tpm_simulator_manager/tpm_simulator_manager.cpp b/host/commands/tpm_simulator_manager/tpm_simulator_manager.cpp
index cd726f3..c5d8225 100644
--- a/host/commands/tpm_simulator_manager/tpm_simulator_manager.cpp
+++ b/host/commands/tpm_simulator_manager/tpm_simulator_manager.cpp
@@ -87,15 +87,18 @@
     }
     if (command_server && platform_server && !sent_init) {
       client = cvd::SharedFD::SocketLocalClient(FLAGS_port + 1, SOCK_STREAM);
-      std::vector<char> command_bytes(4, 0);
-      *reinterpret_cast<std::uint32_t*>(command_bytes.data()) = htobe32(1); // TPM_SIGNAL_POWER_ON
-      CHECK(cvd::WriteAll(client, command_bytes) == 4) << "Could not send TPM_SIGNAL_POWER_ON";
-      std::vector<char> response_bytes(4, 0);
-      CHECK(cvd::ReadExact(client, &response_bytes) == 4) << "Could not read parity response";
+      std::uint32_t command = htobe32(1); // TPM_SIGNAL_POWER_ON
+      CHECK(cvd::WriteAllBinary(client, &command) == 4)
+          << "Could not send TPM_SIGNAL_POWER_ON";
+      std::uint32_t response;
+      CHECK(cvd::ReadExactBinary(client, &response) == 4)
+          << "Could not read parity response";
 
-      *reinterpret_cast<std::uint32_t*>(command_bytes.data()) = htobe32(11); // TPM_SIGNAL_NV_ON
-      CHECK(cvd::WriteAll(client, command_bytes) == 4) << "Could not send TPM_SIGNAL_NV_ON";
-      CHECK(cvd::ReadExact(client, &response_bytes) == 4) << "Could not read parity response";
+      command = htobe32(11); // TPM_SIGNAL_NV_ON
+      CHECK(cvd::WriteAllBinary(client, &command) == 4)
+          << "Could not send TPM_SIGNAL_NV_ON";
+      CHECK(cvd::ReadExactBinary(client, &response) == 4)
+          << "Could not read parity response";
 
       sent_init = true;
     }
diff --git a/host/frontend/gcastv2/https/Android.bp b/host/frontend/gcastv2/https/Android.bp
index 4078e80..1a290d5 100644
--- a/host/frontend/gcastv2/https/Android.bp
+++ b/host/frontend/gcastv2/https/Android.bp
@@ -35,9 +35,9 @@
         "libbase",
         "libcrypto",
         "libssl",
-	"liblog",
+        "liblog",
+        "libcuttlefish_utils",
     ],
     local_include_dirs: ["include"],
     export_include_dirs: ["include"],
 }
-
diff --git a/host/frontend/gcastv2/https/HTTPServer.cpp b/host/frontend/gcastv2/https/HTTPServer.cpp
index 967f5f7..6c4a5ff 100644
--- a/host/frontend/gcastv2/https/HTTPServer.cpp
+++ b/host/frontend/gcastv2/https/HTTPServer.cpp
@@ -19,6 +19,7 @@
 #include <https/ClientSocket.h>
 #include <https/HTTPRequestResponse.h>
 #include <https/Support.h>
+#include "common/libs/utils/base64.h"
 
 #include <android-base/logging.h>
 
@@ -298,7 +299,7 @@
     CHECK_EQ(res, 1);
 
     std::string acceptKey;
-    encodeBase64(digest, sizeof(digest), &acceptKey);
+    cvd::EncodeBase64(digest, sizeof(digest), &acceptKey);
 
     (*responseHeaders)["Sec-WebSocket-Accept"] = acceptKey;
 
diff --git a/host/frontend/gcastv2/https/PlainSocket.cpp b/host/frontend/gcastv2/https/PlainSocket.cpp
index 26ee574..6a870f3 100644
--- a/host/frontend/gcastv2/https/PlainSocket.cpp
+++ b/host/frontend/gcastv2/https/PlainSocket.cpp
@@ -44,9 +44,9 @@
         const sockaddr *addr,
         socklen_t addrLen) {
     if (!addr) {
-        return ::send(fd(), data, size, 0);
+        return ::send(fd(), data, size, MSG_NOSIGNAL);
     }
-    return ::sendto(fd(), data, size, 0, addr, addrLen);
+    return ::sendto(fd(), data, size, MSG_NOSIGNAL, addr, addrLen);
 }
 
 void PlainSocket::postFlush(RunLoop::AsyncFunction fn) {
diff --git a/host/frontend/gcastv2/https/SSLSocket.cpp b/host/frontend/gcastv2/https/SSLSocket.cpp
index db8543a..e86df23 100644
--- a/host/frontend/gcastv2/https/SSLSocket.cpp
+++ b/host/frontend/gcastv2/https/SSLSocket.cpp
@@ -362,7 +362,7 @@
 
     while (offset < size) {
         ssize_t n = ::send(
-                fd(), mOutBuffer.data() + offset, size - offset, 0);
+                fd(), mOutBuffer.data() + offset, size - offset, MSG_NOSIGNAL);
 
         if (n < 0) {
             if (errno == EINTR) {
diff --git a/host/frontend/gcastv2/https/Support.cpp b/host/frontend/gcastv2/https/Support.cpp
index be9bcf5..30a0021 100644
--- a/host/frontend/gcastv2/https/Support.cpp
+++ b/host/frontend/gcastv2/https/Support.cpp
@@ -71,53 +71,6 @@
   return ss.str();
 }
 
-static char encode6Bit(unsigned x) {
-  static char base64[] =
-      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
-  return base64[x & 63];
-}
-
-void encodeBase64(const void *_data, size_t size, std::string *out) {
-    out->clear();
-    out->reserve(((size+2)/3)*4);
-
-    const uint8_t *data = (const uint8_t *)_data;
-
-    size_t i;
-    for (i = 0; i < (size / 3) * 3; i += 3) {
-        uint8_t x1 = data[i];
-        uint8_t x2 = data[i + 1];
-        uint8_t x3 = data[i + 2];
-
-        out->append(1, encode6Bit(x1 >> 2));
-        out->append(1, encode6Bit((x1 << 4 | x2 >> 4) & 0x3f));
-        out->append(1, encode6Bit((x2 << 2 | x3 >> 6) & 0x3f));
-        out->append(1, encode6Bit(x3 & 0x3f));
-    }
-    switch (size % 3) {
-        case 0:
-            break;
-        case 2:
-        {
-            uint8_t x1 = data[i];
-            uint8_t x2 = data[i + 1];
-            out->append(1, encode6Bit(x1 >> 2));
-            out->append(1, encode6Bit((x1 << 4 | x2 >> 4) & 0x3f));
-            out->append(1, encode6Bit((x2 << 2) & 0x3f));
-            out->append(1, '=');
-            break;
-        }
-        default:
-        {
-            uint8_t x1 = data[i];
-            out->append(1, encode6Bit(x1 >> 2));
-            out->append(1, encode6Bit((x1 << 4) & 0x3f));
-            out->append("==");
-            break;
-        }
-    }
-}
-
 uint16_t U16_AT(const uint8_t *ptr) {
     return ptr[0] << 8 | ptr[1];
 }
diff --git a/host/frontend/gcastv2/https/WebSocketHandler.cpp b/host/frontend/gcastv2/https/WebSocketHandler.cpp
index c8b5526..3776a24 100644
--- a/host/frontend/gcastv2/https/WebSocketHandler.cpp
+++ b/host/frontend/gcastv2/https/WebSocketHandler.cpp
@@ -82,6 +82,8 @@
           uint8_t opcode = headerByte & 0x0f;
           if (opcode == 0x9 /*ping*/) {
             sendMessage(&packet[packetOffset], payloadLen, SendMode::pong);
+          } else if (opcode == 0x8 /*close*/) {
+            return -1;
           }
         } else {
           err = handleMessage(headerByte, &packet[packetOffset], payloadLen);
diff --git a/host/frontend/gcastv2/https/include/https/Support.h b/host/frontend/gcastv2/https/include/https/Support.h
index 63c5999..a829fe5 100644
--- a/host/frontend/gcastv2/https/include/https/Support.h
+++ b/host/frontend/gcastv2/https/include/https/Support.h
@@ -19,6 +19,7 @@
 #include <sys/types.h>
 
 #include <string>
+#include <vector>
 
 #ifdef NDEBUG
 #define DEBUG_ONLY(x)
@@ -29,8 +30,6 @@
 void makeFdNonblocking(int fd);
 std::string hexdump(const void *_data, size_t size);
 
-void encodeBase64(const void *_data, size_t size, std::string *out);
-
 uint16_t U16_AT(const uint8_t *ptr);
 uint32_t U32_AT(const uint8_t *ptr);
 uint64_t U64_AT(const uint8_t *ptr);
diff --git a/host/frontend/gcastv2/signaling_server/Android.bp b/host/frontend/gcastv2/signaling_server/Android.bp
new file mode 100644
index 0000000..cf441da
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/Android.bp
@@ -0,0 +1,117 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_library_headers {
+    name: "webrtc_signaling_headers",
+    export_include_dirs: ["./constants"],
+    host_supported: true,
+}
+
+cc_binary_host {
+    name: "webrtc_sig_server",
+    srcs: [
+        "client_handler.cpp",
+        "device_registry.cpp",
+        "device_handler.cpp",
+        "device_list_handler.cpp",
+        "server_config.cpp",
+        "server.cpp",
+        "signal_handler.cpp",
+    ],
+    header_libs: [
+      "webrtc_signaling_headers",
+    ],
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libcrypto",
+        "libssl",
+        "libcuttlefish_fs",
+    ],
+    static_libs: [
+        "libgflags",
+        "libjsoncpp",
+        "libhttps",
+        "libwebrtc",
+        "libcuttlefish_utils",
+    ],
+    defaults: ["cuttlefish_host_only"],
+}
+
+// TODO(jemoreira): Ideally these files should be in $HOST_OUT/webrtc but I
+// couldn't find a module type that would produce that, prebuilt_usr_share_host
+// is the next best thing for now.
+prebuilt_usr_share_host {
+    name: "webrtc_index.html",
+    src: "assets/index.html",
+    filename: "index.html",
+    sub_dir: "webrtc/assets",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_style.css",
+    src: "assets/style.css",
+    filename: "style.css",
+    sub_dir: "webrtc/assets",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_logcat.js",
+    src: "assets/js/logcat.js",
+    filename: "logcat.js",
+    sub_dir: "webrtc/assets/js",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_cf.js",
+    src: "assets/js/cf_webrtc.js",
+    filename: "cf_webrtc.js",
+    sub_dir: "webrtc/assets/js",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_app.js",
+    src: "assets/js/app.js",
+    filename: "app.js",
+    sub_dir: "webrtc/assets/js",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_server.crt",
+    src: "certs/server.crt",
+    filename: "server.crt",
+    sub_dir: "webrtc/certs",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_server.key",
+    src: "certs/server.key",
+    filename: "server.key",
+    sub_dir: "webrtc/certs",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_server.p12",
+    src: "certs/server.p12",
+    filename: "server.p12",
+    sub_dir: "webrtc/certs",
+}
+
+prebuilt_usr_share_host {
+    name: "webrtc_trusted.pem",
+    src: "certs/trusted.pem",
+    filename: "trusted.pem",
+    sub_dir: "webrtc/certs",
+}
diff --git a/host/frontend/gcastv2/signaling_server/Readme.md b/host/frontend/gcastv2/signaling_server/Readme.md
new file mode 100644
index 0000000..8771138
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/Readme.md
@@ -0,0 +1,86 @@
+This signaling server defines a very simple protocol to allow the establishing
+of a WebRTC connection between clients and devices. It should only be used for
+development purposes or for very simple applications with no security, privacy
+or scalability requirements.
+
+Serious applications should build their own signaling server, implementing the
+protocol exactly as defined below (any modifications would likely require
+modifications to the client and/or device which will then not be maintained by
+the cuttlefish team).
+
+The signaling server MUST expose two (different) websocket endpoints:
+
+* wss://<addr>/register_device
+* wss://<addr>/connect_client
+
+Additional endpoints are allowed and are up to the specific applications.
+Extending the messages below with additional fields should be done with extreme
+care, prefixing the field names with an applciation specific word is strongly
+recommended. The same holds true for creating new message types.
+
+Devices connect to the *register_device* endpoint and send these types of
+messages:
+
+* {"message_type": "register", "device_id": <String>, "device_info": <Any>}
+
+* {"message_type": "forward", "client_id": <Integer>, "payload": <Any>}
+
+The server sends the device these types of messages:
+
+* {"message_type": "config", "ice_servers": <Array of IceServer dictionaries>,
+...}
+
+* {"message_type": "client_msg", "client_id": <Integer>, "payload": <Any>}
+
+* {"error": <String>}
+
+Clients connect to the *connect_client* endpoint and send these types of
+messages:
+
+* {"message_type": "connect", "device_id": <String>}
+
+* {"message_type": "forward", "payload": <Any>}
+
+The server sends the clients these types of messages:
+
+* {"message_type": "config", "ice_servers": <Array of IceServer dictionaries>,
+...}
+
+* {"message_type": "device_info", "device_info": <Any>}
+
+* {"message_type": "device_msg", "payload": <Any>}
+
+* {"error": <String>}
+
+A typical application flow looks like this:
+
+* **Device** connects to *register_device*
+
+* **Device** sends **register** message
+
+* **Server** sends **config** message to **Device**
+
+* **Client** connects to *connect_client*
+
+* **Client** sends **connect** message
+
+* **Server** sends **config** message to **Client**
+
+* **Server** sends **device_info** message to **Client**
+
+* **Client** sends **forward** message
+
+* **Server** sends **client_msg** message to **Device** (at this point the
+device knows about the client and cand send **forward** messages intended for
+it)
+
+* **Device** sends **forward** message
+
+* **Server** sends **device_msg** message to client
+
+* ...
+
+In an alternative flow, not supported by this implementation but allowed by the
+design, the **Client** connects first and only receives a **config** message
+from the **Server**, only after the **Device** has sent the **register** message
+the **Server** sends the **device_info** messaage to the **Client**.
diff --git a/host/frontend/gcastv2/webrtc/assets/index.html b/host/frontend/gcastv2/signaling_server/assets/index.html
similarity index 73%
rename from host/frontend/gcastv2/webrtc/assets/index.html
rename to host/frontend/gcastv2/signaling_server/assets/index.html
index 9c8c5a1..ccbd071 100644
--- a/host/frontend/gcastv2/webrtc/assets/index.html
+++ b/host/frontend/gcastv2/signaling_server/assets/index.html
@@ -6,8 +6,13 @@
     </head>
 
     <body>
-        <button id="receiveButton">Receive Media</button>
+      <section id='device_selector'>
+        <h1>Available devices <span id='refresh_list'>&#8635;</span></h1>
+        <ul id="device_list"></ul>
+      </section>
+      <section id='device_connection'>
         <button id="keyboardCaptureBtn">Capture Keyboard</button>
+        <button id="showLogcatBtn">Show Logcat</button>
         <hr>
         <section class="noscroll">
             <div class="one">
@@ -19,6 +24,7 @@
                 </textarea>
             </div>
         </section>
+      </section>
 
         <script src="js/logcat.js"></script>
         <script src="js/cf_webrtc.js" type="module"></script>
diff --git a/host/frontend/gcastv2/signaling_server/assets/js/app.js b/host/frontend/gcastv2/signaling_server/assets/js/app.js
new file mode 100644
index 0000000..54efbf2
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/assets/js/app.js
@@ -0,0 +1,224 @@
+'use strict';
+
+function ConnectToDevice(device_id, use_tcp) {
+  console.log('ConnectToDevice ', device_id);
+  const keyboardCaptureButton = document.getElementById('keyboardCaptureBtn');
+  keyboardCaptureButton.addEventListener('click', onKeyboardCaptureClick);
+
+  const deviceScreen = document.getElementById('deviceScreen');
+  deviceScreen.addEventListener('click', onInitialClick);
+
+  function onInitialClick(e) {
+    // This stupid thing makes sure that we disable controls after the first
+    // click... Why not just disable controls altogether you ask? Because then
+    // audio won't play because these days user-interaction is required to enable
+    // audio playback...
+    console.log('onInitialClick');
+
+    deviceScreen.controls = false;
+    deviceScreen.removeEventListener('click', onInitialClick);
+  }
+
+  let videoStream;
+  let mouseIsDown = false;
+  let deviceConnection;
+
+  let logcatBtn = document.getElementById('showLogcatBtn');
+  logcatBtn.onclick = ev => {
+    init_logcat(deviceConnection);
+    logcatBtn.remove();
+  };
+
+  let options = {
+    // temporarily disable audio to free ports in the server since it's only
+    // producing silence anyways.
+    disable_audio: true,
+    wsUrl: ((location.protocol == 'http:') ? 'ws://' : 'wss://') +
+      location.host + '/connect_client',
+    use_tcp,
+  };
+  let urlParams = new URLSearchParams(location.search);
+  for (const [key, value] of urlParams) {
+    options[key] = JSON.parse(value);
+  }
+
+  import('./cf_webrtc.js')
+    .then(webrtcModule => webrtcModule.Connect(device_id, options))
+    .then(devConn => {
+      deviceConnection = devConn;
+      // TODO(b/143667633): get multiple display configuration from the
+      // description object
+      console.log(deviceConnection.description);
+      let stream_id = devConn.description.displays[0].stream_id;
+      devConn.getStream(stream_id).then(stream => {
+        videoStream = stream;
+        deviceScreen.srcObject = videoStream;
+      }).catch(e => console.error('Unable to get display stream: ', e));
+      startMouseTracking();  // TODO stopMouseTracking() when disconnected
+  });
+
+  function onKeyboardCaptureClick(e) {
+    const selectedClass = 'selected';
+    if (keyboardCaptureButton.classList.contains(selectedClass)) {
+      stopKeyboardTracking();
+      keyboardCaptureButton.classList.remove(selectedClass);
+    } else {
+      startKeyboardTracking();
+      keyboardCaptureButton.classList.add(selectedClass);
+    }
+  }
+
+  function startMouseTracking() {
+    if (window.PointerEvent) {
+      deviceScreen.addEventListener('pointerdown', onStartDrag);
+      deviceScreen.addEventListener('pointermove', onContinueDrag);
+      deviceScreen.addEventListener('pointerup', onEndDrag);
+    } else if (window.TouchEvent) {
+      deviceScreen.addEventListener('touchstart', onStartDrag);
+      deviceScreen.addEventListener('touchmove', onContinueDrag);
+      deviceScreen.addEventListener('touchend', onEndDrag);
+    } else if (window.MouseEvent) {
+      deviceScreen.addEventListener('mousedown', onStartDrag);
+      deviceScreen.addEventListener('mousemove', onContinueDrag);
+      deviceScreen.addEventListener('mouseup', onEndDrag);
+    }
+  }
+
+  function stopMouseTracking() {
+    if (window.PointerEvent) {
+      deviceScreen.removeEventListener('pointerdown', onStartDrag);
+      deviceScreen.removeEventListener('pointermove', onContinueDrag);
+      deviceScreen.removeEventListener('pointerup', onEndDrag);
+    } else if (window.TouchEvent) {
+      deviceScreen.removeEventListener('touchstart', onStartDrag);
+      deviceScreen.removeEventListener('touchmove', onContinueDrag);
+      deviceScreen.removeEventListener('touchend', onEndDrag);
+    } else if (window.MouseEvent) {
+      deviceScreen.removeEventListener('mousedown', onStartDrag);
+      deviceScreen.removeEventListener('mousemove', onContinueDrag);
+      deviceScreen.removeEventListener('mouseup', onEndDrag);
+    }
+  }
+
+  function startKeyboardTracking() {
+    document.addEventListener('keydown', onKeyEvent);
+    document.addEventListener('keyup', onKeyEvent);
+  }
+
+  function stopKeyboardTracking() {
+    document.removeEventListener('keydown', onKeyEvent);
+    document.removeEventListener('keyup', onKeyEvent);
+  }
+
+  function onStartDrag(e) {
+    e.preventDefault();
+
+    // console.log("mousedown at " + e.pageX + " / " + e.pageY);
+    mouseIsDown = true;
+
+    sendMouseUpdate(true, e);
+  }
+
+  function onEndDrag(e) {
+    e.preventDefault();
+
+    // console.log("mouseup at " + e.pageX + " / " + e.pageY);
+    mouseIsDown = false;
+
+    sendMouseUpdate(false, e);
+  }
+
+  function onContinueDrag(e) {
+    e.preventDefault();
+
+    // console.log("mousemove at " + e.pageX + " / " + e.pageY + ", down=" +
+    // mouseIsDown);
+    if (mouseIsDown) {
+      sendMouseUpdate(true, e);
+    }
+  }
+
+  function sendMouseUpdate(down, e) {
+    console.assert(deviceConnection, 'Can\'t send mouse update without device');
+    var x = e.offsetX;
+    var y = e.offsetY;
+
+    const videoWidth = deviceScreen.videoWidth;
+    const videoHeight = deviceScreen.videoHeight;
+    const elementWidth = deviceScreen.offsetWidth;
+    const elementHeight = deviceScreen.offsetHeight;
+
+    // vh*ew > eh*vw? then scale h instead of w
+    const scaleHeight = videoHeight * elementWidth > videoWidth * elementHeight;
+    var elementScaling = 0, videoScaling = 0;
+    if (scaleHeight) {
+      elementScaling = elementHeight;
+      videoScaling = videoHeight;
+    } else {
+      elementScaling = elementWidth;
+      videoScaling = videoWidth;
+    }
+
+    // Substract the offset produced by the difference in aspect ratio if any.
+    if (scaleHeight) {
+      x -= (elementWidth - elementScaling * videoWidth / videoScaling) / 2;
+    } else {
+      y -= (elementHeight - elementScaling * videoHeight / videoScaling) / 2;
+    }
+
+    // Convert to coordinates relative to the video
+    x = videoScaling * x / elementScaling;
+    y = videoScaling * y / elementScaling;
+
+    deviceConnection.sendMousePosition(
+        {x: Math.trunc(x), y: Math.trunc(y), down});
+  }
+
+  function onKeyEvent(e) {
+    e.preventDefault();
+    console.assert(deviceConnection, 'Can\'t send key event without device');
+    deviceConnection.sendKeyEvent(e.code, e.type);
+  }
+}
+
+/******************************************************************************/
+
+function ConnectDeviceCb(dev_id, use_tcp) {
+  console.log('Connect: ' + dev_id);
+  // Hide the device selection screen
+  document.getElementById('device_selector').style.display = 'none';
+  // Show the device control screen
+  document.getElementById('device_connection').style.visibility = 'visible';
+  ConnectToDevice(dev_id, use_tcp);
+}
+
+function ShowNewDeviceList(device_ids) {
+  let ul = document.getElementById('device_list');
+  ul.innerHTML = "";
+  for (const dev_id of device_ids) {
+    ul.innerHTML += ('<li class="device_entry" title="Connect to ' + dev_id
+                     + '">' + dev_id + '<button onclick="ConnectDeviceCb(\''
+                     + dev_id + '\', false)">Connect</button><button '
+                     + 'onclick="ConnectDeviceCb(\'' + dev_id + '\', true)"'
+                     + ' title="Useful when a proxy or firewall forbid UDP '
+                     + 'connections">Connect over TCP only</button></li>');
+  }
+}
+
+function UpdateDeviceList() {
+  let url = ((location.protocol == 'http:') ? 'ws:' : 'wss:') + location.host +
+    '/list_devices';
+  let ws = new WebSocket(url);
+  ws.onopen = () => {
+    ws.send("give me those device ids");
+  };
+  ws.onmessage = msg => {
+   let device_ids = JSON.parse(msg.data);
+    ShowNewDeviceList(device_ids);
+  };
+}
+
+// Get any devices that are already connected
+UpdateDeviceList();
+// Update the list at the user's request
+document.getElementById('refresh_list').onclick = evt => UpdateDeviceList();
diff --git a/host/frontend/gcastv2/signaling_server/assets/js/cf_webrtc.js b/host/frontend/gcastv2/signaling_server/assets/js/cf_webrtc.js
new file mode 100644
index 0000000..d5c054d
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/assets/js/cf_webrtc.js
@@ -0,0 +1,431 @@
+// Javascript provides atob() and btoa() for base64 encoding and decoding, but
+// those don't work with binary data.
+class Base64 {
+  static base64Array = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+  static encode(buffer) {
+    let data = new Uint8Array(buffer);
+    let size = data.length;
+    let ret = '';
+    let i = 0;
+    for (; i < size - size%3; i += 3) {
+      let x1 = data[i];
+      let x2 = data[i+1];
+      let x3 = data[i+2];
+
+      let accum = (x1 * 256 + x2 ) * 256 + x3;
+      ret += this.base64Array[(accum  >> 18) % 64];
+      ret += this.base64Array[(accum  >> 12) % 64];
+      ret += this.base64Array[(accum >> 6) % 64];
+      ret += this.base64Array[accum % 64];
+    }
+    switch (size % 3) {
+      case 1:
+        ret += this.base64Array[data[i] >> 2];
+        ret += this.base64Array[(data[i] % 4)*16];
+        ret += '==';
+        break;
+      case 2:
+        ret += this.base64Array[data[i] >> 2];
+        ret += this.base64Array[(data[i] % 4)*16 + (data[i+1] >> 4)];
+        ret += this.base64Array[(data[i] % 16) * 4];
+        ret += '=';
+        break;
+      default:
+        break;
+    }
+    return ret;
+  }
+  static decode(str) {
+    if ((str.length % 4) != 0) {
+      throw "Invalid base 64";
+    }
+    let n = str.length;
+    let padding = 0;
+    if (n >= 1 && str[n-1] === '=') {
+      padding = 1;
+      if (n >= 2 && str[n-2] == '=') {
+        padding = 2;
+      }
+    }
+    let outLen = (3 * n / 4) - padding;
+    let out = new Uint8Array(outLen);
+
+    let j = 0;
+    let accum = 0;
+    for (let i = 0; i < n; i++) {
+      let value = this.base64Array.indexOf(str[i]);
+      if (str[i] === '=') {
+        if (i < n - padding) {
+          throw 'Invalid base 64';
+        }
+        value = 0;
+      } else if (value < 0) {
+        throw "Invalid base 64 char: " + str[i];
+      }
+      accum = accum * 64 + value;
+      if (((i+1)%4) == 0) {
+        out[j++] = accum >> 16;
+        if (j < outLen) {
+          out[j++] = (accum  >> 8) % 256;
+        }
+        if (j < outLen) {
+          out[j++] = accum % 256;
+        }
+        accum = 0;
+      }
+    }
+
+    return out.buffer;
+  }
+}
+
+function createInputDataChannelPromise(pc) {
+  console.log("creating data channel");
+  let inputChannel = pc.createDataChannel('input-channel');
+  return new Promise((resolve, reject) => {
+    inputChannel.onopen = (event) => {
+      resolve(inputChannel);
+    };
+    inputChannel.onclose = () => {
+      console.log(
+          'handleDataChannelStatusChange state=' + dataChannel.readyState);
+    };
+    inputChannel.onmessage = (msg) => {
+      console.log('handleDataChannelMessage data="' + msg.data + '"');
+    };
+    inputChannel.onerror = err => {
+      reject(err);
+    };
+  });
+}
+
+class DeviceConnection {
+  constructor(pc, control) {
+    this._pc = pc;
+    this._control = control;
+    this._inputChannelPr = createInputDataChannelPromise(pc);
+    this._streams = {};
+    this._streamPromiseResolvers = {};
+
+    pc.addEventListener('track', e => {
+      console.log('Got remote stream: ', e);
+      for (const stream of e.streams) {
+        this._streams[stream.id] = stream;
+        if (this._streamPromiseResolvers[stream.id]) {
+          for (let resolver of this._streamPromiseResolvers[stream.id]) {
+            resolver();
+          }
+          delete this._streamPromiseResolvers[stream.id];
+        }
+      }
+    });
+  }
+
+  set description(desc) {
+    this._description = desc;
+  }
+
+  get description() {
+    return this._description;
+  }
+
+  getStream(stream_id) {
+    return new Promise((resolve, reject) => {
+      if (this._streams[stream_id]) {
+        resolve(this._streams[stream_id]);
+      } else {
+        if (!this._streamPromiseResolvers[stream_id]) {
+          this._streamPromiseResolvers[stream_id] = [];
+        }
+        this._streamPromiseResolvers[stream_id].push(resolve);
+      }
+    });
+  }
+
+  _sendJsonInput(evt) {
+    this._inputChannelPr = this._inputChannelPr.then(inputChannel => {
+      inputChannel.send(JSON.stringify(evt));
+      return inputChannel;
+    });
+  }
+
+  sendMousePosition({x, y, down, display = 0}) {
+    this._sendJsonInput({
+      type: 'mouse',
+      down: down ? 1 : 0,
+      x,
+      y,
+    });
+  }
+
+  sendMultiTouch({id, x, y, initialDown, slot, display = 0}) {
+    this._sendJsonInput({
+      type: 'multi-touch',
+      id,
+      x,
+      y,
+      initialDown: initialDown ? 1 : 0,
+      slot,
+    });
+  }
+
+  sendKeyEvent(code, type) {
+    this._sendJsonInput({type: 'keyboard', keycode: code, event_type: type});
+  }
+
+  disconnect() {
+    this._pc.close();
+  }
+
+  // Sends binary data directly to the in-device adb daemon (skipping the host)
+  sendAdbMessage(msg) {
+    // TODO(b/148086548) send over data channel instead of websocket
+    this._control.sendAdbMessage(Base64.encode(msg));
+  }
+
+  // Provide a callback to receive data from the in-device adb daemon
+  onAdbMessage(cb) {
+    // TODO(b/148086548) send over data channel instead of websocket
+    this._control.onAdbMessage(msg => cb(Base64.decode(msg)));
+  }
+}
+
+
+class WebRTCControl {
+  constructor({
+    wsUrl = '',
+    disable_audio = false,
+    bundle_tracks = false,
+    use_tcp = true,
+  }) {
+    /*
+     * Private attributes:
+     *
+     * _options
+     *
+     * _wsPromise: promises the underlying websocket, should resolve when the
+     *             socket passes to OPEN state, will be rejecte/replaced by a
+     *             rejected promise if an error is detected on the socket.
+     *
+     * _onOffer
+     * _onIceCandidate
+     */
+
+    this._options = {
+      disable_audio,
+      bundle_tracks,
+      use_tcp,
+    };
+
+    this._promiseResolvers = {};
+
+    this._wsPromise = new Promise((resolve, reject) => {
+      let ws = new WebSocket(wsUrl);
+      ws.onopen = () => {
+        console.info(`Connected to ${wsUrl}`);
+        resolve(ws);
+      };
+      ws.onerror = evt => {
+        console.error('WebSocket error:', evt);
+        reject(evt);
+        // If the promise was already resolved the previous line has no effect
+        this._wsPromise = Promise.reject(new Error(evt));
+      };
+      ws.onmessage = e => {
+        let data = JSON.parse(e.data);
+        this._onWebsocketMessage(data);
+      };
+    });
+  }
+
+  _onWebsocketMessage(message) {
+    const type = message.message_type;
+    if (message.error) {
+      console.error(message.error);
+      return;
+    }
+    switch (type) {
+      case 'config':
+        this._infra_config = message;
+        break;
+      case 'device_info':
+        if (this._on_device_available) {
+          this._on_device_available(message.device_info);
+          delete this._on_device_available;
+        } else {
+          console.error('Received unsolicited device info');
+        }
+        break;
+      case 'device_msg':
+        this._onDeviceMessage(message.payload);
+        break;
+      default:
+        console.error('Unrecognized message type from server: ', type);
+        console.error(message);
+    }
+  }
+
+  _onDeviceMessage(message) {
+    let type = message.type;
+    switch(type) {
+      case 'offer':
+        if (this._onOffer) {
+          this._onOffer({type: 'offer', sdp: message.sdp});
+        } else {
+          console.error('Receive offer, but nothing is wating for it');
+        }
+        break;
+      case 'ice-candidate':
+        if (this._onIceCandidate) {
+          this._onIceCandidate(new RTCIceCandidate({
+            sdpMid: message.mid,
+            sdpMLineIndex: message.mLineIndex,
+            candidate: message.candidate}));
+        } else {
+          console.error('Received ice candidate but nothing is waiting for it');
+        }
+        break;
+      case 'adb-message':
+        if (this._onAdbMessage) {
+          this._onAdbMessage(message.payload);
+        }
+        break;
+      default:
+        console.error('Unrecognized message type from device: ', type);
+    }
+  }
+
+  async _wsSendJson(obj) {
+    let ws = await this._wsPromise;
+    return ws.send(JSON.stringify(obj));
+  }
+  async _sendToDevice(payload) {
+    this._wsSendJson({message_type: 'forward', payload});
+  }
+
+  onOffer(cb) {
+    this._onOffer = cb;
+  }
+
+  onIceCandidate(cb) {
+    this._onIceCandidate = cb;
+  }
+
+  async requestDevice(device_id) {
+    return new Promise((resolve, reject) => {
+      this._on_device_available = (deviceInfo) => resolve({
+        deviceInfo,
+        infraConfig: this._infra_config,
+      });
+      this._wsSendJson({
+        message_type: 'connect',
+        device_id,
+      });
+    });
+  }
+
+  ConnectDevice() {
+    console.log('ConnectDevice');
+    const is_chrome = navigator.userAgent.indexOf('Chrome') !== -1;
+    this._sendToDevice({type: 'request-offer', options: this._options, is_chrome: is_chrome ? 1 : 0});
+  }
+
+  /**
+   * Sends a remote description to the device.
+   */
+  async sendClientDescription(desc) {
+    console.log('sendClientDescription');
+    this._sendToDevice({type: 'answer', sdp: desc.sdp});
+  }
+
+  /**
+   * Sends an ICE candidate to the device
+   */
+  async sendIceCandidate(candidate) {
+    this._sendToDevice({type: 'ice-candidate', candidate});
+  }
+
+  sendAdbMessage(msg) {
+    this._sendToDevice({type: 'adb-message', payload: msg});
+  }
+
+  onAdbMessage(cb) {
+    this._onAdbMessage = cb;
+  }
+}
+
+function createPeerConnection(infra_config) {
+  let pc_config = {iceServers:[]};
+  for (const stun of infra_config.ice_servers) {
+    pc_config.iceServers.push({urls: 'stun:' + stun});
+  }
+  let pc = new RTCPeerConnection(pc_config);
+
+  pc.addEventListener('icecandidate', evt => {
+    console.log('Local ICE Candidate: ', evt.candidate);
+  });
+  pc.addEventListener('iceconnectionstatechange', evt => {
+    console.log(`ICE State Change: ${pc.iceConnectionState}`);
+  });
+  pc.addEventListener('connectionstatechange', evt =>
+    console.log(`WebRTC Connection State Change: ${pc.connectionState}`));
+  return pc;
+}
+
+export async function Connect(deviceId, options) {
+  let control = new WebRTCControl(options);
+  let requestRet = await control.requestDevice(deviceId);
+  let deviceInfo = requestRet.deviceInfo;
+  let infraConfig = requestRet.infraConfig;
+  console.log("Device available:");
+  console.log(deviceInfo);
+  let pc_config = {
+    iceServers: []
+  };
+  if (infraConfig.ice_servers && infraConfig.ice_servers.length > 0) {
+    for (const server of infraConfig.ice_servers) {
+      pc_config.iceServers.push(server);
+    }
+  }
+  let pc = createPeerConnection(infraConfig, control);
+  let deviceConnection = new DeviceConnection(pc, control);
+  deviceConnection.description = deviceInfo;
+  async function acceptOfferAndReplyAnswer(offer) {
+    try {
+      await pc.setRemoteDescription(offer);
+      let answer = await pc.createAnswer();
+      await pc.setLocalDescription(answer);
+      await control.sendClientDescription(answer);
+    } catch(e) {
+      console.error('Error establishing WebRTC connection: ', e)
+      throw e;
+    }
+  }
+  control.onOffer(desc => {
+      console.log('Offer: ', desc);
+      acceptOfferAndReplyAnswer(desc);
+  });
+  control.onIceCandidate(iceCandidate => {
+    console.log(`Remote ICE Candidate: `, iceCandidate);
+    pc.addIceCandidate(iceCandidate);
+  });
+
+  pc.addEventListener('icecandidate',
+                      evt => {
+                        if (evt.candidate)
+                          control.sendIceCandidate(evt.candidate);
+                      });
+  let connected_promise = new Promise((resolve, reject) => {
+    pc.addEventListener('connectionstatechange', evt => {
+      let state = pc.connectionState;
+      if (state == 'connected') {
+        resolve(deviceConnection);
+      } else if (state == 'failed') {
+        reject(evt);
+      }
+    });
+  });
+  control.ConnectDevice();
+
+  return connected_promise;
+}
diff --git a/host/frontend/gcastv2/webrtc/assets/js/logcat.js b/host/frontend/gcastv2/signaling_server/assets/js/logcat.js
similarity index 88%
rename from host/frontend/gcastv2/webrtc/assets/js/logcat.js
rename to host/frontend/gcastv2/signaling_server/assets/js/logcat.js
index 1753bb6..e1ee94f 100644
--- a/host/frontend/gcastv2/webrtc/assets/js/logcat.js
+++ b/host/frontend/gcastv2/signaling_server/assets/js/logcat.js
@@ -131,7 +131,8 @@
         {
             let payloadText = utf8Decoder.decode(array.slice(24));
 
-            logcat.value += payloadText;
+            // Limit to 100 lines
+            logcat.value = (logcat.value + payloadText).split('\n').slice(-100).join('\n');
 
             // Scroll to bottom
             logcat.scrollTop = logcat.scrollHeight;
@@ -143,20 +144,15 @@
     }
 }
 
-function init_logcat() {
-    const wsProtocol = (location.protocol == "http:") ? "ws:" : "wss:";
-
-    adb_ws = new WebSocket(
-        wsProtocol + "//" + location.host + "/control_adb");
-
-    adb_ws.binaryType = "arraybuffer";
-
-    adb_ws.onopen = function() {
-        console.log("adb_ws: onopen");
-
-        adbOpenConnection();
-
-        logcat.style.display = "initial";
+function init_logcat(devConn) {
+    adb_ws = {
+      send: function(buffer) {
+        devConn.sendAdbMessage(buffer);
+      }
     };
-    adb_ws.onmessage = adbOnMessage;
+
+    logcat.style.display = "initial";
+    devConn.onAdbMessage(msg => adbOnMessage({data: msg}));
+
+    adbOpenConnection();
 }
diff --git a/host/frontend/gcastv2/signaling_server/assets/style.css b/host/frontend/gcastv2/signaling_server/assets/style.css
new file mode 100644
index 0000000..dba8b03
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/assets/style.css
@@ -0,0 +1,64 @@
+body {
+    background-color:black
+}
+
+#device_connection {
+  visibility: hidden;
+}
+
+#device_selector {
+  color: whitesmoke;
+}
+#device_selector li.device_entry {
+  cursor: pointer;
+}
+
+.noscroll {
+    touch-action: none;
+}
+
+.one {
+    float: left;
+}
+
+#logcat {
+    display: none;
+    font-family: monospace;
+    padding: 10px;
+}
+
+button.selected {
+    background-color: #aaaaaa;
+    border-style: solid;
+    border-color: #aaaaaa;
+}
+
+
+.noscroll {
+    text-align: center;
+}
+
+section.noscroll div.one {
+    display: inline-block;
+    width: 100%;
+    height: 90%;
+}
+
+#deviceScreen {
+    max-width: 100%;
+    max-height: 100%;
+}
+
+#refresh_list {
+    cursor: pointer;
+}
+
+#device_list .device_entry button {
+    margin-left: 10px;
+}
+
+#logcat {
+  color: #aaaaaa;
+  background-color: black;
+  margin-top: 40px;
+}
diff --git a/host/frontend/gcastv2/webrtc/certs/create_certs.sh b/host/frontend/gcastv2/signaling_server/certs/create_certs.sh
similarity index 100%
rename from host/frontend/gcastv2/webrtc/certs/create_certs.sh
rename to host/frontend/gcastv2/signaling_server/certs/create_certs.sh
diff --git a/host/frontend/gcastv2/signaling_server/certs/server.crt b/host/frontend/gcastv2/signaling_server/certs/server.crt
new file mode 100644
index 0000000..0b9aa36
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/certs/server.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDVzCCAj8CFBXBydw0e/7l31d9fzO7vrBxAw4yMA0GCSqGSIb3DQEBCwUAMGgx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50
+YSBDbGFyYTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxv
+Y2FsaG9zdDAeFw0yMDA1MDcyMTMzMDFaFw0yMTA1MDcyMTMzMDFaMGgxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50YSBDbGFy
+YTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxvY2FsaG9z
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPdz0Tom1NSujwxYFhG2
+MnqTTU5F9E5OwnO9svlXchXozJSoYpuFG43ZI/9exVmhQKZ4WwJUX74beYuZh611
+S1v9nAiAX+w3lpaiH/9gNH9PaR6kyOTveS9DtHqHlsHm9Ahuls/6mIlHVLsfGVcS
+DDIu5eYqBU0Xq1RYm3+9EUtEOLPQGfcaSUTnI6AkZ55TcJiKhq0CIoTpv/I+7mlw
+zsqPi2f2G7kI47bz1aiXeh34jelKR321fKl1/DW3F0CLSj0/u4gMgNIgPB/tHIKj
+GiNnvJTE7ZDSV34oUmqKhKkUixwjFHUFpMislpIJTsefzaKE4NLa57g5qgAnaofw
+m1UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEASZx0QGNR5DT8vUgEBTMD1OKG3rFw
+zXLI1Lsn5nIMSGkL7aIlx7D8lbdvy0OS+Cg8jE256yiM7cZTF07rKwUeI2v/wDrX
+KP9qfMhonICrbQyKlZ6J4hLVV9wCkYQnMqwS+uSH1l1X+qr3ZCcamgTZ2hrhJFy4
+HEeoC4qdL0+uM2NhrjmPBvqMq9hYWe3nAREmRjSAxBMawjThldLqQCooyvtMskkn
+QAzPte/qvP4kWRpI+KQEv9Rc8iI9PNCF9+W4zl6pIyRDRVYWx3C1PSdniaTc/yDQ
+FL5UbuZ5ujUOdvMy1yAlcTiDVo+Ke7ybAK9FhEBxMPELyTFTY0GVKI46QA==
+-----END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/webrtc/certs/server.key b/host/frontend/gcastv2/signaling_server/certs/server.key
similarity index 100%
rename from host/frontend/gcastv2/webrtc/certs/server.key
rename to host/frontend/gcastv2/signaling_server/certs/server.key
diff --git a/host/frontend/gcastv2/signaling_server/certs/server.p12 b/host/frontend/gcastv2/signaling_server/certs/server.p12
new file mode 100644
index 0000000..87a94c5
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/certs/server.p12
Binary files differ
diff --git a/host/frontend/gcastv2/signaling_server/certs/trusted.pem b/host/frontend/gcastv2/signaling_server/certs/trusted.pem
new file mode 100644
index 0000000..8097b16
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/certs/trusted.pem
@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 1 (0x0)
+        Serial Number:
+            15:c1:c9:dc:34:7b:fe:e5:df:57:7d:7f:33:bb:be:b0:71:03:0e:32
+        Signature Algorithm: sha256WithRSAEncryption
+        Issuer: C = US, ST = California, L = Santa Clara, O = Beyond Aggravated, CN = localhost
+        Validity
+            Not Before: May  7 21:33:01 2020 GMT
+            Not After : May  7 21:33:01 2021 GMT
+        Subject: C = US, ST = California, L = Santa Clara, O = Beyond Aggravated, CN = localhost
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                RSA Public-Key: (2048 bit)
+                Modulus:
+                    00:f7:73:d1:3a:26:d4:d4:ae:8f:0c:58:16:11:b6:
+                    32:7a:93:4d:4e:45:f4:4e:4e:c2:73:bd:b2:f9:57:
+                    72:15:e8:cc:94:a8:62:9b:85:1b:8d:d9:23:ff:5e:
+                    c5:59:a1:40:a6:78:5b:02:54:5f:be:1b:79:8b:99:
+                    87:ad:75:4b:5b:fd:9c:08:80:5f:ec:37:96:96:a2:
+                    1f:ff:60:34:7f:4f:69:1e:a4:c8:e4:ef:79:2f:43:
+                    b4:7a:87:96:c1:e6:f4:08:6e:96:cf:fa:98:89:47:
+                    54:bb:1f:19:57:12:0c:32:2e:e5:e6:2a:05:4d:17:
+                    ab:54:58:9b:7f:bd:11:4b:44:38:b3:d0:19:f7:1a:
+                    49:44:e7:23:a0:24:67:9e:53:70:98:8a:86:ad:02:
+                    22:84:e9:bf:f2:3e:ee:69:70:ce:ca:8f:8b:67:f6:
+                    1b:b9:08:e3:b6:f3:d5:a8:97:7a:1d:f8:8d:e9:4a:
+                    47:7d:b5:7c:a9:75:fc:35:b7:17:40:8b:4a:3d:3f:
+                    bb:88:0c:80:d2:20:3c:1f:ed:1c:82:a3:1a:23:67:
+                    bc:94:c4:ed:90:d2:57:7e:28:52:6a:8a:84:a9:14:
+                    8b:1c:23:14:75:05:a4:c8:ac:96:92:09:4e:c7:9f:
+                    cd:a2:84:e0:d2:da:e7:b8:39:aa:00:27:6a:87:f0:
+                    9b:55
+                Exponent: 65537 (0x10001)
+    Signature Algorithm: sha256WithRSAEncryption
+         49:9c:74:40:63:51:e4:34:fc:bd:48:04:05:33:03:d4:e2:86:
+         de:b1:70:cd:72:c8:d4:bb:27:e6:72:0c:48:69:0b:ed:a2:25:
+         c7:b0:fc:95:b7:6f:cb:43:92:f8:28:3c:8c:4d:b9:eb:28:8c:
+         ed:c6:53:17:4e:eb:2b:05:1e:23:6b:ff:c0:3a:d7:28:ff:6a:
+         7c:c8:68:9c:80:ab:6d:0c:8a:95:9e:89:e2:12:d5:57:dc:02:
+         91:84:27:32:ac:12:fa:e4:87:d6:5d:57:fa:aa:f7:64:27:1a:
+         9a:04:d9:da:1a:e1:24:5c:b8:1c:47:a8:0b:8a:9d:2f:4f:ae:
+         33:63:61:ae:39:8f:06:fa:8c:ab:d8:58:59:ed:e7:01:11:26:
+         46:34:80:c4:13:1a:c2:34:e1:95:d2:ea:40:2a:28:ca:fb:4c:
+         b2:49:27:40:0c:cf:b5:ef:ea:bc:fe:24:59:1a:48:f8:a4:04:
+         bf:d4:5c:f2:22:3d:3c:d0:85:f7:e5:b8:ce:5e:a9:23:24:43:
+         45:56:16:c7:70:b5:3d:27:67:89:a4:dc:ff:20:d0:14:be:54:
+         6e:e6:79:ba:35:0e:76:f3:32:d7:20:25:71:38:83:56:8f:8a:
+         7b:bc:9b:00:af:45:84:40:71:30:f1:0b:c9:31:53:63:41:95:
+         28:8e:3a:40
+-----BEGIN CERTIFICATE-----
+MIIDVzCCAj8CFBXBydw0e/7l31d9fzO7vrBxAw4yMA0GCSqGSIb3DQEBCwUAMGgx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50
+YSBDbGFyYTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxv
+Y2FsaG9zdDAeFw0yMDA1MDcyMTMzMDFaFw0yMTA1MDcyMTMzMDFaMGgxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQHDAtTYW50YSBDbGFy
+YTEaMBgGA1UECgwRQmV5b25kIEFnZ3JhdmF0ZWQxEjAQBgNVBAMMCWxvY2FsaG9z
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPdz0Tom1NSujwxYFhG2
+MnqTTU5F9E5OwnO9svlXchXozJSoYpuFG43ZI/9exVmhQKZ4WwJUX74beYuZh611
+S1v9nAiAX+w3lpaiH/9gNH9PaR6kyOTveS9DtHqHlsHm9Ahuls/6mIlHVLsfGVcS
+DDIu5eYqBU0Xq1RYm3+9EUtEOLPQGfcaSUTnI6AkZ55TcJiKhq0CIoTpv/I+7mlw
+zsqPi2f2G7kI47bz1aiXeh34jelKR321fKl1/DW3F0CLSj0/u4gMgNIgPB/tHIKj
+GiNnvJTE7ZDSV34oUmqKhKkUixwjFHUFpMislpIJTsefzaKE4NLa57g5qgAnaofw
+m1UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEASZx0QGNR5DT8vUgEBTMD1OKG3rFw
+zXLI1Lsn5nIMSGkL7aIlx7D8lbdvy0OS+Cg8jE256yiM7cZTF07rKwUeI2v/wDrX
+KP9qfMhonICrbQyKlZ6J4hLVV9wCkYQnMqwS+uSH1l1X+qr3ZCcamgTZ2hrhJFy4
+HEeoC4qdL0+uM2NhrjmPBvqMq9hYWe3nAREmRjSAxBMawjThldLqQCooyvtMskkn
+QAzPte/qvP4kWRpI+KQEv9Rc8iI9PNCF9+W4zl6pIyRDRVYWx3C1PSdniaTc/yDQ
+FL5UbuZ5ujUOdvMy1yAlcTiDVo+Ke7ybAK9FhEBxMPELyTFTY0GVKI46QA==
+-----END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/signaling_server/client_handler.cpp b/host/frontend/gcastv2/signaling_server/client_handler.cpp
new file mode 100644
index 0000000..3643528
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/client_handler.cpp
@@ -0,0 +1,105 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "host/frontend/gcastv2/signaling_server/client_handler.h"
+
+#include <android-base/logging.h>
+
+#include "host/frontend/gcastv2/signaling_server/constants/signaling_constants.h"
+#include "host/frontend/gcastv2/signaling_server/device_handler.h"
+
+namespace cvd {
+
+ClientHandler::ClientHandler(DeviceRegistry* registry,
+                             const ServerConfig& server_config)
+    : SignalHandler(registry, server_config),
+      device_handler_(),
+      client_id_(0) {}
+
+void ClientHandler::SendDeviceMessage(const Json::Value& device_message) {
+  Json::Value message;
+  message[webrtc_signaling::kTypeField] = webrtc_signaling::kDeviceMessageType;
+  message[webrtc_signaling::kPayloadField] = device_message;
+  Reply(message);
+}
+
+int ClientHandler::handleMessage(const std::string& type,
+                                 const Json::Value& message) {
+  if (type == webrtc_signaling::kConnectType) {
+    return handleConnectionRequest(message);
+  } else if (type == webrtc_signaling::kForwardType) {
+    return handleForward(message);
+  } else {
+    LogAndReplyError("Unknown message type: " + type);
+    return -1;
+  }
+}
+
+int ClientHandler::handleConnectionRequest(const Json::Value& message) {
+  if (client_id_ > 0) {
+    LOG(ERROR) << "Detected attempt to connect to multiple devices over same "
+                  "websocket";
+    return -EINVAL;
+  }
+  if (!message.isMember(webrtc_signaling::kDeviceIdField) ||
+      !message[webrtc_signaling::kDeviceIdField].isString()) {
+    LogAndReplyError("Invalid connection request: Missing device id");
+    return -EINVAL;
+  }
+  auto device_id = message[webrtc_signaling::kDeviceIdField].asString();
+  // Always send the server config back, even if the requested device is not
+  // registered. Applications may put clients on hold until the device is ready
+  // to connect.
+  SendServerConfig();
+
+  auto device_handler = registry_->GetDevice(device_id);
+  if (!device_handler) {
+    LogAndReplyError("Connection failed: Device not found: '" + device_id +
+                     "'");
+    return -1;
+  }
+
+  client_id_ = device_handler->RegisterClient(shared_from_this());
+  device_handler_ = device_handler;
+  Json::Value device_info_reply;
+  device_info_reply[webrtc_signaling::kTypeField] =
+      webrtc_signaling::kDeviceInfoType;
+  device_info_reply[webrtc_signaling::kDeviceInfoField] =
+      device_handler->device_info();
+  Reply(device_info_reply);
+  return 0;
+}
+
+int ClientHandler::handleForward(const Json::Value& message) {
+  if (client_id_ == 0) {
+    LogAndReplyError("Forward failed: No device asociated to client");
+    return 0;
+  }
+  if (!message.isMember(webrtc_signaling::kPayloadField)) {
+    LogAndReplyError("Forward failed: No payload present in message");
+    return 0;
+  }
+  auto device_handler = device_handler_.lock();
+  if (!device_handler) {
+    LogAndReplyError("Forward failed: Device disconnected");
+    // Disconnect this client since the device is gone
+    return -1;
+  }
+  device_handler->SendClientMessage(client_id_,
+                                    message[webrtc_signaling::kPayloadField]);
+  return 0;
+}
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/client_handler.h b/host/frontend/gcastv2/signaling_server/client_handler.h
new file mode 100644
index 0000000..afd3c7c
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/client_handler.h
@@ -0,0 +1,48 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <json/json.h>
+
+#include "host/frontend/gcastv2/https/include/https/WebSocketHandler.h"
+#include "host/frontend/gcastv2/signaling_server/device_registry.h"
+#include "host/frontend/gcastv2/signaling_server/server_config.h"
+#include "host/frontend/gcastv2/signaling_server/signal_handler.h"
+
+namespace cvd {
+class DeviceHandler;
+class ClientHandler : public SignalHandler,
+                      public std::enable_shared_from_this<ClientHandler> {
+ public:
+  ClientHandler(DeviceRegistry* registry, const ServerConfig& server_config);
+  void SendDeviceMessage(const Json::Value& message);
+ protected:
+  int handleMessage(const std::string& type,
+                    const Json::Value& message) override;
+
+ private:
+  int handleConnectionRequest(const Json::Value& message);
+  int handleForward(const Json::Value& message);
+
+  std::weak_ptr<DeviceHandler> device_handler_;
+  // The device handler assigns this to each client to be able to differentiate
+  // them.
+  size_t client_id_;
+};
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/constants/signaling_constants.h b/host/frontend/gcastv2/signaling_server/constants/signaling_constants.h
new file mode 100644
index 0000000..b3b2870
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/constants/signaling_constants.h
@@ -0,0 +1,42 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+namespace cvd {
+namespace webrtc_signaling {
+
+constexpr auto kTypeField = "message_type";
+constexpr auto kDeviceInfoField = "device_info";
+constexpr auto kDeviceIdField = "device_id";
+constexpr auto kClientIdField = "client_id";
+constexpr auto kPayloadField = "payload";
+constexpr auto kServersField = "ice_servers";
+// These are defined in the IceServer dictionary
+constexpr auto kUrlsField = "urls";
+constexpr auto kUsernameField = "username";
+constexpr auto kCredentialField = "credential";
+constexpr auto kCredentialTypeField = "credentialType";
+
+constexpr auto kRegisterType = "register";
+constexpr auto kForwardType = "forward";
+constexpr auto kConfigType = "config";
+constexpr auto kConnectType = "connect";
+constexpr auto kDeviceInfoType = "device_info";
+constexpr auto kClientMessageType = "client_msg";
+constexpr auto kDeviceMessageType = "device_msg";
+
+}  // namespace webrtc_signaling
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/device_handler.cpp b/host/frontend/gcastv2/signaling_server/device_handler.cpp
new file mode 100644
index 0000000..b48f6c9
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/device_handler.cpp
@@ -0,0 +1,115 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "host/frontend/gcastv2/signaling_server/device_handler.h"
+
+#include <android-base/logging.h>
+
+#include "host/frontend/gcastv2/signaling_server/client_handler.h"
+#include "host/frontend/gcastv2/signaling_server/constants/signaling_constants.h"
+
+namespace cvd {
+
+DeviceHandler::DeviceHandler(DeviceRegistry* registry,
+                             const ServerConfig& server_config)
+    : SignalHandler(registry, server_config), device_info_(), clients_() {}
+
+DeviceHandler::~DeviceHandler() {
+  // Unregister the device when the websocket connection is closed
+  if (!device_id_.empty() && registry_) {
+    registry_->UnRegisterDevice(device_id_);
+  }
+}
+
+size_t DeviceHandler::RegisterClient(
+    std::shared_ptr<ClientHandler> client_handler) {
+  clients_.emplace_back(client_handler);
+  return clients_.size();
+}
+
+int DeviceHandler::handleMessage(const std::string& type,
+                                 const Json::Value& message) {
+  if (type == webrtc_signaling::kRegisterType) {
+    return HandleRegistrationRequest(message);
+  } else if (type == webrtc_signaling::kForwardType) {
+    return HandleForward(message);
+  } else {
+    LogAndReplyError("Unknown message type: " + type);
+  }
+
+  return 0;
+}
+
+int DeviceHandler::HandleRegistrationRequest(const Json::Value& message) {
+  if (!device_id_.empty()) {
+    LogAndReplyError("Device already registered: " + device_id_);
+    return -EINVAL;
+  }
+  if (!message.isMember(webrtc_signaling::kDeviceIdField) ||
+      !message[webrtc_signaling::kDeviceIdField].isString() ||
+      message[webrtc_signaling::kDeviceIdField].asString().empty()) {
+    LogAndReplyError("Missing device id in registration request");
+    return -EINVAL;
+  }
+  device_id_ = message[webrtc_signaling::kDeviceIdField].asString();
+  if (message.isMember(webrtc_signaling::kDeviceInfoField)) {
+    device_info_ = message[webrtc_signaling::kDeviceInfoField];
+  }
+  if (!registry_->RegisterDevice(device_id_, weak_from_this())) {
+    LOG(ERROR) << "Device registration failed";
+    return -1;
+  }
+
+  SendServerConfig();
+
+  return 0;
+}
+
+int DeviceHandler::HandleForward(const Json::Value& message) {
+  if (!message.isMember(webrtc_signaling::kClientIdField) || !message[webrtc_signaling::kClientIdField].isInt()) {
+    LogAndReplyError("Forward failed: Missing or invalid client id");
+    return 0;
+  }
+  size_t client_id = message[webrtc_signaling::kClientIdField].asInt();
+  if (!message.isMember(webrtc_signaling::kPayloadField)) {
+    LogAndReplyError("Forward failed: Missing payload");
+    return 0;
+  }
+  if (client_id > clients_.size()) {
+    LogAndReplyError("Forward failed: Unknown client " +
+                     std::to_string(client_id));
+    return 0;
+  }
+  auto client_index = client_id - 1;
+  auto client_handler = clients_[client_index].lock();
+  if (!client_handler) {
+    LogAndReplyError("Forward failed: Client " + std::to_string(client_id) +
+                     " disconnected");
+    return 0;
+  }
+  client_handler->SendDeviceMessage(message[webrtc_signaling::kPayloadField]);
+  return 0;
+}
+
+void DeviceHandler::SendClientMessage(size_t client_id,
+                                      const Json::Value& client_message) {
+  Json::Value msg;
+  msg[webrtc_signaling::kTypeField] = webrtc_signaling::kClientMessageType;
+  msg[webrtc_signaling::kClientIdField] = static_cast<Json::UInt>(client_id);
+  msg[webrtc_signaling::kPayloadField] = client_message;
+  Reply(msg);
+}
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/device_handler.h b/host/frontend/gcastv2/signaling_server/device_handler.h
new file mode 100644
index 0000000..b94a38a
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/device_handler.h
@@ -0,0 +1,56 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <json/json.h>
+
+#include "host/frontend/gcastv2/https/include/https/WebSocketHandler.h"
+#include "host/frontend/gcastv2/signaling_server/device_registry.h"
+#include "host/frontend/gcastv2/signaling_server/server_config.h"
+#include "host/frontend/gcastv2/signaling_server/signal_handler.h"
+
+namespace cvd {
+
+class ClientHandler;
+
+class DeviceHandler
+    : public SignalHandler,
+      public std::enable_shared_from_this<DeviceHandler> {
+ public:
+  DeviceHandler(DeviceRegistry* registry, const ServerConfig& server_config);
+  ~DeviceHandler() override;
+
+  Json::Value device_info() const { return device_info_; }
+
+  size_t RegisterClient(std::shared_ptr<ClientHandler> client_handler);
+  void SendClientMessage(size_t client_id, const Json::Value& message);
+ protected:
+  int handleMessage(const std::string& type,
+                    const Json::Value& message) override;
+
+ private:
+  int HandleRegistrationRequest(const Json::Value& message);
+  int HandleForward(const Json::Value& message);
+
+  std::string device_id_;
+  Json::Value device_info_;
+  std::vector<std::weak_ptr<ClientHandler>> clients_;
+};
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/device_list_handler.cpp b/host/frontend/gcastv2/signaling_server/device_list_handler.cpp
new file mode 100644
index 0000000..ac946ae
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/device_list_handler.cpp
@@ -0,0 +1,38 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "host/frontend/gcastv2/signaling_server/device_list_handler.h"
+
+namespace cvd {
+
+DeviceListHandler::DeviceListHandler(const DeviceRegistry& registry)
+    : registry_(registry) {}
+
+int DeviceListHandler::handleMessage(uint8_t /*header_byte*/,
+                                     const uint8_t* /*msg*/,
+                                     size_t /*len*/) {
+  // ignore the message, just send the reply
+  Json::Value reply(Json::ValueType::arrayValue);
+
+  for (const auto& id : registry_.ListDeviceIds()) {
+    reply.append(id);
+  }
+  Json::FastWriter json_writer;
+  auto replyAsString = json_writer.write(reply);
+  sendMessage(replyAsString.c_str(), replyAsString.size());
+  return -1;  // disconnect
+}
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/device_list_handler.h b/host/frontend/gcastv2/signaling_server/device_list_handler.h
new file mode 100644
index 0000000..0d099f1
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/device_list_handler.h
@@ -0,0 +1,38 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <json/json.h>
+
+#include "host/frontend/gcastv2/https/include/https/WebSocketHandler.h"
+#include "host/frontend/gcastv2/signaling_server/device_registry.h"
+
+namespace cvd {
+
+class DeviceListHandler : public WebSocketHandler {
+ public:
+  DeviceListHandler(const DeviceRegistry& registry);
+
+ protected:
+  int handleMessage(uint8_t, const uint8_t*, size_t) override;
+
+ private:
+  const DeviceRegistry& registry_;
+};
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/device_registry.cpp b/host/frontend/gcastv2/signaling_server/device_registry.cpp
new file mode 100644
index 0000000..f5dc362
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/device_registry.cpp
@@ -0,0 +1,71 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "host/frontend/gcastv2/signaling_server/device_registry.h"
+
+#include <android-base/logging.h>
+
+#include "host/frontend/gcastv2/signaling_server/device_handler.h"
+
+namespace cvd {
+
+bool DeviceRegistry::RegisterDevice(
+    const std::string& device_id,
+    std::weak_ptr<DeviceHandler> device_handler) {
+  if (devices_.count(device_id) > 0) {
+    LOG(ERROR) << "Device '" << device_id << "' is already registered";
+    return false;
+  }
+
+  devices_.try_emplace(device_id, device_handler);
+  LOG(INFO) << "Registered device: '" << device_id << "'";
+  return true;
+}
+
+void DeviceRegistry::UnRegisterDevice(const std::string& device_id) {
+  auto record = devices_.find(device_id);
+  if (record == devices_.end()) {
+    LOG(WARNING) << "Requested to unregister an unkwnown device: '" << device_id
+                 << "'";
+    return;
+  }
+  devices_.erase(record);
+  LOG(INFO) << "Unregistered device: '" << device_id << "'";
+}
+
+std::shared_ptr<DeviceHandler> DeviceRegistry::GetDevice(
+    const std::string& device_id) {
+  if (devices_.count(device_id) == 0) {
+    LOG(INFO) << "Requested device (" << device_id << ") is not registered";
+    return nullptr;
+  }
+  auto device_handler = devices_[device_id].lock();
+  if (!device_handler) {
+    LOG(WARNING) << "Destroyed device handler detected for device '"
+                 << device_id << "'";
+    UnRegisterDevice(device_id);
+  }
+  return device_handler;
+}
+
+std::vector<std::string> DeviceRegistry::ListDeviceIds() const {
+  std::vector<std::string> ret;
+  for (const auto& entry: devices_) {
+    ret.push_back(entry.first);
+  }
+  return ret;
+}
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/device_registry.h b/host/frontend/gcastv2/signaling_server/device_registry.h
new file mode 100644
index 0000000..0d2a46b
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/device_registry.h
@@ -0,0 +1,45 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <cinttypes>
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <json/json.h>
+
+namespace cvd {
+
+class DeviceHandler;
+
+class DeviceRegistry {
+ public:
+  bool RegisterDevice(const std::string& device_id,
+                      std::weak_ptr<DeviceHandler> device_handler);
+  void UnRegisterDevice(const std::string& device_id);
+
+  std::shared_ptr<DeviceHandler> GetDevice(const std::string& device_id);
+
+  std::vector<std::string> ListDeviceIds() const;
+
+ private:
+  std::map<std::string, std::weak_ptr<DeviceHandler>> devices_;
+};
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/server.cpp b/host/frontend/gcastv2/signaling_server/server.cpp
new file mode 100644
index 0000000..8fdf4c1
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/server.cpp
@@ -0,0 +1,122 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <android-base/logging.h>
+#include <gflags/gflags.h>
+#include <netdb.h>
+#include <openssl/bio.h>
+#include <openssl/err.h>
+#include <openssl/pem.h>
+#include <openssl/ssl.h>
+
+#include "host/frontend/gcastv2/https/include/https/HTTPServer.h"
+#include "host/frontend/gcastv2/https/include/https/PlainSocket.h"
+#include "host/frontend/gcastv2/https/include/https/RunLoop.h"
+#include "host/frontend/gcastv2/https/include/https/SSLSocket.h"
+#include "host/frontend/gcastv2/https/include/https/SafeCallbackable.h"
+#include "host/frontend/gcastv2/https/include/https/Support.h"
+#include "host/frontend/gcastv2/webrtc/Utils.h"
+#include "host/frontend/gcastv2/webrtc/include/webrtc/STUNClient.h"
+#include "host/frontend/gcastv2/webrtc/include/webrtc/STUNMessage.h"
+
+#include "host/frontend/gcastv2/signaling_server/client_handler.h"
+#include "host/frontend/gcastv2/signaling_server/device_handler.h"
+#include "host/frontend/gcastv2/signaling_server/device_list_handler.h"
+#include "host/frontend/gcastv2/signaling_server/device_registry.h"
+#include "host/frontend/gcastv2/signaling_server/server_config.h"
+
+DEFINE_int32(http_server_port, 8443, "The port for the http server.");
+DEFINE_bool(use_secure_http, true, "Whether to use HTTPS or HTTP.");
+DEFINE_string(assets_dir, "webrtc",
+              "Directory with location of webpage assets.");
+DEFINE_string(certs_dir, "webrtc/certs", "Directory to certificates.");
+DEFINE_string(stun_server, "stun.l.google.com:19302",
+              "host:port of STUN server to use for public address resolution");
+
+namespace {
+
+void InitSSL() {
+  SSL_library_init();
+  SSL_load_error_strings();
+}
+
+void ServeStaticFiles(std::shared_ptr<HTTPServer> httpd) {
+  const std::string index_html = FLAGS_assets_dir + "/index.html";
+  const std::string logcat_js = FLAGS_assets_dir + "/js/logcat.js";
+  const std::string app_js = FLAGS_assets_dir + "/js/app.js";
+  const std::string viewpane_js = FLAGS_assets_dir + "/js/viewpane.js";
+  const std::string cf_webrtc_js = FLAGS_assets_dir + "/js/cf_webrtc.js";
+  const std::string style_css = FLAGS_assets_dir + "/style.css";
+
+  httpd->addStaticFile("/index.html", index_html.c_str());
+  httpd->addStaticFile("/js/logcat.js", logcat_js.c_str());
+  httpd->addStaticFile("/js/app.js", app_js.c_str());
+  httpd->addStaticFile("/js/viewpane.js", viewpane_js.c_str());
+  httpd->addStaticFile("/js/cf_webrtc.js", cf_webrtc_js.c_str());
+  httpd->addStaticFile("/style.css", style_css.c_str());
+}
+
+}  // namespace
+
+int main(int argc, char **argv) {
+  ::gflags::ParseCommandLineFlags(&argc, &argv, true);
+  ::android::base::InitLogging(argv, android::base::StderrLogger);
+
+  InitSSL();
+
+  auto run_loop = RunLoop::main();
+
+  auto port = FLAGS_http_server_port;
+ /******************************************************************************
+  * WARNING!: The device registry doesn't need synchronization because it runs *
+  * in this run_loop. If a different run_loop or server implementation is used *
+  * synchronization all over needs to be revised.                              *
+  *****************************************************************************/
+
+  auto httpd = std::make_shared<HTTPServer>(
+      run_loop, "0.0.0.0", port,
+      FLAGS_use_secure_http ? ServerSocket::TransportType::TLS
+                            : ServerSocket::TransportType::TCP,
+      FLAGS_certs_dir + "/server.crt", FLAGS_certs_dir + "/server.key");
+
+  ServeStaticFiles(httpd);
+
+  cvd::ServerConfig server_config({FLAGS_stun_server});
+  cvd::DeviceRegistry device_registry;
+
+  httpd->addWebSocketHandlerFactory(
+      "/register_device", [&device_registry, &server_config] {
+        return std::make_pair(0, std::make_shared<cvd::DeviceHandler>(
+                                     &device_registry, server_config));
+      });
+
+  httpd->addWebSocketHandlerFactory(
+      "/connect_client", [&device_registry, &server_config] {
+        return std::make_pair(0, std::make_shared<cvd::ClientHandler>(
+                                     &device_registry, server_config));
+      });
+
+  // This is non-standard utility endpoint, it's the simplest way for clients to
+  // obtain the ids of registered devices.
+  httpd->addWebSocketHandlerFactory("/list_devices", [&device_registry] {
+    return std::make_pair(
+        0, std::make_shared<cvd::DeviceListHandler>(device_registry));
+  });
+
+  httpd->run();
+  run_loop->run();
+
+  return 0;
+}
diff --git a/host/frontend/gcastv2/signaling_server/server_config.cpp b/host/frontend/gcastv2/signaling_server/server_config.cpp
new file mode 100644
index 0000000..6fa880a
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/server_config.cpp
@@ -0,0 +1,43 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "host/frontend/gcastv2/signaling_server/server_config.h"
+
+#include <android-base/strings.h>
+
+using android::base::StartsWith;
+
+namespace cvd {
+
+namespace {
+  constexpr auto kStunPrefix = "stun:";
+}
+
+ServerConfig::ServerConfig(const std::vector<std::string>& stuns)
+    : stun_servers_(stuns) {}
+
+Json::Value ServerConfig::ToJson() const {
+  Json::Value ice_servers(Json::ValueType::arrayValue);
+  for (const auto& str : stun_servers_) {
+    Json::Value server;
+    server["urls"] = StartsWith(str, kStunPrefix)? str: kStunPrefix + str;
+    ice_servers.append(server);
+  }
+  Json::Value server_config;
+  server_config["ice_servers"] = ice_servers;
+  return server_config;
+}
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/server_config.h b/host/frontend/gcastv2/signaling_server/server_config.h
new file mode 100644
index 0000000..d4a42a5
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/server_config.h
@@ -0,0 +1,33 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <json/json.h>
+
+namespace cvd {
+class ServerConfig {
+ public:
+  ServerConfig(const std::vector<std::string>& stuns);
+
+  Json::Value ToJson() const;
+
+ private:
+  std::vector<std::string> stun_servers_;
+};
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/signal_handler.cpp b/host/frontend/gcastv2/signaling_server/signal_handler.cpp
new file mode 100644
index 0000000..1df26cc
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/signal_handler.cpp
@@ -0,0 +1,79 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "host/frontend/gcastv2/signaling_server/signal_handler.h"
+
+#include <android-base/logging.h>
+#include <json/json.h>
+
+#include "host/frontend/gcastv2/signaling_server/constants/signaling_constants.h"
+
+namespace cvd {
+
+SignalHandler::SignalHandler(DeviceRegistry* registry,
+                             const ServerConfig& server_config)
+    : registry_(registry), server_config_(server_config) {}
+
+bool SignalHandler::IsBinaryMessage(uint8_t header_byte) {
+  // https://tools.ietf.org/html/rfc6455#section-5.2
+  return (header_byte & 0x0f) == 0x02;
+}
+
+int SignalHandler::handleMessage(uint8_t header_byte, const uint8_t* msg,
+                                 size_t len) {
+  if (IsBinaryMessage(header_byte)) {
+    LOG(ERROR) << "Received a binary message";
+    return -EINVAL;
+  }
+  Json::Value json_message;
+  Json::Reader json_reader;
+  auto str = reinterpret_cast<const char*>(msg);
+  if (!json_reader.parse(str, str + len, json_message)) {
+    LOG(ERROR) << "Received Invalid JSON";
+    // Rate limiting would be a good idea here
+    return -EINVAL;
+  }
+  if (!json_message.isMember(webrtc_signaling::kTypeField) ||
+      !json_message[webrtc_signaling::kTypeField].isString()) {
+    LogAndReplyError("Invalid message format: '" + std::string(msg, msg + len) +
+                     "'");
+    // Rate limiting would be a good idea here
+    return -EINVAL;
+  }
+
+  auto type = json_message[webrtc_signaling::kTypeField].asString();
+  return handleMessage(type, json_message);
+}
+
+void SignalHandler::SendServerConfig() {
+  // Call every time to allow config changes?
+  auto reply = server_config_.ToJson();
+  reply[webrtc_signaling::kTypeField] = webrtc_signaling::kConfigType;
+  Reply(reply);
+}
+
+void SignalHandler::LogAndReplyError(const std::string& error_message) {
+  LOG(ERROR) << error_message;
+  auto reply_str = "{\"error\":\"" + error_message + "\"}";
+  sendMessage(reply_str.c_str(), reply_str.size());
+}
+
+void SignalHandler::Reply(const Json::Value& json) {
+  Json::FastWriter json_writer;
+  auto replyAsString = json_writer.write(json);
+  sendMessage(replyAsString.c_str(), replyAsString.size());
+}
+
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/signaling_server/signal_handler.h b/host/frontend/gcastv2/signaling_server/signal_handler.h
new file mode 100644
index 0000000..c0bf96f
--- /dev/null
+++ b/host/frontend/gcastv2/signaling_server/signal_handler.h
@@ -0,0 +1,47 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include <json/json.h>
+
+#include "host/frontend/gcastv2/https/include/https/WebSocketHandler.h"
+#include "host/frontend/gcastv2/signaling_server/device_registry.h"
+#include "host/frontend/gcastv2/signaling_server/server_config.h"
+
+namespace cvd {
+
+class SignalHandler : public WebSocketHandler {
+ protected:
+  SignalHandler(DeviceRegistry* registry, const ServerConfig& server_config);
+
+  bool IsBinaryMessage(uint8_t header_byte);
+
+  int handleMessage(uint8_t header_byte, const uint8_t* msg,
+                    size_t len) override;
+  virtual int handleMessage(const std::string& message_type,
+                            const Json::Value& message) = 0;
+  void SendServerConfig();
+
+  void LogAndReplyError(const std::string& message);
+  void Reply(const Json::Value& json);
+
+  DeviceRegistry* registry_;
+  const ServerConfig& server_config_;
+};
+}  // namespace cvd
diff --git a/host/frontend/gcastv2/webrtc/AdbWebSocketHandler.cpp b/host/frontend/gcastv2/webrtc/AdbHandler.cpp
similarity index 68%
rename from host/frontend/gcastv2/webrtc/AdbWebSocketHandler.cpp
rename to host/frontend/gcastv2/webrtc/AdbHandler.cpp
index e663d27..9d4c5c8 100644
--- a/host/frontend/gcastv2/webrtc/AdbWebSocketHandler.cpp
+++ b/host/frontend/gcastv2/webrtc/AdbHandler.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include <webrtc/AdbWebSocketHandler.h>
+#include <webrtc/AdbHandler.h>
 
 #include "Utils.h"
 
@@ -27,26 +27,26 @@
 
 using namespace android;
 
-struct AdbWebSocketHandler::AdbConnection : public BaseConnection {
+struct AdbHandler::AdbConnection : public BaseConnection {
     explicit AdbConnection(
-            AdbWebSocketHandler *parent,
+            AdbHandler *parent,
             std::shared_ptr<RunLoop> runLoop,
             int sock);
 
     void send(const void *_data, size_t size);
 
 protected:
-    ssize_t processClientRequest(const void *data, size_t size) override;
+    long processClientRequest(const void *data, size_t size) override;
     void onDisconnect(int err) override;
 
 private:
-    AdbWebSocketHandler *mParent;
+    AdbHandler *mParent;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
 
-AdbWebSocketHandler::AdbConnection::AdbConnection(
-        AdbWebSocketHandler *parent,
+AdbHandler::AdbConnection::AdbConnection(
+        AdbHandler *parent,
         std::shared_ptr<RunLoop> runLoop,
         int sock)
     : BaseConnection(runLoop, sock),
@@ -99,7 +99,7 @@
     return 0;
 }
 
-ssize_t AdbWebSocketHandler::AdbConnection::processClientRequest(
+long AdbHandler::AdbConnection::processClientRequest(
         const void *_data, size_t size) {
     auto data = static_cast<const uint8_t *>(_data);
 
@@ -115,32 +115,29 @@
         return err;
     }
 
-    mParent->sendMessage(
-            data, payloadLength + 24, WebSocketHandler::SendMode::binary);
-
+    mParent->send_to_client_(data, payloadLength + 24);
     return payloadLength + 24;
 }
 
-void AdbWebSocketHandler::AdbConnection::onDisconnect(int err) {
+void AdbHandler::AdbConnection::onDisconnect(int err) {
     LOG(INFO) << "AdbConnection::onDisconnect(err=" << err << ")";
 
-    mParent->sendMessage(
-            nullptr /* data */,
-            0 /* size */,
-            WebSocketHandler::SendMode::closeConnection);
+    mParent->send_to_client_(nullptr /* data */, 0 /* size */);
 }
 
-void AdbWebSocketHandler::AdbConnection::send(const void *_data, size_t size) {
+void AdbHandler::AdbConnection::send(const void *_data, size_t size) {
     BaseConnection::send(_data, size);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 
-AdbWebSocketHandler::AdbWebSocketHandler(
+AdbHandler::AdbHandler(
         std::shared_ptr<RunLoop> runLoop,
-        const std::string &adb_host_and_port)
+        const std::string &adb_host_and_port,
+        std::function<void(const uint8_t*, size_t)>  send_to_client)
     : mRunLoop(runLoop),
-      mSocket(-1) {
+      mSocket(-1),
+      send_to_client_(send_to_client) {
     LOG(INFO) << "Connecting to " << adb_host_and_port;
 
     auto err = setupSocket(adb_host_and_port);
@@ -149,18 +146,18 @@
     mAdbConnection = std::make_shared<AdbConnection>(this, mRunLoop, mSocket);
 }
 
-AdbWebSocketHandler::~AdbWebSocketHandler() {
+AdbHandler::~AdbHandler() {
     if (mSocket >= 0) {
         close(mSocket);
         mSocket = -1;
     }
 }
 
-void AdbWebSocketHandler::run() {
+void AdbHandler::run() {
     mAdbConnection->run();
 }
 
-int AdbWebSocketHandler::setupSocket(const std::string &adb_host_and_port) {
+int AdbHandler::setupSocket(const std::string &adb_host_and_port) {
     auto colonPos = adb_host_and_port.find(':');
     if (colonPos == std::string::npos) {
         return -EINVAL;
@@ -213,47 +210,17 @@
     return err;
 }
 
-int AdbWebSocketHandler::handleMessage(
-        uint8_t headerByte, const uint8_t *msg, size_t len) {
-    LOG(VERBOSE)
-        << "headerByte = "
-        << StringPrintf("0x%02x", (unsigned)headerByte);
-
+void AdbHandler::handleMessage(const uint8_t *msg, size_t len) {
     LOG(VERBOSE) << hexdump(msg, len);
 
-    if (!(headerByte & 0x80)) {
-        // I only want to receive whole messages here, not fragments.
-        return -EINVAL;
+    size_t payloadLength;
+    int err = verifyAdbHeader(msg, len, &payloadLength);
+
+    if (err || len != 24 + payloadLength) {
+        LOG(ERROR) << "Not a valid adb message.";
+        return;
     }
 
-    auto opcode = headerByte & 0x1f;
-    switch (opcode) {
-        case 0x8:
-        {
-            // closeConnection.
-            break;
-        }
-
-        case 0x2:
-        {
-            // binary
-
-            size_t payloadLength;
-            int err = verifyAdbHeader(msg, len, &payloadLength);
-
-            if (err || len != 24 + payloadLength) {
-                LOG(ERROR) << "websocket message is not a valid adb message.";
-                return -EINVAL;
-            }
-
-            mAdbConnection->send(msg, len);
-            break;
-        }
-
-        default:
-            return -EINVAL;
-    }
-
-    return 0;
+    mAdbConnection->send(msg, len);
 }
 
diff --git a/host/frontend/gcastv2/webrtc/Android.bp b/host/frontend/gcastv2/webrtc/Android.bp
index f662688..d406446 100644
--- a/host/frontend/gcastv2/webrtc/Android.bp
+++ b/host/frontend/gcastv2/webrtc/Android.bp
@@ -16,11 +16,11 @@
 cc_library_static {
     name: "libwebrtc",
     srcs: [
-        "AdbWebSocketHandler.cpp",
+        "AdbHandler.cpp",
         "DTLS.cpp",
         "G711Packetizer.cpp",
         "Keyboard.cpp",
-        "MyWebSocketHandler.cpp",
+        "client_handler.cpp",
         "OpusPacketizer.cpp",
         "Packetizer.cpp",
         "RTPSender.cpp",
@@ -34,6 +34,11 @@
         "STUNMessage.cpp",
         "Utils.cpp",
         "VP8Packetizer.cpp",
+        "ws_connection.cpp",
+        "sig_server_handler.cpp",
+    ],
+    header_libs: [
+      "webrtc_signaling_headers",
     ],
     static_libs: [
         "libhttps",
@@ -48,6 +53,9 @@
         "libffi",
         "libwayland_server",
         "libwayland_extension_server_protocols",
+        "libwebsockets",
+        "libcap",
+        "libcuttlefish_utils",
     ],
     shared_libs: [
         "libssl",
@@ -88,73 +96,9 @@
         "libffi",
         "libwayland_server",
         "libwayland_extension_server_protocols",
+        "libwebsockets",
+        "libcap",
     ],
     cpp_std: "experimental",
     defaults: ["cuttlefish_host_only"],
 }
-
-// TODO(jemoreira): Ideally these files should be in $HOST_OUT/webrtc but I
-// couldn't find a module type that would produce that, prebuilt_usr_share_host
-// is the next best thing for now.
-prebuilt_usr_share_host {
-    name: "webrtc_index.html",
-    src: "assets/index.html",
-    filename: "index.html",
-    sub_dir: "webrtc/assets",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_style.css",
-    src: "assets/style.css",
-    filename: "style.css",
-    sub_dir: "webrtc/assets",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_logcat.js",
-    src: "assets/js/logcat.js",
-    filename: "logcat.js",
-    sub_dir: "webrtc/assets/js",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_cf.js",
-    src: "assets/js/cf_webrtc.js",
-    filename: "cf_webrtc.js",
-    sub_dir: "webrtc/assets/js",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_app.js",
-    src: "assets/js/app.js",
-    filename: "app.js",
-    sub_dir: "webrtc/assets/js",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_server.crt",
-    src: "certs/server.crt",
-    filename: "server.crt",
-    sub_dir: "webrtc/certs",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_server.key",
-    src: "certs/server.key",
-    filename: "server.key",
-    sub_dir: "webrtc/certs",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_server.p12",
-    src: "certs/server.p12",
-    filename: "server.p12",
-    sub_dir: "webrtc/certs",
-}
-
-prebuilt_usr_share_host {
-    name: "webrtc_trusted.pem",
-    src: "certs/trusted.pem",
-    filename: "trusted.pem",
-    sub_dir: "webrtc/certs",
-}
diff --git a/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp b/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp
index a8fff04..835e062 100644
--- a/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp
+++ b/host/frontend/gcastv2/webrtc/RTPSocketHandler.cpp
@@ -17,7 +17,6 @@
 #include <webrtc/RTPSocketHandler.h>
 
 #include <webrtc/Keyboard.h>
-#include <webrtc/MyWebSocketHandler.h>
 #include <webrtc/STUNMessage.h>
 #include <Utils.h>
 
@@ -270,7 +269,7 @@
 }
 
 std::string RTPSocketHandler::getLocalIPString() const {
-    return FLAGS_public_ip;
+    return mServerState->public_ip();
 }
 
 void RTPSocketHandler::run() {
@@ -280,6 +279,23 @@
     } else {
         mSocket->postRecv(makeSafeCallback(this, &RTPSocketHandler::onReceive));
     }
+    ScheduleTimeOutCheck();
+}
+
+void RTPSocketHandler::ScheduleTimeOutCheck() {
+    // RFC 3550 describes the timeout calculation, which for two participants
+    // boils down to M*5s, with M being a constant usually set to 5.
+    mRunLoop->postWithDelay(std::chrono::seconds(25),
+        makeSafeCallback<RTPSocketHandler>(
+            this,
+            [](RTPSocketHandler *me) {
+                bool timed_out = me->CheckParticipantTimeOut();
+                if (timed_out) {
+                  me->on_participant_disconnected_();
+                } else {
+                  me->ScheduleTimeOutCheck();
+                }
+            }));
 }
 
 void RTPSocketHandler::onTCPConnect() {
@@ -379,17 +395,18 @@
     mSocket->postRecv(makeSafeCallback(this, &RTPSocketHandler::onReceive));
 }
 
+bool RTPSocketHandler::CheckParticipantTimeOut() {
+  bool previous_value = packet_received_since_last_check_;
+  packet_received_since_last_check_ = false;
+  return !previous_value;
+}
+
 void RTPSocketHandler::onPacketReceived(
         const sockaddr_storage &addr,
         socklen_t addrLen,
         uint8_t *data,
         size_t n) {
-#if 0
-    std::cout << "========================================" << std::endl;
-
-    hexdump(data, n);
-#endif
-
+    packet_received_since_last_check_ = true;
     STUNMessage msg(data, n);
     if (!msg.isValid()) {
         if (mDTLSConnected) {
@@ -732,6 +749,13 @@
                 continue;
             }
 
+            if (errno == EPIPE) {
+              LOG(ERROR) << "Lost connection to peer: " << strerror(EPIPE);
+              offset = size;
+              disconnected = true;
+              break;
+            }
+
             LOG(FATAL) << "Error sending: "<<strerror(errno);
         } else if (n == 0) {
             offset = size;
@@ -743,7 +767,12 @@
     }
     buf->erase(buf->begin(), buf->begin() + offset);
 
-    if ((!mTcpOutBufferQueue.empty() || !buf->empty()) && !disconnected) {
+    if (disconnected) {
+      on_participant_disconnected_();
+      return;
+    }
+
+    if (!mTcpOutBufferQueue.empty() || !buf->empty()) {
         mSendPending = true;
 
         mSocket->postSend(
diff --git a/host/frontend/gcastv2/webrtc/ServerState.cpp b/host/frontend/gcastv2/webrtc/ServerState.cpp
index 1e8b1ca..144c4d6 100644
--- a/host/frontend/gcastv2/webrtc/ServerState.cpp
+++ b/host/frontend/gcastv2/webrtc/ServerState.cpp
@@ -102,7 +102,8 @@
 }
 
 std::shared_ptr<Packetizer> ServerState::getVideoPacketizer() {
-    auto packetizer = mVideoPacketizer.lock();
+    std::lock_guard<std::mutex> autoLock(mPacketizerLock);
+    std::shared_ptr<Packetizer> packetizer = mVideoPacketizer;
     if (!packetizer) {
         switch (mVideoFormat) {
             case VideoFormat::VP8:
@@ -125,7 +126,8 @@
 }
 
 std::shared_ptr<Packetizer> ServerState::getAudioPacketizer() {
-    auto packetizer = mAudioPacketizer.lock();
+    std::lock_guard<std::mutex> autoLock(mPacketizerLock);
+    std::shared_ptr packetizer = mAudioPacketizer;
     if (!packetizer) {
         packetizer = std::make_shared<OpusPacketizer>(mRunLoop, mAudioSource);
         packetizer->run();
@@ -136,19 +138,6 @@
     return packetizer;
 }
 
-size_t ServerState::acquireHandlerId() {
-    size_t id = 0;
-    while (!mAllocatedHandlerIds.insert(id).second) {
-        ++id;
-    }
-
-    return id;
-}
-
-void ServerState::releaseHandlerId(size_t id) {
-    CHECK_EQ(mAllocatedHandlerIds.erase(id), 1);
-}
-
 std::shared_ptr<android::TouchSink> ServerState::getTouchSink() {
     return mTouchSink;
 }
diff --git a/host/frontend/gcastv2/webrtc/assets/js/app.js b/host/frontend/gcastv2/webrtc/assets/js/app.js
deleted file mode 100644
index f82d91d..0000000
--- a/host/frontend/gcastv2/webrtc/assets/js/app.js
+++ /dev/null
@@ -1,176 +0,0 @@
-'use strict';
-
-const receiveButton = document.getElementById('receiveButton');
-receiveButton.addEventListener('click', onReceive);
-const keyboardCaptureButton = document.getElementById('keyboardCaptureBtn');
-keyboardCaptureButton.addEventListener('click', onKeyboardCaptureClick);
-
-const deviceScreen = document.getElementById('deviceScreen');
-
-deviceScreen.addEventListener("click", onInitialClick);
-
-function onInitialClick(e) {
-    // This stupid thing makes sure that we disable controls after the first click...
-    // Why not just disable controls altogether you ask? Because then audio won't play
-    // because these days user-interaction is required to enable audio playback...
-    console.log("onInitialClick");
-
-    deviceScreen.controls = false;
-    deviceScreen.removeEventListener("click", onInitialClick);
-}
-
-let videoStream;
-
-let mouseIsDown = false;
-
-let deviceConnection;
-
-function onKeyboardCaptureClick(e) {
-    const selectedClass = 'selected';
-    if (keyboardCaptureButton.classList.contains(selectedClass)) {
-        stopKeyboardTracking();
-        keyboardCaptureButton.classList.remove(selectedClass);
-    } else {
-        startKeyboardTracking();
-        keyboardCaptureButton.classList.add(selectedClass);
-    }
-}
-
-async function onReceive() {
-    console.log('onReceive');
-    receiveButton.disabled = true;
-
-    // init_logcat();
-
-    let options = {
-        // temporarily disable audio to free ports in the server since it's only
-        // producing silence anyways.
-        disable_audio: true,
-        wsProtocol: (location.protocol == "http:") ? "ws:" : "wss:",
-        wsPath: location.host + "/control",
-    };
-    let urlParams = new URLSearchParams(location.search);
-    for (const [key, value] of urlParams) {
-        options[key] = JSON.parse(value);
-    }
-
-    import('./cf_webrtc.js')
-        .then(webrtcModule => webrtcModule.Connect('device_id', options))
-        .then(devConn => {
-            deviceConnection = devConn;
-            videoStream = devConn.getVideoStream(0);
-            deviceScreen.srcObject = videoStream;
-            startMouseTracking(); // TODO stopMouseTracking() when disconnected
-        });
-}
-
-function startMouseTracking() {
-    if (window.PointerEvent) {
-        deviceScreen.addEventListener("pointerdown", onStartDrag);
-        deviceScreen.addEventListener("pointermove", onContinueDrag);
-        deviceScreen.addEventListener("pointerup", onEndDrag);
-    } else if (window.TouchEvent) {
-        deviceScreen.addEventListener("touchstart", onStartDrag);
-        deviceScreen.addEventListener("touchmove", onContinueDrag);
-        deviceScreen.addEventListener("touchend", onEndDrag);
-    } else if (window.MouseEvent) {
-        deviceScreen.addEventListener("mousedown", onStartDrag);
-        deviceScreen.addEventListener("mousemove", onContinueDrag);
-        deviceScreen.addEventListener("mouseup", onEndDrag);
-    }
-}
-
-function stopMouseTracking() {
-    if (window.PointerEvent) {
-        deviceScreen.removeEventListener("pointerdown", onStartDrag);
-        deviceScreen.removeEventListener("pointermove", onContinueDrag);
-        deviceScreen.removeEventListener("pointerup", onEndDrag);
-    } else if (window.TouchEvent) {
-        deviceScreen.removeEventListener("touchstart", onStartDrag);
-        deviceScreen.removeEventListener("touchmove", onContinueDrag);
-        deviceScreen.removeEventListener("touchend", onEndDrag);
-    } else if (window.MouseEvent) {
-        deviceScreen.removeEventListener("mousedown", onStartDrag);
-        deviceScreen.removeEventListener("mousemove", onContinueDrag);
-        deviceScreen.removeEventListener("mouseup", onEndDrag);
-    }
-}
-
-function startKeyboardTracking() {
-    document.addEventListener('keydown', onKeyEvent);
-    document.addEventListener('keyup', onKeyEvent);
-}
-
-function stopKeyboardTracking() {
-    document.removeEventListener('keydown', onKeyEvent);
-    document.removeEventListener('keyup', onKeyEvent);
-}
-
-function onStartDrag(e) {
-    e.preventDefault();
-
-    // console.log("mousedown at " + e.pageX + " / " + e.pageY);
-    mouseIsDown = true;
-
-    sendMouseUpdate(true, e);
-}
-
-function onEndDrag(e) {
-    e.preventDefault();
-
-    // console.log("mouseup at " + e.pageX + " / " + e.pageY);
-    mouseIsDown = false;
-
-    sendMouseUpdate(false, e);
-}
-
-function onContinueDrag(e) {
-    e.preventDefault();
-
-    // console.log("mousemove at " + e.pageX + " / " + e.pageY + ", down=" + mouseIsDown);
-    if (mouseIsDown) {
-        sendMouseUpdate(true, e);
-    }
-}
-
-function sendMouseUpdate(down, e) {
-    console.assert(deviceConnection, 'Can\'t send mouse update without device');
-    var x = e.offsetX;
-    var y = e.offsetY;
-
-    // TODO get the device's screen resolution from the device config, not the video stream
-    const videoWidth = deviceScreen.videoWidth;
-    const videoHeight = deviceScreen.videoHeight;
-    const elementWidth = deviceScreen.offsetWidth;
-    const elementHeight = deviceScreen.offsetHeight;
-
-    // vh*ew > eh*vw? then scale h instead of w
-    const scaleHeight = videoHeight * elementWidth > videoWidth * elementHeight;
-    var elementScaling = 0, videoScaling = 0;
-    if (scaleHeight) {
-        elementScaling = elementHeight;
-        videoScaling = videoHeight;
-    } else {
-        elementScaling = elementWidth;
-        videoScaling = videoWidth;
-    }
-
-    // Substract the offset produced by the difference in aspect ratio if any.
-    if (scaleHeight) {
-        x -= (elementWidth - elementScaling * videoWidth / videoScaling) / 2;
-    } else {
-        y -= (elementHeight - elementScaling * videoHeight / videoScaling) / 2;
-    }
-
-    // Convert to coordinates relative to the video
-    x = videoScaling * x / elementScaling;
-    y = videoScaling * y / elementScaling;
-
-    deviceConnection.sendMousePosition({x: Math.trunc(x), y: Math.trunc(y), down});
-}
-
-function onKeyEvent(e) {
-    e.preventDefault();
-    console.assert(deviceConnection, 'Can\'t send key event without device');
-    deviceConnection.sendKeyEvent(e.code, e.type);
-}
diff --git a/host/frontend/gcastv2/webrtc/assets/js/cf_webrtc.js b/host/frontend/gcastv2/webrtc/assets/js/cf_webrtc.js
deleted file mode 100644
index 2b9499f..0000000
--- a/host/frontend/gcastv2/webrtc/assets/js/cf_webrtc.js
+++ /dev/null
@@ -1,302 +0,0 @@
-function createInputDataChannelPromise(pc) {
-  console.log("creating data channel");
-  let inputChannel = pc.createDataChannel('input-channel');
-  return new Promise((resolve, reject) => {
-    inputChannel.onopen = (event) => {
-      resolve(inputChannel);
-    };
-    inputChannel.onclose = () => {
-      console.log(
-          'handleDataChannelStatusChange state=' + dataChannel.readyState);
-    };
-    inputChannel.onmessage = (msg) => {
-      console.log('handleDataChannelMessage data="' + msg.data + '"');
-    };
-    inputChannel.onerror = err => {
-      reject(err);
-    };
-  });
-}
-
-class DeviceConnection {
-  constructor(pc, control) {
-    this._pc = pc;
-    this._control = control;
-    this._inputChannelPr = createInputDataChannelPromise(pc);
-    this._videoStreams = [];
-
-    // Apparently, the only way to obtain the track and the stream at the
-    // same time is by subscribing to this event.
-    pc.addEventListener('track', e => {
-      console.log('Got remote stream: ', e);
-      if (e.track.kind === 'video') {
-        this._videoStreams.push(e.streams[0]);
-      }
-    });
-  }
-
-  set description(desc) {
-    this._description = desc;
-  }
-
-  get description() {
-    return this._description;
-  }
-
-  getVideoStream(displayNum = 0) {
-    return this._videoStreams[displayNum];
-  }
-
-  _sendJsonInput(evt) {
-    this._inputChannelPr = this._inputChannelPr.then(inputChannel => {
-      inputChannel.send(JSON.stringify(evt));
-      return inputChannel;
-    });
-  }
-
-  sendMousePosition({x, y, down, display = 0}) {
-    this._sendJsonInput({
-      type: 'mouse',
-      down: down ? 1 : 0,
-      x,
-      y,
-    });
-  }
-
-  sendMultiTouch({id, x, y, initialDown, slot, display = 0}) {
-    this._sendJsonInput({
-      type: 'multi-touch',
-      id,
-      x,
-      y,
-      initialDown: initialDown ? 1 : 0,
-      slot,
-    });
-  }
-
-  sendKeyEvent(code, type) {
-    this._sendJsonInput({type: 'keyboard', keycode: code, event_type: type});
-  }
-
-  disconnect() {
-    this._pc.close();
-  }
-
-  // TODO(b/148086548) adb
-}
-
-
-const GREETING_MSG_TYPE = 'hello';
-const OFFER_MSG_TYPE = 'offer';
-const ICE_CANDIDATE_MSG_TYPE = 'ice-candidate';
-
-
-class WebRTCControl {
-  constructor(deviceId, {
-    wsProtocol = 'wss',
-    wsPath = '',
-    disable_audio = false,
-    bundle_tracks = false,
-    use_tcp = true,
-  }) {
-    /*
-     * Private attributes:
-     *
-     * _options
-     *
-     * _promiseResolvers = {
-     *    [GREETING_MSG_TYPE]: function to resolve the greeting response promise
-     *
-     *    [OFFER_MSG_TYPE]: function to resolve the offer promise
-     *
-     *    [ICE_CANDIDATE_MSG_TYPE]: function to resolve the ice candidate
-     * promise
-     * }
-     *
-     * _wsPromise: promises the underlying websocket, should resolve when the
-     *             socket passes to OPEN state, will be rejecte/replaced by a
-     *             rejected promise if an error is detected on the socket.
-     *
-     */
-
-    this._options = {
-      disable_audio,
-      bundle_tracks,
-      use_tcp,
-    };
-
-    this._promiseResolvers = {};
-
-    this._wsPromise = new Promise((resolve, reject) => {
-      const wsUrl = `${wsProtocol}//${wsPath}`;
-      let ws = new WebSocket(wsUrl);
-      ws.onopen = () => {
-        console.info('Websocket connected');
-        resolve(ws);
-      };
-      ws.onerror = evt => {
-        console.error('WebSocket error:', evt);
-        reject(evt);
-        // If the promise was already resolved the previous line has no effect
-        this._wsPromise = Promise.reject(new Error(evt));
-      };
-      ws.onmessage = e => {
-        console.log('onmessage ' + e.data);
-        let data = JSON.parse(e.data);
-        this._onWebsocketMessage(data);
-      };
-    });
-  }
-
-  _onWebsocketMessage(message) {
-    const type = message.type;
-    if (!(type in this._promiseResolvers)) {
-      console.warn(`Unexpected message of type: ${type}`);
-      return;
-    }
-    this._promiseResolvers[type](message);
-    delete this._promiseResolvers[type];
-  }
-
-  async _wsSendJson(obj) {
-    let ws = await this._wsPromise;
-    return ws.send(JSON.stringify(obj));
-  }
-
-  /**
-   * Send a greeting to the device, returns a promise of a greeting response.
-   */
-  async greet() {
-    console.log('greet');
-    return new Promise((resolve, reject) => {
-      if (GREETING_MSG_TYPE in this._promiseResolvers) {
-        const msg = 'Greeting already sent and not yet responded';
-        console.error(msg);
-        throw new Error(msg);
-      }
-      this._promiseResolvers[GREETING_MSG_TYPE] = resolve;
-
-      this._wsSendJson({
-        type: 'greeting',
-        message: 'Hello, world!',
-        options: this._options,
-      });
-    });
-  }
-
-  /**
-   * Sends an offer request to the device, returns a promise of an offer.
-   */
-  async requestOffer() {
-    console.log('requestOffer');
-    return new Promise((resolve, reject) => {
-      if (OFFER_MSG_TYPE in this._promiseResolvers) {
-        const msg = 'Offer already requested and not yet received';
-        console.error(msg);
-        throw new Error(msg);
-      }
-      this._promiseResolvers[OFFER_MSG_TYPE] = resolve;
-
-      const is_chrome = navigator.userAgent.indexOf('Chrome') !== -1;
-      this._wsSendJson({type: 'request-offer', is_chrome: is_chrome ? 1 : 0});
-    });
-  }
-
-  /**
-   * Sends a remote description to the device.
-   */
-  async sendClientDescription(desc) {
-    console.log('sendClientDescription');
-    this._wsSendJson({type: 'set-client-desc', sdp: desc.sdp});
-  }
-
-  /**
-   * Request an ICE candidate from the device. Returns a promise of an ice
-   * candidate.
-   */
-  async requestICECandidate(mid) {
-    console.log(`requestICECandidate(${mid})`);
-    if (ICE_CANDIDATE_MSG_TYPE in this._promiseResolvers) {
-      const msg = 'An ice candidate request is already pending';
-      console.error(msg);
-      throw new Error(msg);
-    }
-
-    let iceCandidatePromise = new Promise((resolve, reject) => {
-      this._promiseResolvers[ICE_CANDIDATE_MSG_TYPE] = resolve;
-    });
-    this._wsSendJson({type: 'get-ice-candidate', mid: mid});
-
-    let reply = await iceCandidatePromise;
-    console.log('got reply: ', reply);
-
-    if (reply == undefined || reply.candidate == undefined) {
-      console.warn('Undefined reply or candidate');
-      return null;
-    }
-
-    const replyCandidate = reply.candidate;
-    const mlineIndex = reply.mlineIndex;
-
-    const result = new RTCIceCandidate(
-        {sdpMid: mid, sdpMLineIndex: mlineIndex, candidate: replyCandidate});
-
-    console.log('got result: ', result);
-
-    return result;
-  }
-}
-
-function createPeerConnection() {
-  let pc = new RTCPeerConnection();
-  console.log('got pc2=', pc);
-
-  pc.addEventListener('icecandidate', e => {
-    console.log('pc.onIceCandidate: ', e.candidate);
-  });
-
-  pc.addEventListener(
-      'iceconnectionstatechange',
-      e => console.log(`Ice State Change: ${pc.iceConnectionState}`));
-
-  pc.addEventListener('connectionstatechange', e => {
-    console.log('connection state = ' + pc.connectionState);
-  });
-
-  return pc;
-}
-
-export async function Connect(deviceId, options) {
-  let control = new WebRTCControl(deviceId, options);
-  let pc = createPeerConnection();
-  let deviceConnection = new DeviceConnection(pc, control);
-  try {
-    let greetResponse = await control.greet();
-    console.log('Greeting response: ', greetResponse);
-    // TODO(jemoreira): get the description from the device
-    deviceConnection.description = {};
-
-    let desc = await control.requestOffer();
-    console.log('Offer: ', desc);
-    await pc.setRemoteDescription(desc);
-
-    let answer = await pc.createAnswer();
-    console.log('Answer: ', answer);
-    // nest then() calls here to have easy access to the answer
-    await pc.setLocalDescription(answer);
-
-    await control.sendClientDescription(answer);
-
-    for (let mid = 0; mid < 3; ++mid) {
-      let iceCandidate = await control.requestICECandidate(mid);
-      console.log(`ICE Candidate[${mid}]: `, iceCandidate);
-      if (iceCandidate) await pc.addIceCandidate(iceCandidate);
-    }
-
-    console.log('WebRTC connection established');
-    return deviceConnection;
-  } catch (e) {
-    console.error('Error establishing WebRTC connection: ', e);
-    throw e;
-  };
-}
diff --git a/host/frontend/gcastv2/webrtc/assets/style.css b/host/frontend/gcastv2/webrtc/assets/style.css
deleted file mode 100644
index 272b5f0..0000000
--- a/host/frontend/gcastv2/webrtc/assets/style.css
+++ /dev/null
@@ -1,39 +0,0 @@
-body {
-    background-color:black
-}
-
-.noscroll {
-    touch-action: none;
-}
-
-.one {
-    float: left;
-}
-
-#logcat {
-    display: none;
-    font-family: monospace;
-    padding: 10px;
-}
-
-button.selected {
-    background-color: #aaaaaa;
-    border-style: solid;
-    border-color: #aaaaaa;
-}
-
-
-.noscroll {
-    text-align: center;
-}
-
-section.noscroll div.one {
-    display: inline-block;
-    width: 100%;
-    height: 90%;
-}
-
-#deviceScreen {
-    max-width: 100%;
-    max-height: 100%;
-}
diff --git a/host/frontend/gcastv2/webrtc/certs/server.crt b/host/frontend/gcastv2/webrtc/certs/server.crt
deleted file mode 100644
index 81759be..0000000
--- a/host/frontend/gcastv2/webrtc/certs/server.crt
+++ /dev/null
@@ -1,20 +0,0 @@
------BEGIN CERTIFICATE-----
-MIIDTDCCAjQCCQCsLGBNpNbWCjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJV
-UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAY
-BgNVBAoMEUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcN
-MTkwNTA4MjAxMTQ5WhcNMjAwNTA3MjAxMTQ5WjBoMQswCQYDVQQGEwJVUzETMBEG
-A1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoM
-EUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqG
-SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3c9E6JtTUro8MWBYRtjJ6k01ORfROTsJz
-vbL5V3IV6MyUqGKbhRuN2SP/XsVZoUCmeFsCVF++G3mLmYetdUtb/ZwIgF/sN5aW
-oh//YDR/T2kepMjk73kvQ7R6h5bB5vQIbpbP+piJR1S7HxlXEgwyLuXmKgVNF6tU
-WJt/vRFLRDiz0Bn3GklE5yOgJGeeU3CYioatAiKE6b/yPu5pcM7Kj4tn9hu5COO2
-89Wol3od+I3pSkd9tXypdfw1txdAi0o9P7uIDIDSIDwf7RyCoxojZ7yUxO2Q0ld+
-KFJqioSpFIscIxR1BaTIrJaSCU7Hn82ihODS2ue4OaoAJ2qH8JtVAgMBAAEwDQYJ
-KoZIhvcNAQELBQADggEBAPQ+7bXvy/RSx9lgSAiO/R5ep3Si2xWUe0hYivmQ1bX2
-80FpDP8tZXtAkIhpyWdiS0aYBMySLDDDiDl2xUWfxZO0NnOcVJ17jZ2sJxhqKksW
-NMTLb7dCr7kUS2+FOuXwR+Yeb77up2e54lXLuiKVWevFAUVc8Xhgq/sNz2rwt5iG
-XFcLCXoEgLwHnd7LBR8y6IsEfVW5UVSWpFQPODdcYtVgaWYo7TYghZjzEya8VIc7
-HgHlH/1Uj9yvh+eY2hLoLwukZRV/CnMbSk8LLgTYuEeLfPuSnCTERrydMEyRcF3H
-ljCthgV7YRt8cQovsVZBvuMRJYzl4hM3ema00Px7SD8=
------END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/webrtc/certs/server.p12 b/host/frontend/gcastv2/webrtc/certs/server.p12
deleted file mode 100644
index 3d0d595..0000000
--- a/host/frontend/gcastv2/webrtc/certs/server.p12
+++ /dev/null
Binary files differ
diff --git a/host/frontend/gcastv2/webrtc/certs/trusted.pem b/host/frontend/gcastv2/webrtc/certs/trusted.pem
deleted file mode 100644
index b2080b4..0000000
--- a/host/frontend/gcastv2/webrtc/certs/trusted.pem
+++ /dev/null
@@ -1,69 +0,0 @@
-Certificate:
-    Data:
-        Version: 1 (0x0)
-        Serial Number: 12406396960093165066 (0xac2c604da4d6d60a)
-    Signature Algorithm: sha256WithRSAEncryption
-        Issuer: C=US, ST=California, L=Santa Clara, O=Beyond Aggravated, CN=localhost
-        Validity
-            Not Before: May  8 20:11:49 2019 GMT
-            Not After : May  7 20:11:49 2020 GMT
-        Subject: C=US, ST=California, L=Santa Clara, O=Beyond Aggravated, CN=localhost
-        Subject Public Key Info:
-            Public Key Algorithm: rsaEncryption
-                Public-Key: (2048 bit)
-                Modulus:
-                    00:f7:73:d1:3a:26:d4:d4:ae:8f:0c:58:16:11:b6:
-                    32:7a:93:4d:4e:45:f4:4e:4e:c2:73:bd:b2:f9:57:
-                    72:15:e8:cc:94:a8:62:9b:85:1b:8d:d9:23:ff:5e:
-                    c5:59:a1:40:a6:78:5b:02:54:5f:be:1b:79:8b:99:
-                    87:ad:75:4b:5b:fd:9c:08:80:5f:ec:37:96:96:a2:
-                    1f:ff:60:34:7f:4f:69:1e:a4:c8:e4:ef:79:2f:43:
-                    b4:7a:87:96:c1:e6:f4:08:6e:96:cf:fa:98:89:47:
-                    54:bb:1f:19:57:12:0c:32:2e:e5:e6:2a:05:4d:17:
-                    ab:54:58:9b:7f:bd:11:4b:44:38:b3:d0:19:f7:1a:
-                    49:44:e7:23:a0:24:67:9e:53:70:98:8a:86:ad:02:
-                    22:84:e9:bf:f2:3e:ee:69:70:ce:ca:8f:8b:67:f6:
-                    1b:b9:08:e3:b6:f3:d5:a8:97:7a:1d:f8:8d:e9:4a:
-                    47:7d:b5:7c:a9:75:fc:35:b7:17:40:8b:4a:3d:3f:
-                    bb:88:0c:80:d2:20:3c:1f:ed:1c:82:a3:1a:23:67:
-                    bc:94:c4:ed:90:d2:57:7e:28:52:6a:8a:84:a9:14:
-                    8b:1c:23:14:75:05:a4:c8:ac:96:92:09:4e:c7:9f:
-                    cd:a2:84:e0:d2:da:e7:b8:39:aa:00:27:6a:87:f0:
-                    9b:55
-                Exponent: 65537 (0x10001)
-    Signature Algorithm: sha256WithRSAEncryption
-         f4:3e:ed:b5:ef:cb:f4:52:c7:d9:60:48:08:8e:fd:1e:5e:a7:
-         74:a2:db:15:94:7b:48:58:8a:f9:90:d5:b5:f6:f3:41:69:0c:
-         ff:2d:65:7b:40:90:88:69:c9:67:62:4b:46:98:04:cc:92:2c:
-         30:c3:88:39:76:c5:45:9f:c5:93:b4:36:73:9c:54:9d:7b:8d:
-         9d:ac:27:18:6a:2a:4b:16:34:c4:cb:6f:b7:42:af:b9:14:4b:
-         6f:85:3a:e5:f0:47:e6:1e:6f:be:ee:a7:67:b9:e2:55:cb:ba:
-         22:95:59:eb:c5:01:45:5c:f1:78:60:ab:fb:0d:cf:6a:f0:b7:
-         98:86:5c:57:0b:09:7a:04:80:bc:07:9d:de:cb:05:1f:32:e8:
-         8b:04:7d:55:b9:51:54:96:a4:54:0f:38:37:5c:62:d5:60:69:
-         66:28:ed:36:20:85:98:f3:13:26:bc:54:87:3b:1e:01:e5:1f:
-         fd:54:8f:dc:af:87:e7:98:da:12:e8:2f:0b:a4:65:15:7f:0a:
-         73:1b:4a:4f:0b:2e:04:d8:b8:47:8b:7c:fb:92:9c:24:c4:46:
-         bc:9d:30:4c:91:70:5d:c7:96:30:ad:86:05:7b:61:1b:7c:71:
-         0a:2f:b1:56:41:be:e3:11:25:8c:e5:e2:13:37:7a:66:b4:d0:
-         fc:7b:48:3f
------BEGIN CERTIFICATE-----
-MIIDTDCCAjQCCQCsLGBNpNbWCjANBgkqhkiG9w0BAQsFADBoMQswCQYDVQQGEwJV
-UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAY
-BgNVBAoMEUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcN
-MTkwNTA4MjAxMTQ5WhcNMjAwNTA3MjAxMTQ5WjBoMQswCQYDVQQGEwJVUzETMBEG
-A1UECAwKQ2FsaWZvcm5pYTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoM
-EUJleW9uZCBBZ2dyYXZhdGVkMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqG
-SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD3c9E6JtTUro8MWBYRtjJ6k01ORfROTsJz
-vbL5V3IV6MyUqGKbhRuN2SP/XsVZoUCmeFsCVF++G3mLmYetdUtb/ZwIgF/sN5aW
-oh//YDR/T2kepMjk73kvQ7R6h5bB5vQIbpbP+piJR1S7HxlXEgwyLuXmKgVNF6tU
-WJt/vRFLRDiz0Bn3GklE5yOgJGeeU3CYioatAiKE6b/yPu5pcM7Kj4tn9hu5COO2
-89Wol3od+I3pSkd9tXypdfw1txdAi0o9P7uIDIDSIDwf7RyCoxojZ7yUxO2Q0ld+
-KFJqioSpFIscIxR1BaTIrJaSCU7Hn82ihODS2ue4OaoAJ2qH8JtVAgMBAAEwDQYJ
-KoZIhvcNAQELBQADggEBAPQ+7bXvy/RSx9lgSAiO/R5ep3Si2xWUe0hYivmQ1bX2
-80FpDP8tZXtAkIhpyWdiS0aYBMySLDDDiDl2xUWfxZO0NnOcVJ17jZ2sJxhqKksW
-NMTLb7dCr7kUS2+FOuXwR+Yeb77up2e54lXLuiKVWevFAUVc8Xhgq/sNz2rwt5iG
-XFcLCXoEgLwHnd7LBR8y6IsEfVW5UVSWpFQPODdcYtVgaWYo7TYghZjzEya8VIc7
-HgHlH/1Uj9yvh+eY2hLoLwukZRV/CnMbSk8LLgTYuEeLfPuSnCTERrydMEyRcF3H
-ljCthgV7YRt8cQovsVZBvuMRJYzl4hM3ema00Px7SD8=
------END CERTIFICATE-----
diff --git a/host/frontend/gcastv2/webrtc/MyWebSocketHandler.cpp b/host/frontend/gcastv2/webrtc/client_handler.cpp
similarity index 61%
rename from host/frontend/gcastv2/webrtc/MyWebSocketHandler.cpp
rename to host/frontend/gcastv2/webrtc/client_handler.cpp
index e48acbd..703cdf7 100644
--- a/host/frontend/gcastv2/webrtc/MyWebSocketHandler.cpp
+++ b/host/frontend/gcastv2/webrtc/client_handler.cpp
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-#include <webrtc/MyWebSocketHandler.h>
+#include <webrtc/client_handler.h>
+
+#include <vector>
 
 #include "Utils.h"
 
@@ -23,6 +25,9 @@
 #include <netdb.h>
 #include <openssl/rand.h>
 
+#include "https/SafeCallbackable.h"
+#include "common/libs/utils/base64.h"
+
 namespace {
 
 // helper method to ensure a json object has the required fields convertible
@@ -54,79 +59,87 @@
 
 } // namespace
 
-MyWebSocketHandler::MyWebSocketHandler(
-        std::shared_ptr<RunLoop> runLoop,
+ClientHandler::ClientHandler(
         std::shared_ptr<ServerState> serverState,
-        size_t handlerId)
-    : mRunLoop(runLoop),
+        std::function<void(const Json::Value&)> send_to_client_cb)
+    : mRunLoop(serverState->run_loop()),
       mServerState(serverState),
-      mId(handlerId),
       mOptions(OptionBits::useSingleCertificateForAllTracks
               | OptionBits::enableData),
+      sendToClient_(send_to_client_cb),
       mTouchSink(mServerState->getTouchSink()),
       mKeyboardSink(mServerState->getKeyboardSink()) {
 }
 
-MyWebSocketHandler::~MyWebSocketHandler() {
-    mServerState->releaseHandlerId(mId);
+std::shared_ptr<AdbHandler> ClientHandler::adb_handler() {
+  if (!adb_handler_) {
+    auto config = vsoc::CuttlefishConfig::Get();
+    adb_handler_.reset(
+        new AdbHandler(mRunLoop, config->ForDefaultInstance().adb_ip_and_port(),
+                       [this](const uint8_t *msg, size_t length) {
+                         std::string base64_msg;
+                         cvd::EncodeBase64(msg, length, &base64_msg);
+                         Json::Value reply;
+                         reply["type"] = "adb-message";
+                         reply["payload"] = base64_msg;
+                         sendToClient_(reply);
+                       }));
+    adb_handler_->run();
+  }
+  return adb_handler_;
 }
 
-int MyWebSocketHandler::handleMessage(
-        uint8_t /*headerByte*/, const uint8_t *msg, size_t len) {
-    // android::hexdump(msg, len);
+void ClientHandler::LogAndReplyError(const std::string& error_msg) const {
+    LOG(ERROR) << error_msg;
+    Json::Value reply;
+    reply["error"] = error_msg;
+    sendToClient_(reply);
+}
 
-    Json::Value obj;
-    Json::Reader json_reader;
-    Json::FastWriter json_writer;
-    auto str = reinterpret_cast<const char *>(msg);
-    if (!json_reader.parse(str, str + len, obj) < 0) {
-        return -EINVAL;
+void ClientHandler::HandleMessage(const Json::Value& message) {
+    LOG(VERBOSE) << message.toStyledString();
+
+    if (!validateJsonObject(
+            message, "", {{"type", Json::ValueType::stringValue}},
+            [this](const std::string &error) { LogAndReplyError(error); })) {
+      return;
     }
-
-    LOG(VERBOSE) << obj.toStyledString();
-
-    auto sendMessageOnError =
-        [this](const std::string &error_msg) {
-          auto reply = "{\"error\": \"" + error_msg + "\"}";
-          sendMessage(reply.c_str(), reply.size());
-        };
-
-    if (!validateJsonObject(obj, "", {{"type", Json::ValueType::stringValue}},
-                            sendMessageOnError)) {
-        return -EINVAL;
-    }
-    std::string type = obj["type"].asString();
-
-    if (type == "greeting") {
-        Json::Value reply;
-        reply["type"] = "hello";
-        reply["reply"] = "Right back at ya!";
-
-        auto replyAsString = json_writer.write(reply);
-        sendMessage(replyAsString.c_str(), replyAsString.size());
-
-        if (obj.isMember("options")) {
-            parseOptions(obj["options"]);
+    auto type = message["type"].asString();
+    if (type == "request-offer") {
+        if (message.isMember("options")) {
+            parseOptions(message["options"]);
         }
-
         if (mOptions & OptionBits::useSingleCertificateForAllTracks) {
             mCertificateAndKey = CreateDTLSCertificateAndKey();
         }
-
         prepareSessions();
-    } else if (type == "set-client-desc") {
-        if (!validateJsonObject(obj, type,
+        auto offer = BuildOffer();
+
+        Json::Value reply;
+        reply["type"] = "offer";
+        reply["sdp"] = offer;
+
+        sendToClient_(reply);
+    } else if (type == "answer") {
+        if (mSessions.size() == 0) {
+            LOG(ERROR)
+                  << "Received sdp answer from client before request for offer";
+            return;
+        }
+
+        if (!validateJsonObject(message, type,
                                 {{"sdp", Json::ValueType::stringValue}},
-                                sendMessageOnError)) {
-            return -EINVAL;
+                                [this](const std::string &error) {
+                                  LogAndReplyError(error);
+                                })) {
+          return;
         }
 
-        int err = mOfferedSDP.setTo(obj["sdp"].asString());
-
+        int err = mOfferedSDP.setTo(message["sdp"].asString());
         if (err) {
-            LOG(ERROR) << "Offered SDP could not be parsed (" << err << ")";
+            LogAndReplyError("Offered SDP could not be parsed (" +
+                                std::to_string(err) + ")");
         }
-
         for (size_t i = 0; i < mSessions.size(); ++i) {
             const auto &session = mSessions[i];
 
@@ -136,155 +149,148 @@
                     getRemoteFingerprint(i));
         }
 
-        return err;
-    } else if (type == "request-offer") {
-        std::stringstream ss;
-
-        ss <<
-"v=0\r\n"
-"o=- 7794515898627856655 2 IN IP4 127.0.0.1\r\n"
-"s=-\r\n"
-"t=0 0\r\n"
-"a=msid-semantic: WMS pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n";
-
-        bool bundled = false;
-
-        if ((mOptions & OptionBits::bundleTracks) && countTracks() > 1) {
-            bundled = true;
-
-            ss << "a=group:BUNDLE 0";
-
-            if (!(mOptions & OptionBits::disableAudio)) {
-                ss << " 1";
-            }
-
-            if (mOptions & OptionBits::enableData) {
-                ss << " 2";
-            }
-
-            ss << "\r\n";
-
-            emitTrackIceOptionsAndFingerprint(ss, 0 /* mlineIndex */);
+        for (int32_t mid = 0; mid < 3; mid++) {
+            GatherAndSendCandidate(mid);
         }
-
-        size_t mlineIndex = 0;
-
-        // Video track (mid = 0)
-
-        std::string videoEncodingSpecific = "a=rtpmap:96 VP8/90000\r\n";
-
-        videoEncodingSpecific +=
-"a=rtcp-fb:96 ccm fir\r\n"
-"a=rtcp-fb:96 nack\r\n"
-"a=rtcp-fb:96 nack pli\r\n";
-
-        ss <<
-"m=video 9 "
-<< ((mOptions & OptionBits::useTCP) ? "TCP" : "UDP")
-<< "/TLS/RTP/SAVPF 96 97\r\n"
-"c=IN IP4 0.0.0.0\r\n"
-"a=rtcp:9 IN IP4 0.0.0.0\r\n";
-
-        if (!bundled) {
-            emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
-        }
-
-        ss <<
-"a=setup:actpass\r\n"
-"a=mid:0\r\n"
-"a=sendonly\r\n"
-"a=rtcp-mux\r\n"
-"a=rtcp-rsize\r\n"
-"a=rtcp-xr:rcvr-rtt=all\r\n";
-
-        ss << videoEncodingSpecific <<
-"a=rtpmap:97 rtx/90000\r\n"
-"a=fmtp:97 apt=96\r\n"
-"a=ssrc-group:FID 3735928559 3405689008\r\n"
-"a=ssrc:3735928559 cname:myWebRTP\r\n"
-"a=ssrc:3735928559 msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
-"a=ssrc:3735928559 mslabel:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n"
-"a=ssrc:3735928559 label:61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
-"a=ssrc:3405689008 cname:myWebRTP\r\n"
-"a=ssrc:3405689008 msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
-"a=ssrc:3405689008 mslabel:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n"
-"a=ssrc:3405689008 label:61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n";
-
-        if (!(mOptions & OptionBits::disableAudio)) {
-            ss <<
-"m=audio 9 "
-<< ((mOptions & OptionBits::useTCP) ? "TCP" : "UDP")
-<< "/TLS/RTP/SAVPF 98\r\n"
-"c=IN IP4 0.0.0.0\r\n"
-"a=rtcp:9 IN IP4 0.0.0.0\r\n";
-
-            if (!bundled) {
-                emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
-            }
-
-            ss <<
-"a=setup:actpass\r\n"
-"a=mid:1\r\n"
-"a=sendonly\r\n"
-"a=msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n"
-"a=rtcp-mux\r\n"
-"a=rtcp-rsize\r\n"
-"a=rtpmap:98 opus/48000/2\r\n"
-"a=fmtp:98 minptime=10;useinbandfec=1\r\n"
-"a=ssrc-group:FID 2343432205\r\n"
-"a=ssrc:2343432205 cname:myWebRTP\r\n"
-"a=ssrc:2343432205 msid:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw 61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n"
-"a=ssrc:2343432205 mslabel:pqWEULZNyLiJHA7lcwlUnbule9FJNk0pY0aw\r\n"
-"a=ssrc:2343432205 label:61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n";
-        }
-
-        if (mOptions & OptionBits::enableData) {
-            ss <<
-"m=application 9 "
-<< ((mOptions & OptionBits::useTCP) ? "TCP" : "UDP")
-<< "/DTLS/SCTP webrtc-datachannel\r\n"
-"c=IN IP4 0.0.0.0\r\n"
-"a=sctp-port:5000\r\n";
-
-            if (!bundled) {
-                emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
-            }
-
-            ss <<
-"a=setup:actpass\r\n"
-"a=mid:2\r\n"
-"a=sendrecv\r\n"
-"a=fmtp:webrtc-datachannel max-message-size=65536\r\n";
-        }
-
-        Json::Value reply;
-        reply["type"] = "offer";
-        reply["sdp"] = ss.str();
-
-        auto replyAsString = json_writer.write(reply);
-        sendMessage(replyAsString.c_str(), replyAsString.size());
-    } else if (type == "get-ice-candidate") {
-        if (!validateJsonObject(obj, type, {{"mid", Json::ValueType::intValue}},
-                                sendMessageOnError)) {
-            return -EINVAL;
-        }
-        int32_t mid = obj["mid"].asInt();
-
-        bool success = getCandidate(mid);
-
-        if (!success) {
-            Json::Value reply;
-            reply["type"] = "ice-candidate";
-
-            auto replyAsString = json_writer.write(reply);
-            sendMessage(replyAsString.c_str(), replyAsString.size());
-        }
+    } else if (type == "ice-candidate") {
+      LOG(INFO) << "Received ice candidate from client, ignoring";
+    } else if (type == "adb-message") {
+      if (!message.isMember("payload") || !message["payload"].isString()) {
+        LOG(ERROR) << "adb-message has invalid payload";
+        return;
+      }
+      auto base64_msg = message["payload"].asString();
+      std::vector<uint8_t> raw_msg;
+      if (!cvd::DecodeBase64(base64_msg, &raw_msg)) {
+        LOG(ERROR) << "Invalid base64 string in adb-message";
+        return;
+      }
+      adb_handler()->handleMessage(raw_msg.data(), raw_msg.size());
+    } else {
+        LogAndReplyError("Unknown type: " + type);
+        return;
     }
-
-    return 0;
 }
 
-size_t MyWebSocketHandler::countTracks() const {
+std::string ClientHandler::BuildOffer() {
+  std::stringstream ss;
+
+  ss << "v=0\r\n"
+        "o=- 7794515898627856655 2 IN IP4 127.0.0.1\r\n"
+        "s=-\r\n"
+        "t=0 0\r\n"
+        "a=msid-semantic: WMS display_0\r\n";
+
+  bool bundled = false;
+
+  if ((mOptions & OptionBits::bundleTracks) && countTracks() > 1) {
+    bundled = true;
+
+    ss << "a=group:BUNDLE 0";
+
+    if (!(mOptions & OptionBits::disableAudio)) {
+      ss << " 1";
+    }
+
+    if (mOptions & OptionBits::enableData) {
+      ss << " 2";
+    }
+
+    ss << "\r\n";
+
+    emitTrackIceOptionsAndFingerprint(ss, 0 /* mlineIndex */);
+  }
+
+  size_t mlineIndex = 0;
+
+  // Video track (mid = 0)
+
+  std::string videoEncodingSpecific = "a=rtpmap:96 VP8/90000\r\n";
+
+  videoEncodingSpecific +=
+      "a=rtcp-fb:96 ccm fir\r\n"
+      "a=rtcp-fb:96 nack\r\n"
+      "a=rtcp-fb:96 nack pli\r\n";
+
+  ss << "m=video 9 " << ((mOptions & OptionBits::useTCP) ? "TCP" : "UDP")
+     << "/TLS/RTP/SAVPF 96 97\r\n"
+        "c=IN IP4 0.0.0.0\r\n"
+        "a=rtcp:9 IN IP4 0.0.0.0\r\n";
+
+  if (!bundled) {
+    emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
+  }
+
+  ss << "a=setup:actpass\r\n"
+        "a=mid:0\r\n"
+        "a=sendonly\r\n"
+        "a=rtcp-mux\r\n"
+        "a=rtcp-rsize\r\n"
+        "a=rtcp-xr:rcvr-rtt=all\r\n";
+
+  ss << videoEncodingSpecific
+     << "a=rtpmap:97 rtx/90000\r\n"
+        "a=fmtp:97 apt=96\r\n"
+        "a=ssrc-group:FID 3735928559 3405689008\r\n"
+        "a=ssrc:3735928559 cname:myWebRTP\r\n"
+        "a=ssrc:3735928559 msid:display_0 "
+        "61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+        "a=ssrc:3735928559 mslabel:display_0\r\n"
+        "a=ssrc:3735928559 label:61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+        "a=ssrc:3405689008 cname:myWebRTP\r\n"
+        "a=ssrc:3405689008 msid:display_0 "
+        "61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+        "a=ssrc:3405689008 mslabel:display_0\r\n"
+        "a=ssrc:3405689008 label:61843855-edd7-4ca9-be79-4e3ccc6cc035\r\n";
+
+  if (!(mOptions & OptionBits::disableAudio)) {
+    ss << "m=audio 9 " << ((mOptions & OptionBits::useTCP) ? "TCP" : "UDP")
+       << "/TLS/RTP/SAVPF 98\r\n"
+          "c=IN IP4 0.0.0.0\r\n"
+          "a=rtcp:9 IN IP4 0.0.0.0\r\n";
+
+    if (!bundled) {
+      emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
+    }
+
+    ss << "a=setup:actpass\r\n"
+          "a=mid:1\r\n"
+          "a=sendonly\r\n"
+          "a=msid:display_0 "
+          "61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+          "a=rtcp-mux\r\n"
+          "a=rtcp-rsize\r\n"
+          "a=rtpmap:98 opus/48000/2\r\n"
+          "a=fmtp:98 minptime=10;useinbandfec=1\r\n"
+          "a=ssrc-group:FID 2343432205\r\n"
+          "a=ssrc:2343432205 cname:myWebRTP\r\n"
+          "a=ssrc:2343432205 msid:display_0 "
+          "61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n"
+          "a=ssrc:2343432205 mslabel:display_0\r\n"
+          "a=ssrc:2343432205 label:61843856-edd7-4ca9-be79-4e3ccc6cc035\r\n";
+  }
+
+  if (mOptions & OptionBits::enableData) {
+    ss << "m=application 9 "
+       << ((mOptions & OptionBits::useTCP) ? "TCP" : "UDP")
+       << "/DTLS/SCTP webrtc-datachannel\r\n"
+          "c=IN IP4 0.0.0.0\r\n"
+          "a=sctp-port:5000\r\n";
+
+    if (!bundled) {
+      emitTrackIceOptionsAndFingerprint(ss, mlineIndex++);
+    }
+
+    ss << "a=setup:actpass\r\n"
+          "a=mid:2\r\n"
+          "a=sendrecv\r\n"
+          "a=fmtp:webrtc-datachannel max-message-size=65536\r\n";
+  }
+  return ss.str();
+}
+
+
+size_t ClientHandler::countTracks() const {
     size_t n = 1;  // We always have a video track.
 
     if (!(mOptions & OptionBits::disableAudio)) {
@@ -298,7 +304,7 @@
     return n;
 }
 
-ssize_t MyWebSocketHandler::mlineIndexForMid(int32_t mid) const {
+ssize_t ClientHandler::mlineIndexForMid(int32_t mid) const {
     switch (mid) {
         case 0:
             return 0;
@@ -326,7 +332,7 @@
     }
 }
 
-bool MyWebSocketHandler::getCandidate(int32_t mid) {
+bool ClientHandler::GatherAndSendCandidate(int32_t mid) {
     auto mlineIndex = mlineIndexForMid(mid);
 
     if (mlineIndex < 0) {
@@ -371,16 +377,19 @@
                 trackMask,
                 session);
 
-        rtp->run();
-
         mRTPs.push_back(rtp);
+        rtp->OnParticipantDisconnected([this]{
+          mRunLoop->post(makeSafeCallback<ClientHandler>(
+            this,
+            [](ClientHandler *me) {
+                me->on_connection_closed_cb_();
+            }));
+        });
+        rtp->run();
     }
 
     auto rtp = mRTPs.back();
 
-    Json::Value reply;
-    reply["type"] = "ice-candidate";
-
     auto localIPString = rtp->getLocalIPString();
 
     std::stringstream ss;
@@ -401,17 +410,17 @@
 
     ss << "generation 0 ufrag " << rtp->getLocalUFrag();
 
+    Json::Value reply;
+    reply["type"] = "ice-candidate";
+    reply["mid"] = mid;
+    reply["mLineIndex"] = static_cast<Json::UInt64>(mlineIndex);
     reply["candidate"] = ss.str();
-    reply["mlineIndex"] = static_cast<Json::UInt64>(mlineIndex);
 
-    Json::FastWriter json_writer;
-    auto replyAsString = json_writer.write(reply);
-    sendMessage(replyAsString.c_str(), replyAsString.size());
-
+    sendToClient_(reply);
     return true;
 }
 
-std::optional<std::string> MyWebSocketHandler::getSDPValue(
+std::optional<std::string> ClientHandler::getSDPValue(
         ssize_t targetMediaIndex,
         std::string_view key,
         bool fallthroughToGeneralSection) const {
@@ -464,21 +473,21 @@
     return (*it).substr(prefix.size());
 }
 
-std::string MyWebSocketHandler::getRemotePassword(size_t mlineIndex) const {
+std::string ClientHandler::getRemotePassword(size_t mlineIndex) const {
     auto value = getSDPValue(
             mlineIndex, "ice-pwd", true /* fallthroughToGeneralSection */);
 
     return value ? *value : std::string();
 }
 
-std::string MyWebSocketHandler::getRemoteUFrag(size_t mlineIndex) const {
+std::string ClientHandler::getRemoteUFrag(size_t mlineIndex) const {
     auto value = getSDPValue(
             mlineIndex, "ice-ufrag", true /* fallthroughToGeneralSection */);
 
     return value ? *value : std::string();
 }
 
-std::string MyWebSocketHandler::getRemoteFingerprint(size_t mlineIndex) const {
+std::string ClientHandler::getRemoteFingerprint(size_t mlineIndex) const {
     auto value = getSDPValue(
             mlineIndex, "fingerprint", true /* fallthroughToGeneralSection */);
 
@@ -487,7 +496,7 @@
 
 // static
 std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
-MyWebSocketHandler::CreateDTLSCertificateAndKey() {
+ClientHandler::CreateDTLSCertificateAndKey() {
     // Modeled after "https://stackoverflow.com/questions/256405/
     // programmatically-create-x509-certificate-using-openssl".
 
@@ -542,7 +551,7 @@
     return std::make_pair(x509, pkey);
 }
 
-void MyWebSocketHandler::parseOptions(const Json::Value& options) {
+void ClientHandler::parseOptions(const Json::Value& options) {
     if (options.isMember("disable_audio") && options["disable_audio"].isBool()) {
         auto mask = OptionBits::disableAudio;
         mOptions = (mOptions & ~mask) | (options["disable_audio"].asBool() ? mask : 0);
@@ -562,7 +571,7 @@
 }
 
 // static
-void MyWebSocketHandler::CreateRandomIceCharSequence(char *dst, size_t size) {
+void ClientHandler::CreateRandomIceCharSequence(char *dst, size_t size) {
     // Per RFC 5245 an ice-char is alphanumeric, '+' or '/', i.e. 64 distinct
     // character values (6 bit).
 
@@ -587,7 +596,7 @@
 }
 
 std::pair<std::string, std::string>
-MyWebSocketHandler::createUniqueUFragAndPassword() {
+ClientHandler::createUniqueUFragAndPassword() {
     // RFC 5245, section 15.4 mandates that uFrag is at least 4 and password
     // at least 22 ice-chars long.
 
@@ -618,7 +627,7 @@
             std::string(passwordChars, sizeof(passwordChars)));
 }
 
-void MyWebSocketHandler::prepareSessions() {
+void ClientHandler::prepareSessions() {
     size_t numSessions =
         (mOptions & OptionBits::bundleTracks) ? 1 : countTracks();
 
@@ -635,7 +644,7 @@
     }
 }
 
-void MyWebSocketHandler::emitTrackIceOptionsAndFingerprint(
+void ClientHandler::emitTrackIceOptionsAndFingerprint(
         std::stringstream &ss, size_t mlineIndex) const {
     CHECK_LT(mlineIndex, mSessions.size());
     const auto &session = mSessions[mlineIndex];
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/AdbWebSocketHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/AdbHandler.h
similarity index 73%
rename from host/frontend/gcastv2/webrtc/include/webrtc/AdbWebSocketHandler.h
rename to host/frontend/gcastv2/webrtc/include/webrtc/AdbHandler.h
index 0a622b2..44ae35f 100644
--- a/host/frontend/gcastv2/webrtc/include/webrtc/AdbWebSocketHandler.h
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/AdbHandler.h
@@ -21,20 +21,19 @@
 
 #include <memory>
 
-struct AdbWebSocketHandler
-    : public WebSocketHandler,
-      public std::enable_shared_from_this<AdbWebSocketHandler> {
+struct AdbHandler :
+      public std::enable_shared_from_this<AdbHandler> {
 
-    explicit AdbWebSocketHandler(
+    explicit AdbHandler(
             std::shared_ptr<RunLoop> runLoop,
-            const std::string &adb_host_and_port);
+            const std::string &adb_host_and_port,
+            std::function<void(const uint8_t*, size_t)> send_to_client);
 
-    ~AdbWebSocketHandler() override;
+    ~AdbHandler();
 
     void run();
 
-    int handleMessage(
-            uint8_t headerByte, const uint8_t *msg, size_t len) override;
+    void handleMessage(const uint8_t *msg, size_t len);
 
 private:
     struct AdbConnection;
@@ -44,6 +43,7 @@
 
     int mSocket;
 
+    std::function<void(const uint8_t*, size_t)> send_to_client_;
+
     int setupSocket(const std::string &adb_host_and_port);
 };
-
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/MyWebSocketHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/MyWebSocketHandler.h
deleted file mode 100644
index cc70491..0000000
--- a/host/frontend/gcastv2/webrtc/include/webrtc/MyWebSocketHandler.h
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Copyright (C) 2019 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.
- */
-
-#pragma once
-
-#include <webrtc/RTPSession.h>
-#include <webrtc/RTPSocketHandler.h>
-#include <webrtc/SDP.h>
-#include <webrtc/ServerState.h>
-
-#include <https/WebSocketHandler.h>
-#include <https/RunLoop.h>
-#include <source/KeyboardSink.h>
-#include <source/TouchSink.h>
-
-#include <memory>
-#include <optional>
-#include <sstream>
-#include <string>
-#include <vector>
-
-struct MyWebSocketHandler
-    : public WebSocketHandler,
-      public std::enable_shared_from_this<MyWebSocketHandler> {
-
-    explicit MyWebSocketHandler(
-            std::shared_ptr<RunLoop> runLoop,
-            std::shared_ptr<ServerState> serverState,
-            size_t handlerId);
-
-    ~MyWebSocketHandler() override;
-
-    int handleMessage(
-            uint8_t headerByte, const uint8_t *msg, size_t len) override;
-
-private:
-    enum OptionBits : uint32_t {
-        disableAudio                        = 1,
-        bundleTracks                        = 2,
-        enableData                          = 4,
-        useSingleCertificateForAllTracks    = 8,
-        useTCP                              = 16,
-    };
-
-    using TouchSink = android::TouchSink;
-    using KeyboardSink = android::KeyboardSink;
-
-    std::shared_ptr<RunLoop> mRunLoop;
-    std::shared_ptr<ServerState> mServerState;
-    size_t mId;
-    uint32_t mOptions;
-
-    // Vector has the same ordering as the media entries in the SDP, i.e.
-    // vector index is "mlineIndex". (unless we are bundling, in which case
-    // there is only a single session).
-    std::vector<std::shared_ptr<RTPSession>> mSessions;
-
-    SDP mOfferedSDP;
-    std::vector<std::shared_ptr<RTPSocketHandler>> mRTPs;
-
-    std::shared_ptr<TouchSink> mTouchSink;
-    std::shared_ptr<KeyboardSink> mKeyboardSink;
-
-    std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
-        mCertificateAndKey;
-
-    // Pass -1 for mlineIndex to access the "general" section.
-    std::optional<std::string> getSDPValue(
-            ssize_t mlineIndex,
-            std::string_view key,
-            bool fallthroughToGeneralSection) const;
-
-    std::string getRemotePassword(size_t mlineIndex) const;
-    std::string getRemoteUFrag(size_t mlineIndex) const;
-    std::string getRemoteFingerprint(size_t mlineIndex) const;
-
-    bool getCandidate(int32_t mid);
-
-    static std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
-        CreateDTLSCertificateAndKey();
-
-    std::pair<std::string, std::string> createUniqueUFragAndPassword();
-
-    void parseOptions(const Json::Value& options);
-    size_t countTracks() const;
-
-    void prepareSessions();
-
-    void emitTrackIceOptionsAndFingerprint(
-            std::stringstream &ss, size_t mlineIndex) const;
-
-    // Returns -1 on error.
-    ssize_t mlineIndexForMid(int32_t mid) const;
-
-    static void CreateRandomIceCharSequence(char *dst, size_t size);
-};
-
-
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h
index dbbf000..5cea620 100644
--- a/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/RTPSocketHandler.h
@@ -67,6 +67,10 @@
 
     void notifyDTLSConnected();
 
+    void OnParticipantDisconnected(std::function<void()> cb) {
+      on_participant_disconnected_ = cb;
+    }
+
 private:
     struct Datagram {
         explicit Datagram(
@@ -109,6 +113,11 @@
 
     std::shared_ptr<std::vector<uint8_t>> mTcpOutBuffer;
     std::deque<std::shared_ptr<std::vector<uint8_t>>> mTcpOutBufferQueue;
+    bool packet_received_since_last_check_ = false;
+    std::function<void()> on_participant_disconnected_ = []{}; // do nothing by default
+
+    void ScheduleTimeOutCheck();
+    bool CheckParticipantTimeOut();
 
     void onReceive();
     void onDTLSReceive(const uint8_t *data, size_t size);
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h b/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h
index b98a3f4..fbda93a 100644
--- a/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/ServerState.h
@@ -50,18 +50,21 @@
 
     VideoFormat videoFormat() const { return mVideoFormat; }
 
-    size_t acquireHandlerId();
-    void releaseHandlerId(size_t id);
+    std::shared_ptr<RunLoop> run_loop() { return mRunLoop; }
+    std::string public_ip() const { return mPublicIp; }
+    void SetPublicIp(const std::string& public_ip) { mPublicIp = public_ip; }
 
-private:
+   private:
     using StreamingSource = android::StreamingSource;
 
     std::shared_ptr<RunLoop> mRunLoop;
 
     VideoFormat mVideoFormat;
 
-    std::weak_ptr<Packetizer> mVideoPacketizer;
-    std::weak_ptr<Packetizer> mAudioPacketizer;
+    std::mutex mPacketizerLock;
+
+    std::shared_ptr<Packetizer> mVideoPacketizer;
+    std::shared_ptr<Packetizer> mAudioPacketizer;
 
     std::shared_ptr<StreamingSource> mFrameBufferSource;
 
@@ -73,7 +76,7 @@
     std::shared_ptr<TouchSink> mTouchSink;
     std::shared_ptr<KeyboardSink> mKeyboardSink;
 
-    std::set<size_t> mAllocatedHandlerIds;
+    std::string mPublicIp;
 
     void MonitorScreenConnector();
 };
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/client_handler.h b/host/frontend/gcastv2/webrtc/include/webrtc/client_handler.h
new file mode 100644
index 0000000..5a13da8
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/client_handler.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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.
+ */
+
+#pragma once
+
+#include <webrtc/AdbHandler.h>
+#include <webrtc/RTPSession.h>
+#include <webrtc/RTPSocketHandler.h>
+#include <webrtc/SDP.h>
+#include <webrtc/ServerState.h>
+
+#include <https/RunLoop.h>
+#include <https/WebSocketHandler.h>
+#include <source/KeyboardSink.h>
+#include <source/TouchSink.h>
+
+#include <functional>
+#include <memory>
+#include <optional>
+#include <sstream>
+#include <string>
+#include <vector>
+
+struct ClientHandler : public std::enable_shared_from_this<ClientHandler> {
+  explicit ClientHandler(std::shared_ptr<ServerState> serverState,
+                         std::function<void(const Json::Value&)> send_client_cb);
+
+  void HandleMessage(const Json::Value& client_message);
+
+  void OnConnectionClosed(std::function<void()> cb) {
+    on_connection_closed_cb_ = cb;
+  }
+
+ private:
+  enum OptionBits : uint32_t {
+    disableAudio = 1,
+    bundleTracks = 2,
+    enableData = 4,
+    useSingleCertificateForAllTracks = 8,
+    useTCP = 16,
+  };
+
+  using TouchSink = android::TouchSink;
+  using KeyboardSink = android::KeyboardSink;
+
+  std::shared_ptr<RunLoop> mRunLoop;
+  std::shared_ptr<ServerState> mServerState;
+  uint32_t mOptions;
+  std::function<void(const Json::Value&)> sendToClient_;
+
+  // Vector has the same ordering as the media entries in the SDP, i.e.
+  // vector index is "mlineIndex". (unless we are bundling, in which case
+  // there is only a single session).
+  std::vector<std::shared_ptr<RTPSession>> mSessions;
+
+  SDP mOfferedSDP;
+  std::vector<std::shared_ptr<RTPSocketHandler>> mRTPs;
+
+  std::shared_ptr<TouchSink> mTouchSink;
+  std::shared_ptr<KeyboardSink> mKeyboardSink;
+
+  std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
+      mCertificateAndKey;
+
+  std::function<void()> on_connection_closed_cb_ = []{};
+  std::shared_ptr<AdbHandler> adb_handler_;
+
+  std::shared_ptr<AdbHandler> adb_handler();
+
+  void LogAndReplyError(const std::string& error_msg) const ;
+
+  std::string BuildOffer();
+
+  // Pass -1 for mlineIndex to access the "general" section.
+  std::optional<std::string> getSDPValue(
+      ssize_t mlineIndex, std::string_view key,
+      bool fallthroughToGeneralSection) const;
+
+  std::string getRemotePassword(size_t mlineIndex) const;
+  std::string getRemoteUFrag(size_t mlineIndex) const;
+  std::string getRemoteFingerprint(size_t mlineIndex) const;
+
+  bool GatherAndSendCandidate(int32_t mid);
+
+  static std::pair<std::shared_ptr<X509>, std::shared_ptr<EVP_PKEY>>
+  CreateDTLSCertificateAndKey();
+
+  std::pair<std::string, std::string> createUniqueUFragAndPassword();
+
+  void parseOptions(const Json::Value &options);
+  size_t countTracks() const;
+
+  void prepareSessions();
+
+  void emitTrackIceOptionsAndFingerprint(std::stringstream &ss,
+                                         size_t mlineIndex) const;
+
+  // Returns -1 on error.
+  ssize_t mlineIndexForMid(int32_t mid) const;
+
+  static void CreateRandomIceCharSequence(char *dst, size_t size);
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/sig_server_handler.h b/host/frontend/gcastv2/webrtc/include/webrtc/sig_server_handler.h
new file mode 100644
index 0000000..14cef3e
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/sig_server_handler.h
@@ -0,0 +1,47 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+//
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "webrtc/ServerState.h"
+#include "webrtc/client_handler.h"
+#include "webrtc/ws_connection.h"
+
+class SigServerHandler
+    : public WsConnectionObserver,
+      public std::enable_shared_from_this<WsConnectionObserver> {
+ public:
+  SigServerHandler(const std::string& device_id,
+                   std::shared_ptr<ServerState> server_state);
+  ~SigServerHandler() override = default;
+
+  void OnOpen() override;
+  void OnClose() override;
+  void OnError(const std::string& error) override;
+  void OnReceive(const uint8_t* msg, size_t length, bool is_binary) override;
+
+  void Connect(const std::string& server_addr, int server_port,
+               const std::string& server_path,
+               WsConnection::Security security);
+
+ private:
+  std::shared_ptr<ServerState> server_state_;
+  std::shared_ptr<WsConnection> server_connection_;
+  std::map<int, std::shared_ptr<ClientHandler>> clients_;
+  std::string device_id_;
+};
diff --git a/host/frontend/gcastv2/webrtc/include/webrtc/ws_connection.h b/host/frontend/gcastv2/webrtc/include/webrtc/ws_connection.h
new file mode 100644
index 0000000..ca81dd0
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/include/webrtc/ws_connection.h
@@ -0,0 +1,73 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+//
+
+#pragma once
+
+#include <string.h>
+
+#include <deque>
+#include <functional>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "libwebsockets.h"
+
+class WsConnectionObserver {
+ public:
+  virtual ~WsConnectionObserver() = default;
+  virtual void OnOpen() = 0;
+  virtual void OnClose() = 0;
+  virtual void OnError(const std::string& error) = 0;
+  virtual void OnReceive(const uint8_t* msg, size_t length, bool is_binary) = 0;
+};
+
+class WsConnection {
+ public:
+  enum class Security {
+    kInsecure,
+    kAllowSelfSigned,
+    kStrict,
+  };
+
+  static std::shared_ptr<WsConnection> Create();
+
+  virtual ~WsConnection() = default;
+
+  virtual void Connect() = 0;
+
+  virtual bool Send(const uint8_t* data, size_t len, bool binary = false) = 0;
+
+ protected:
+  WsConnection() = default;
+};
+
+class WsConnectionContext {
+ public:
+  static std::shared_ptr<WsConnectionContext> Create();
+
+  virtual ~WsConnectionContext() = default;
+
+  virtual std::shared_ptr<WsConnection> CreateConnection(
+      int port, const std::string& addr, const std::string& path,
+      WsConnection::Security secure,
+      std::weak_ptr<WsConnectionObserver> observer) = 0;
+
+ protected:
+  WsConnectionContext() = default;
+};
diff --git a/host/frontend/gcastv2/webrtc/sig_server_handler.cpp b/host/frontend/gcastv2/webrtc/sig_server_handler.cpp
new file mode 100644
index 0000000..5f2d1c4
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/sig_server_handler.cpp
@@ -0,0 +1,265 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+//
+
+#include "host/frontend/gcastv2/webrtc/include/webrtc/sig_server_handler.h"
+
+#include <android-base/strings.h>
+#include <gflags/gflags.h>
+#include <json/json.h>
+
+#include <webrtc/STUNClient.h>
+#include <webrtc/STUNMessage.h>
+#include <webrtc/ServerState.h>
+#include "Utils.h"
+
+#include "host/frontend/gcastv2/signaling_server/constants/signaling_constants.h"
+
+DECLARE_string(public_ip);
+
+namespace {
+
+constexpr auto kStreamIdField = "stream_id";
+constexpr auto kXResField = "x_res";
+constexpr auto kYResField = "y_res";
+constexpr auto kDpiField = "dpi";
+constexpr auto kIsTouchField = "is_touch";
+constexpr auto kDisplaysField = "displays";
+
+std::string FigureOutPublicIp(const std::string &stun_server) {
+  if (!FLAGS_public_ip.empty() && FLAGS_public_ip != "0.0.0.0") {
+    return FLAGS_public_ip;
+  }
+
+  // NOTE: We only contact the external STUN server once upon startup
+  // to determine our own public IP.
+  // This only works if NAT does not remap ports, i.e. a local port 15550
+  // is visible to the outside world on port 15550 as well.
+  // If this condition is not met, this code will have to be modified
+  // and a STUN request made for each locally bound socket before
+  // fulfilling a "MyWebSocketHandler::getCandidate" ICE request.
+
+  const addrinfo kHints = {
+      AI_ADDRCONFIG,
+      PF_INET,
+      SOCK_DGRAM,
+      IPPROTO_UDP,
+      0,        // ai_addrlen
+      nullptr,  // ai_addr
+      nullptr,  // ai_canonname
+      nullptr   // ai_next
+  };
+
+  auto pieces = SplitString(stun_server, ':');
+  CHECK_EQ(pieces.size(), 2u);
+
+  addrinfo *infos;
+  CHECK(!getaddrinfo(pieces[0].c_str(), pieces[1].c_str(), &kHints, &infos));
+
+  sockaddr_storage stunAddr;
+  memcpy(&stunAddr, infos->ai_addr, infos->ai_addrlen);
+
+  freeaddrinfo(infos);
+  infos = nullptr;
+
+  CHECK_EQ(stunAddr.ss_family, AF_INET);
+
+  std::mutex lock;
+  std::condition_variable cond;
+  bool done = false;
+
+  auto runLoop = std::make_shared<RunLoop>("STUN");
+  std::string public_ip;
+
+  auto stunClient = std::make_shared<STUNClient>(
+      runLoop, reinterpret_cast<const sockaddr_in &>(stunAddr),
+      [&lock, &cond, &done, &public_ip](int result,
+                                        const std::string &myPublicIp) {
+        CHECK(!result);
+        LOG(INFO) << "STUN-discovered public IP: " << myPublicIp;
+
+        public_ip = myPublicIp;
+
+        std::lock_guard autoLock(lock);
+        done = true;
+        cond.notify_all();
+      });
+
+  stunClient->run();
+
+  std::unique_lock autoLock(lock);
+  while (!done) {
+    cond.wait(autoLock);
+  }
+  return public_ip;
+}
+
+std::string StunServerFromConfig(const Json::Value &server_config) {
+  if (!server_config.isMember(cvd::webrtc_signaling::kServersField) ||
+      !server_config[cvd::webrtc_signaling::kServersField].isArray()) {
+    return "";
+  }
+  auto ice_servers = server_config[cvd::webrtc_signaling::kServersField];
+  for (Json::ArrayIndex i = 0; i < ice_servers.size(); ++i) {
+    if (!ice_servers[i].isMember(cvd::webrtc_signaling::kUrlsField)) {
+      LOG(WARNING) << "Ice server received without a urls field";
+      continue;
+    }
+    auto url = ice_servers[i][cvd::webrtc_signaling::kUrlsField];
+    if (url.isArray()) {
+      if (url.size() == 0) {
+        LOG(WARNING) << "Ice server received with empty urls field";
+        continue;
+      }
+      url = url[0];
+    }
+    if (!url.isString()) {
+      LOG(WARNING) << "Ice server with non-string url";
+      continue;
+    }
+    auto url_str = url.asString();
+    if (::android::base::StartsWith(url_str, "stun:")) {
+      return url_str.substr(std::string("stun:").size());
+    }
+  }
+  return "";
+}
+
+void SendJson(std::shared_ptr<WsConnection> ws_conn, const Json::Value &data) {
+  Json::FastWriter json_writer;
+  auto data_str = json_writer.write(data);
+  ws_conn->Send(reinterpret_cast<const uint8_t *>(data_str.c_str()),
+                data_str.size());
+}
+
+bool ParseMessage(const uint8_t *data, size_t length, Json::Value *msg_out) {
+  Json::Reader json_reader;
+  auto str = reinterpret_cast<const char *>(data);
+  return json_reader.parse(str, str + length, *msg_out) >= 0;
+}
+
+}  // namespace
+
+SigServerHandler::SigServerHandler(const std::string &device_id,
+                                   std::shared_ptr<ServerState> server_state)
+    : server_state_(server_state), device_id_(device_id) {}
+
+void SigServerHandler::Connect(const std::string &server_addr, int server_port,
+                               const std::string &server_path,
+                               WsConnection::Security security) {
+  // This can be a local variable since the connection object will keep a
+  // reference to it.
+  auto ws_context = WsConnectionContext::Create();
+  server_connection_ = ws_context->CreateConnection(
+      server_port, server_addr, server_path, security, weak_from_this());
+
+  CHECK(server_connection_) << "Unable to create websocket connection object";
+
+  server_connection_->Connect();
+}
+
+void SigServerHandler::OnOpen() {
+  auto config = vsoc::CuttlefishConfig::Get();
+  Json::Value register_obj;
+  register_obj[cvd::webrtc_signaling::kTypeField] =
+      cvd::webrtc_signaling::kRegisterType;
+  register_obj[cvd::webrtc_signaling::kDeviceIdField] =
+      device_id_.empty() ? config->ForDefaultInstance().instance_name()
+                         : device_id_;
+  Json::Value device_info;
+  Json::Value displays(Json::ValueType::arrayValue);
+  Json::Value main_display;
+
+  main_display[kStreamIdField] = "display_0";
+  main_display[kXResField] = config->x_res();
+  main_display[kYResField] = config->y_res();
+  main_display[kDpiField] = config->dpi();
+  main_display[kIsTouchField] = true;
+
+  displays.append(main_display);
+  device_info[kDisplaysField] = displays;
+  register_obj[cvd::webrtc_signaling::kDeviceInfoField] = device_info;
+  SendJson(server_connection_, register_obj);
+}
+
+void SigServerHandler::OnClose() {
+  LOG(WARNING) << "Websocket closed unexpectedly";
+}
+
+void SigServerHandler::OnError(const std::string &error) {
+  LOG(FATAL) << "Error detected on server connection: " << error;
+}
+
+void SigServerHandler::OnReceive(const uint8_t *data, size_t length,
+                                 bool is_binary) {
+  Json::Value server_message;
+  if (is_binary || !ParseMessage(data, length, &server_message)) {
+    LOG(ERROR) << "Received invalid JSON from server: '"
+               << (is_binary ? std::string("(binary_data)")
+                             : std::string(data, data + length))
+               << "'";
+    return;
+  }
+  if (!server_message.isMember(cvd::webrtc_signaling::kTypeField) ||
+      !server_message[cvd::webrtc_signaling::kTypeField].isString()) {
+    LOG(ERROR) << "No message_type field from server";
+    return;
+  }
+  auto type = server_message[cvd::webrtc_signaling::kTypeField].asString();
+  if (type == cvd::webrtc_signaling::kConfigType) {
+    auto stun_server = StunServerFromConfig(server_message);
+    auto public_ip =
+        stun_server.empty() ? FLAGS_public_ip : FigureOutPublicIp(stun_server);
+    server_state_->SetPublicIp(public_ip);
+  } else if (type == cvd::webrtc_signaling::kClientMessageType) {
+    if (!server_message.isMember(cvd::webrtc_signaling::kClientIdField) ||
+        !server_message[cvd::webrtc_signaling::kClientIdField].isInt()) {
+      LOG(ERROR) << "Client message received without valid client id";
+      return;
+    }
+    auto client_id =
+        server_message[cvd::webrtc_signaling::kClientIdField].asInt();
+    if (!server_message.isMember(cvd::webrtc_signaling::kPayloadField)) {
+      LOG(ERROR) << "Received empty client message";
+      return;
+    }
+    auto client_message = server_message[cvd::webrtc_signaling::kPayloadField];
+    if (clients_.count(client_id) == 0) {
+      clients_[client_id].reset(new ClientHandler(
+          server_state_, [this, client_id](const Json::Value &msg) {
+            Json::Value wrapper;
+            wrapper[cvd::webrtc_signaling::kPayloadField] = msg;
+            wrapper[cvd::webrtc_signaling::kTypeField] =
+                cvd::webrtc_signaling::kForwardType;
+            wrapper[cvd::webrtc_signaling::kClientIdField] = client_id;
+            // This is safe to call from the webrtc runloop because
+            // WsConnection is thread safe
+            SendJson(server_connection_, wrapper);
+          }));
+      clients_[client_id]->OnConnectionClosed([this, client_id]{
+        clients_.erase(client_id);
+      });
+    }
+
+    auto client_handler = clients_[client_id];
+
+    // Client handler operations need to happen in its own runloop
+    server_state_->run_loop()->post([client_handler, client_message] {
+      client_handler->HandleMessage(client_message);
+    });
+  } else {
+    LOG(ERROR) << "Unknown message type: " << type;
+    return;
+  }
+}
diff --git a/host/frontend/gcastv2/webrtc/webRTC.cpp b/host/frontend/gcastv2/webrtc/webRTC.cpp
index 427f1a3..e17309d 100644
--- a/host/frontend/gcastv2/webrtc/webRTC.cpp
+++ b/host/frontend/gcastv2/webrtc/webRTC.cpp
@@ -14,21 +14,24 @@
  * limitations under the License.
  */
 
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+
+#include <host/libs/config/cuttlefish_config.h>
+
 #include "Utils.h"
 
-#include <webrtc/AdbWebSocketHandler.h>
+#include <webrtc/AdbHandler.h>
 #include <webrtc/DTLS.h>
-#include <webrtc/MyWebSocketHandler.h>
 #include <webrtc/RTPSocketHandler.h>
 #include <webrtc/ServerState.h>
-#include <webrtc/STUNClient.h>
-#include <webrtc/STUNMessage.h>
+#include <webrtc/sig_server_handler.h>
 
 #include <https/HTTPServer.h>
 #include <https/PlainSocket.h>
 #include <https/RunLoop.h>
-#include <https/SafeCallbackable.h>
 #include <https/SSLSocket.h>
+#include <https/SafeCallbackable.h>
 #include <https/Support.h>
 
 #include <iostream>
@@ -38,159 +41,48 @@
 
 #include <gflags/gflags.h>
 
-DEFINE_int32(http_server_port, 8443, "The port for the http server.");
-DEFINE_bool(use_secure_http, true, "Whether to use HTTPS or HTTP.");
-DEFINE_string(
-        public_ip,
-        "0.0.0.0",
-        "Public IPv4 address of your server, a.b.c.d format");
-DEFINE_string(
-        assets_dir,
-        "webrtc",
-        "Directory with location of webpage assets.");
-DEFINE_string(
-        certs_dir,
-        "webrtc/certs",
-        "Directory to certificates.");
+DEFINE_string(public_ip, "0.0.0.0", "Public IPv4 address, a.b.c.d format");
 
 DEFINE_int32(touch_fd, -1, "An fd to listen on for touch connections.");
 DEFINE_int32(keyboard_fd, -1, "An fd to listen on for keyboard connections.");
 DEFINE_int32(frame_server_fd, -1, "An fd to listen on for frame updates");
-DEFINE_bool(write_virtio_input, false, "Whether to send input events in virtio format.");
+DEFINE_bool(write_virtio_input, false,
+            "Whether to send input events in virtio format.");
 
 DEFINE_string(adb, "", "Interface:port of local adb service.");
 
-DEFINE_string(
-        stun_server,
-        "stun.l.google.com:19302",
-        "host:port of STUN server to use for public address resolution");
-
 int main(int argc, char **argv) {
-    ::gflags::ParseCommandLineFlags(&argc, &argv, true);
+  ::gflags::ParseCommandLineFlags(&argc, &argv, true);
+  ::android::base::InitLogging(argv, android::base::StderrLogger);
 
-    SSLSocket::Init();
-    DTLS::Init();
+  SSLSocket::Init();
+  DTLS::Init();
 
-    if (FLAGS_public_ip.empty() || FLAGS_public_ip == "0.0.0.0") {
-        // NOTE: We only contact the external STUN server once upon startup
-        // to determine our own public IP.
-        // This only works if NAT does not remap ports, i.e. a local port 15550
-        // is visible to the outside world on port 15550 as well.
-        // If this condition is not met, this code will have to be modified
-        // and a STUN request made for each locally bound socket before
-        // fulfilling a "MyWebSocketHandler::getCandidate" ICE request.
+  auto config = vsoc::CuttlefishConfig::Get();
 
-        const addrinfo kHints = {
-            AI_ADDRCONFIG,
-            PF_INET,
-            SOCK_DGRAM,
-            IPPROTO_UDP,
-            0,  // ai_addrlen
-            nullptr,  // ai_addr
-            nullptr,  // ai_canonname
-            nullptr  // ai_next
-        };
+  auto sig_server_addr = config->sig_server_address();
+  auto sig_server_port = config->sig_server_port();
+  auto sig_server_path = config->sig_server_path();
+  auto sig_server_strict = config->sig_server_strict();
+  auto device_id = config->ForDefaultInstance().webrtc_device_id();
 
-        auto pieces = SplitString(FLAGS_stun_server, ':');
-        CHECK_EQ(pieces.size(), 2u);
+  auto runLoop = RunLoop::main();
 
-        addrinfo *infos;
-        CHECK(!getaddrinfo(pieces[0].c_str(), pieces[1].c_str(), &kHints, &infos));
+  auto state =
+      std::make_shared<ServerState>(runLoop, ServerState::VideoFormat::VP8);
 
-        sockaddr_storage stunAddr;
-        memcpy(&stunAddr, infos->ai_addr, infos->ai_addrlen);
+  auto security = WsConnection::Security::kAllowSelfSigned;
+  if (sig_server_strict) {
+    security = WsConnection::Security::kStrict;
+  }
 
-        freeaddrinfo(infos);
-        infos = nullptr;
+  auto sig_server_handler =
+      std::make_shared<SigServerHandler>(device_id, state);
 
-        CHECK_EQ(stunAddr.ss_family, AF_INET);
+  sig_server_handler->Connect(sig_server_addr, sig_server_port, sig_server_path,
+                              security);
 
-        std::mutex lock;
-        std::condition_variable cond;
-        bool done = false;
+  runLoop->run();
 
-        auto runLoop = std::make_shared<RunLoop>("STUN");
-
-        auto stunClient = std::make_shared<STUNClient>(
-                runLoop,
-                reinterpret_cast<const sockaddr_in &>(stunAddr),
-                [&lock, &cond, &done](int result, const std::string &myPublicIp) {
-                    CHECK(!result);
-                    LOG(INFO)
-                        << "STUN-discovered public IP: " << myPublicIp;
-
-                    FLAGS_public_ip = myPublicIp;
-
-                    std::lock_guard autoLock(lock);
-                    done = true;
-                    cond.notify_all();
-                });
-
-        stunClient->run();
-
-        std::unique_lock autoLock(lock);
-        while (!done) {
-            cond.wait(autoLock);
-        }
-    }
-
-    auto runLoop = RunLoop::main();
-
-    auto state = std::make_shared<ServerState>(
-            runLoop, ServerState::VideoFormat::VP8);
-
-    auto port = FLAGS_http_server_port;
-
-    auto httpd = std::make_shared<HTTPServer>(
-            runLoop,
-            "0.0.0.0",
-            port,
-            FLAGS_use_secure_http
-                ? ServerSocket::TransportType::TLS
-                : ServerSocket::TransportType::TCP,
-            FLAGS_certs_dir + "/server.crt",
-            FLAGS_certs_dir + "/server.key");
-
-    const std::string index_html = FLAGS_assets_dir + "/index.html";
-    const std::string logcat_js = FLAGS_assets_dir + "/js/logcat.js";
-    const std::string app_js = FLAGS_assets_dir + "/js/app.js";
-    const std::string viewpane_js = FLAGS_assets_dir + "/js/viewpane.js";
-    const std::string cf_webrtc_js = FLAGS_assets_dir + "/js/cf_webrtc.js";
-    const std::string style_css = FLAGS_assets_dir + "/style.css";
-
-    httpd->addStaticFile("/index.html", index_html.c_str());
-    httpd->addStaticFile("/js/logcat.js", logcat_js.c_str());
-    httpd->addStaticFile("/js/app.js", app_js.c_str());
-    httpd->addStaticFile("/js/viewpane.js", viewpane_js.c_str());
-    httpd->addStaticFile("/js/cf_webrtc.js", cf_webrtc_js.c_str());
-    httpd->addStaticFile("/style.css", style_css.c_str());
-
-    httpd->addWebSocketHandlerFactory(
-            "/control",
-            [runLoop, state]{
-                auto id = state->acquireHandlerId();
-
-                auto handler =
-                    std::make_shared<MyWebSocketHandler>(runLoop, state, id);
-
-                return std::make_pair(0 /* OK */, handler);
-            });
-
-    if (!FLAGS_adb.empty()) {
-        httpd->addWebSocketHandlerFactory(
-                "/control_adb",
-                [runLoop]{
-                    auto handler = std::make_shared<AdbWebSocketHandler>(
-                            runLoop, FLAGS_adb);
-
-                    handler->run();
-
-                    return std::make_pair(0 /* OK */, handler);
-                });
-    }
-
-    httpd->run();
-    runLoop->run();
-
-    return 0;
+  return 0;
 }
diff --git a/host/frontend/gcastv2/webrtc/ws_connection.cpp b/host/frontend/gcastv2/webrtc/ws_connection.cpp
new file mode 100644
index 0000000..3d3977a
--- /dev/null
+++ b/host/frontend/gcastv2/webrtc/ws_connection.cpp
@@ -0,0 +1,403 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+//
+
+#include "webrtc/ws_connection.h"
+
+#include "android-base/logging.h"
+#include "libwebsockets.h"
+
+class WsConnectionContextImpl;
+
+class WsConnectionImpl : public WsConnection,
+                         public std::enable_shared_from_this<WsConnectionImpl> {
+ public:
+  struct CreateConnectionSul {
+    lws_sorted_usec_list_t sul = {};
+    std::weak_ptr<WsConnectionImpl> weak_this;
+  };
+
+  WsConnectionImpl(int port, const std::string& addr, const std::string& path,
+                   Security secure,
+                   std::weak_ptr<WsConnectionObserver> observer,
+                   std::shared_ptr<WsConnectionContextImpl> context);
+
+  ~WsConnectionImpl() override;
+
+  void Connect() override;
+  void ConnectInner();
+
+  bool Send(const uint8_t* data, size_t len, bool binary = false) override;
+
+  void OnError(const std::string& error);
+  void OnReceive(const uint8_t* data, size_t len, bool is_binary);
+  void OnOpen();
+  void OnClose();
+  void OnWriteable();
+
+ private:
+  struct WsBuffer {
+    WsBuffer() = default;
+    WsBuffer(const uint8_t* data, size_t len, bool binary)
+        : buffer_(LWS_PRE + len), is_binary_(binary) {
+      memcpy(&buffer_[LWS_PRE], data, len);
+    }
+
+    uint8_t* data() { return &buffer_[LWS_PRE]; }
+    bool is_binary() const { return is_binary_; }
+    size_t size() const { return buffer_.size() - LWS_PRE; }
+
+   private:
+    std::vector<uint8_t> buffer_;
+    bool is_binary_;
+  };
+
+  CreateConnectionSul extended_sul_;
+  struct lws* wsi_;
+  const int port_;
+  const std::string addr_;
+  const std::string path_;
+  const Security security_;
+
+  std::weak_ptr<WsConnectionObserver> observer_;
+
+  // each element contains the data to be sent and whether it's binary or not
+  std::deque<WsBuffer> write_queue_;
+  std::mutex write_queue_mutex_;
+  // The connection object should not outlive the context object. This reference
+  // guarantees it.
+  std::shared_ptr<WsConnectionContextImpl> context_;
+};
+
+class WsConnectionContextImpl
+    : public WsConnectionContext,
+      public std::enable_shared_from_this<WsConnectionContextImpl> {
+ public:
+  WsConnectionContextImpl(struct lws_context* lws_ctx);
+  ~WsConnectionContextImpl() override;
+
+  std::shared_ptr<WsConnection> CreateConnection(
+      int port, const std::string& addr, const std::string& path,
+      WsConnection::Security secure,
+      std::weak_ptr<WsConnectionObserver> observer) override;
+
+  void RememberConnection(void*, std::weak_ptr<WsConnectionImpl>);
+  void ForgetConnection(void*);
+  std::shared_ptr<WsConnectionImpl> GetConnection(void*);
+
+  struct lws_context* lws_context() {
+    return lws_context_;
+  }
+
+ private:
+  void Start();
+
+  std::map<void*, std::weak_ptr<WsConnectionImpl>> weak_by_ptr_;
+  std::mutex map_mutex_;
+  struct lws_context* lws_context_;
+  std::thread message_loop_;
+};
+
+int LwsCallback(struct lws* wsi, enum lws_callback_reasons reason, void* user,
+                void* in, size_t len);
+void CreateConnectionCallback(lws_sorted_usec_list_t* sul);
+
+namespace {
+
+constexpr char kProtocolName[] = "lws-websocket-protocol";
+constexpr int kBufferSize = 65536;
+
+const uint32_t backoff_ms[] = {1000, 2000, 3000, 4000, 5000};
+
+const lws_retry_bo_t kRetry = {
+    .retry_ms_table = backoff_ms,
+    .retry_ms_table_count = LWS_ARRAY_SIZE(backoff_ms),
+    .conceal_count = LWS_ARRAY_SIZE(backoff_ms),
+
+    .secs_since_valid_ping = 3,    /* force PINGs after secs idle */
+    .secs_since_valid_hangup = 10, /* hangup after secs idle */
+
+    .jitter_percent = 20,
+};
+
+const struct lws_protocols kProtocols[2] = {
+    {kProtocolName, LwsCallback, 0, kBufferSize, 0, NULL, 0},
+    {NULL, NULL, 0, 0, 0, NULL, 0}};
+
+}  // namespace
+
+std::shared_ptr<WsConnectionContext> WsConnectionContext::Create() {
+  struct lws_context_creation_info context_info = {};
+  context_info.port = CONTEXT_PORT_NO_LISTEN;
+  context_info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
+  context_info.protocols = kProtocols;
+  struct lws_context* lws_ctx = lws_create_context(&context_info);
+  if (!lws_ctx) {
+    return nullptr;
+  }
+  auto ret = std::shared_ptr<WsConnectionContext>(
+      new WsConnectionContextImpl(lws_ctx));
+  return ret;
+}
+
+WsConnectionContextImpl::WsConnectionContextImpl(struct lws_context* lws_ctx)
+    : lws_context_(lws_ctx) {
+  Start();
+}
+
+WsConnectionContextImpl::~WsConnectionContextImpl() {
+  lws_context_destroy(lws_context_);
+  if (message_loop_.joinable()) message_loop_.join();
+}
+
+void WsConnectionContextImpl::Start() {
+  message_loop_ = std::thread([this]() {
+    for (;;) {
+      if (lws_service(lws_context_, 0) < 0) {
+        break;
+      }
+    }
+  });
+}
+
+std::shared_ptr<WsConnection> WsConnectionContextImpl::CreateConnection(
+    int port, const std::string& addr, const std::string& path,
+    WsConnection::Security security,
+    std::weak_ptr<WsConnectionObserver> observer) {
+  std::shared_ptr<WsConnection> ret(new WsConnectionImpl(
+      port, addr, path, security, observer, shared_from_this()));
+  return ret;
+}
+
+std::shared_ptr<WsConnectionImpl> WsConnectionContextImpl::GetConnection(
+    void* raw) {
+  std::shared_ptr<WsConnectionImpl> connection;
+  {
+    std::lock_guard<std::mutex> lock(map_mutex_);
+    if (weak_by_ptr_.count(raw) == 0) {
+      return nullptr;
+    }
+    connection = weak_by_ptr_[raw].lock();
+    if (!connection) {
+      weak_by_ptr_.erase(raw);
+    }
+  }
+  return connection;
+}
+
+void WsConnectionContextImpl::RememberConnection(
+    void* raw, std::weak_ptr<WsConnectionImpl> conn) {
+  std::lock_guard<std::mutex> lock(map_mutex_);
+  weak_by_ptr_.emplace(
+      std::pair<void*, std::weak_ptr<WsConnectionImpl>>(raw, conn));
+}
+
+void WsConnectionContextImpl::ForgetConnection(void* raw) {
+  std::lock_guard<std::mutex> lock(map_mutex_);
+  weak_by_ptr_.erase(raw);
+}
+
+WsConnectionImpl::WsConnectionImpl(
+    int port, const std::string& addr, const std::string& path,
+    Security security, std::weak_ptr<WsConnectionObserver> observer,
+    std::shared_ptr<WsConnectionContextImpl> context)
+    : port_(port),
+      addr_(addr),
+      path_(path),
+      security_(security),
+      observer_(observer),
+      context_(context) {}
+
+WsConnectionImpl::~WsConnectionImpl() {
+  context_->ForgetConnection(this);
+  // This will cause the callback to be called which will drop the connection
+  // after seeing the context doesn't remember this object
+  lws_callback_on_writable(wsi_);
+}
+
+void WsConnectionImpl::Connect() {
+  memset(&extended_sul_.sul, 0, sizeof(extended_sul_.sul));
+  extended_sul_.weak_this = weak_from_this();
+  lws_sul_schedule(context_->lws_context(), 0, &extended_sul_.sul,
+                   CreateConnectionCallback, 1);
+}
+
+void WsConnectionImpl::OnError(const std::string& error) {
+  auto observer = observer_.lock();
+  if (observer) {
+    observer->OnError(error);
+  }
+}
+void WsConnectionImpl::OnReceive(const uint8_t* data, size_t len,
+                                 bool is_binary) {
+  auto observer = observer_.lock();
+  if (observer) {
+    observer->OnReceive(data, len, is_binary);
+  }
+}
+void WsConnectionImpl::OnOpen() {
+  auto observer = observer_.lock();
+  if (observer) {
+    observer->OnOpen();
+  }
+}
+void WsConnectionImpl::OnClose() {
+  auto observer = observer_.lock();
+  if (observer) {
+    observer->OnClose();
+  }
+}
+
+void WsConnectionImpl::OnWriteable() {
+  WsBuffer buffer;
+  {
+    std::lock_guard<std::mutex> lock(write_queue_mutex_);
+    if (write_queue_.size() == 0) {
+      return;
+    }
+    buffer = std::move(write_queue_.front());
+    write_queue_.pop_front();
+  }
+  auto flags = lws_write_ws_flags(
+      buffer.is_binary() ? LWS_WRITE_BINARY : LWS_WRITE_TEXT, true, true);
+  auto res = lws_write(wsi_, buffer.data(), buffer.size(),
+                       (enum lws_write_protocol)flags);
+  if (res != buffer.size()) {
+    LOG(WARNING) << "Unable to send the entire message!";
+  }
+}
+
+bool WsConnectionImpl::Send(const uint8_t* data, size_t len, bool binary) {
+  if (!wsi_) {
+    LOG(WARNING) << "Send called on an uninitialized connection!!";
+    return false;
+  }
+  WsBuffer buffer(data, len, binary);
+  {
+    std::lock_guard<std::mutex> lock(write_queue_mutex_);
+    write_queue_.emplace_back(std::move(buffer));
+  }
+
+  lws_callback_on_writable(wsi_);
+  return true;
+}
+
+int LwsCallback(struct lws* wsi, enum lws_callback_reasons reason, void* user,
+                void* in, size_t len) {
+  constexpr int DROP = -1;
+  constexpr int OK = 0;
+
+  // For some values of `reason`, `user` doesn't point to the value provided
+  // when the connection was created. This function object should be used with
+  // care.
+  auto with_connection =
+      [wsi, user](std::function<void(std::shared_ptr<WsConnectionImpl>)> cb) {
+        auto context = reinterpret_cast<WsConnectionContextImpl*>(user);
+        auto connection = context->GetConnection(wsi);
+        if (!connection) {
+          return DROP;
+        }
+        cb(connection);
+        return OK;
+      };
+
+  switch (reason) {
+    case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
+      return with_connection(
+          [in](std::shared_ptr<WsConnectionImpl> connection) {
+            connection->OnError(in ? (char*)in : "(null)");
+          });
+
+    case LWS_CALLBACK_CLIENT_RECEIVE:
+      return with_connection(
+          [in, len, wsi](std::shared_ptr<WsConnectionImpl> connection) {
+            connection->OnReceive((const uint8_t*)in, len,
+                                  lws_frame_is_binary(wsi));
+          });
+
+    case LWS_CALLBACK_CLIENT_ESTABLISHED:
+      return with_connection([](std::shared_ptr<WsConnectionImpl> connection) {
+        connection->OnOpen();
+      });
+
+    case LWS_CALLBACK_CLIENT_CLOSED:
+      return with_connection([](std::shared_ptr<WsConnectionImpl> connection) {
+        connection->OnClose();
+      });
+
+    case LWS_CALLBACK_CLIENT_WRITEABLE:
+      return with_connection([](std::shared_ptr<WsConnectionImpl> connection) {
+        connection->OnWriteable();
+      });
+
+    default:
+      LOG(VERBOSE) << "Unhandled value: " << reason;
+      return lws_callback_http_dummy(wsi, reason, user, in, len);
+  }
+}
+
+void CreateConnectionCallback(lws_sorted_usec_list_t* sul) {
+  std::shared_ptr<WsConnectionImpl> connection =
+      reinterpret_cast<WsConnectionImpl::CreateConnectionSul*>(sul)
+          ->weak_this.lock();
+  if (!connection) {
+    LOG(WARNING) << "The object was already destroyed by the time of the first "
+                 << "connection attempt. That's unusual.";
+    return;
+  }
+  connection->ConnectInner();
+}
+
+void WsConnectionImpl::ConnectInner() {
+  struct lws_client_connect_info connect_info;
+
+  memset(&connect_info, 0, sizeof(connect_info));
+
+  connect_info.context = context_->lws_context();
+  connect_info.port = port_;
+  connect_info.address = addr_.c_str();
+  connect_info.path = path_.c_str();
+  connect_info.host = connect_info.address;
+  connect_info.origin = connect_info.address;
+  switch (security_) {
+    case Security::kAllowSelfSigned:
+      connect_info.ssl_connection = LCCSCF_ALLOW_SELFSIGNED |
+                                    LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK |
+                                    LCCSCF_USE_SSL;
+      break;
+    case Security::kStrict:
+      connect_info.ssl_connection = LCCSCF_USE_SSL;
+      break;
+    case Security::kInsecure:
+      connect_info.ssl_connection = 0;
+      break;
+  }
+  connect_info.protocol = "UNNUSED";
+  connect_info.local_protocol_name = kProtocolName;
+  connect_info.pwsi = &wsi_;
+  connect_info.retry_and_idle_policy = &kRetry;
+  // There is no guarantee the connection object still exists when the callback
+  // is called. Put the context instead as the user data which is guaranteed to
+  // still exist and holds a weak ptr to the connection.
+  connect_info.userdata = context_.get();
+
+  if (lws_client_connect_via_info(&connect_info)) {
+    // wsi_ is not initialized until after the call to
+    // lws_client_connect_via_info(). Luckily, this is guaranteed to run before
+    // the protocol callback is called because it runs in the same loop.
+    context_->RememberConnection(wsi_, weak_from_this());
+  } else {
+    LOG(ERROR) << "Connection failed!";
+  }
+}
diff --git a/host/libs/config/cuttlefish_config.cpp b/host/libs/config/cuttlefish_config.cpp
index 83428a9..af27987 100644
--- a/host/libs/config/cuttlefish_config.cpp
+++ b/host/libs/config/cuttlefish_config.cpp
@@ -153,6 +153,13 @@
 const char* kTombstoneReceiverPort = "tombstone_receiver_port";
 
 const char* kWebRTCCertsDir = "webrtc_certs_dir";
+const char* kSigServerBinary = "webrtc_sig_server_binary";
+const char* kSigServerPort = "webrtc_sig_server_port";
+const char* kSigServerAddress = "webrtc_sig_server_addr";
+const char* kSigServerPath = "webrtc_sig_server_path";
+const char* kSigServerStrict = "webrtc_sig_server_strict";
+const char* kWebrtcDeviceId = "webrtc_device_id";
+const char* kStartSigServer = "webrtc_start_sig_server";
 
 const char* kBootloader = "bootloader";
 const char* kUseBootloader = "use_bootloader";
@@ -172,6 +179,9 @@
 const char* kFramesServerPort = "frames_server_port";
 const char* kTouchServerPort = "touch_server_port";
 const char* kKeyboardServerPort = "keyboard_server_port";
+
+const char* kRilDns = "ril_dns";
+const char* kKeymasterVsockPort = "keymaster_vsock_port";
 }  // namespace
 
 namespace vsoc {
@@ -408,6 +418,10 @@
   return cvd::AbsolutePath(PerInstancePath("launcher.log"));
 }
 
+std::string CuttlefishConfig::InstanceSpecific::sdcard_path() const {
+  return cvd::AbsolutePath(PerInstancePath("sdcard.img"));
+}
+
 std::string CuttlefishConfig::InstanceSpecific::mobile_bridge_name() const {
   return (*Dictionary())[kMobileBridgeName].asString();
 }
@@ -633,6 +647,14 @@
   (*Dictionary())[kKeyboardServerPort] = keyboard_server_port;
 }
 
+int CuttlefishConfig::InstanceSpecific::keymaster_vsock_port() const {
+  return (*Dictionary())[kKeymasterVsockPort].asInt();
+}
+
+void CuttlefishConfig::MutableInstanceSpecific::set_keymaster_vsock_port(int keymaster_vsock_port) {
+  (*Dictionary())[kKeymasterVsockPort] = keymaster_vsock_port;
+}
+
 int CuttlefishConfig::InstanceSpecific::tombstone_receiver_port() const {
   return (*Dictionary())[kTombstoneReceiverPort].asInt();
 }
@@ -856,6 +878,64 @@
   return (*dictionary_)[kWebRTCCertsDir].asString();
 }
 
+void CuttlefishConfig::set_sig_server_binary(const std::string& binary) {
+  SetPath(kSigServerBinary, binary);
+}
+
+std::string CuttlefishConfig::sig_server_binary() const {
+  return (*dictionary_)[kSigServerBinary].asString();
+}
+
+void CuttlefishConfig::set_sig_server_port(int port) {
+  (*dictionary_)[kSigServerPort] = port;
+}
+
+int CuttlefishConfig::sig_server_port() const {
+  return (*dictionary_)[kSigServerPort].asInt();
+}
+
+void CuttlefishConfig::set_sig_server_address(const std::string& addr) {
+  (*dictionary_)[kSigServerAddress] = addr;
+}
+
+std::string CuttlefishConfig::sig_server_address() const {
+  return (*dictionary_)[kSigServerAddress].asString();
+}
+
+void CuttlefishConfig::set_sig_server_path(const std::string& path) {
+  // Don't use SetPath here, it's a URL path not a file system path
+  (*dictionary_)[kSigServerPath] = path;
+}
+
+std::string CuttlefishConfig::sig_server_path() const {
+  return (*dictionary_)[kSigServerPath].asString();
+}
+
+void CuttlefishConfig::set_sig_server_strict(bool strict) {
+  (*dictionary_)[kSigServerStrict] = strict;
+}
+
+bool CuttlefishConfig::sig_server_strict() const {
+  return (*dictionary_)[kSigServerStrict].asBool();
+}
+
+void CuttlefishConfig::MutableInstanceSpecific::set_webrtc_device_id(
+    const std::string& id) {
+  (*Dictionary())[kWebrtcDeviceId] = id;
+}
+
+std::string CuttlefishConfig::InstanceSpecific::webrtc_device_id() const {
+  return (*Dictionary())[kWebrtcDeviceId].asString();
+}
+
+void CuttlefishConfig::MutableInstanceSpecific::set_start_webrtc_signaling_server(bool start) {
+  (*Dictionary())[kStartSigServer] = start;
+}
+
+bool CuttlefishConfig::InstanceSpecific::start_webrtc_sig_server() const {
+  return (*Dictionary())[kStartSigServer].asBool();
+}
+
 std::string CuttlefishConfig::InstanceSpecific::touch_socket_path() const {
   return PerInstanceInternalPath("touch.sock");
 }
@@ -954,6 +1034,13 @@
   return cmdline;
 }
 
+void CuttlefishConfig::set_ril_dns(const std::string& ril_dns) {
+  (*dictionary_)[kRilDns] = ril_dns;
+}
+std::string CuttlefishConfig::ril_dns()const {
+  return (*dictionary_)[kRilDns].asString();
+}
+
 // Creates the (initially empty) config object and populates it with values from
 // the config file if the CUTTLEFISH_CONFIG_FILE env variable is present.
 // Returns nullptr if there was an error loading from file
@@ -1081,12 +1168,6 @@
 }
 int ForCurrentInstance(int base) { return base + GetInstance() - 1; }
 
-std::string GetDefaultPerInstanceDir() {
-  std::ostringstream stream;
-  stream << std::getenv("HOME") << "/cuttlefish_runtime";
-  return stream.str();
-}
-
 int GetDefaultPerInstanceVsockCid() {
   constexpr int kFirstGuestCid = 3;
   return vsoc::HostSupportsVsock() ? ForCurrentInstance(kFirstGuestCid) : 0;
@@ -1101,8 +1182,7 @@
 
 std::string DefaultGuestImagePath(const std::string& file_name) {
   return (cvd::StringFromEnv("ANDROID_PRODUCT_OUT",
-                             cvd::StringFromEnv("HOME", ".")) +
-          "/") +
+                             cvd::StringFromEnv("HOME", "."))) +
          file_name;
 }
 
diff --git a/host/libs/config/cuttlefish_config.h b/host/libs/config/cuttlefish_config.h
index 142c257..2ff6500 100644
--- a/host/libs/config/cuttlefish_config.h
+++ b/host/libs/config/cuttlefish_config.h
@@ -282,11 +282,36 @@
   void set_extra_kernel_cmdline(std::string extra_cmdline);
   std::vector<std::string> extra_kernel_cmdline() const;
 
+  // A directory containing the SSL certificates for the signaling server
   void set_webrtc_certs_dir(const std::string& certs_dir);
   std::string webrtc_certs_dir() const;
 
-  void set_dialog_certs_dir(const std::string& certs_dir);
-  std::string dialog_certs_dir() const;
+  // The path to the webrtc signaling server binary
+  void set_sig_server_binary(const std::string& sig_server_binary);
+  std::string sig_server_binary() const;
+
+  // The port for the webrtc signaling server. It's used by the signaling server
+  // to bind to it and by the webrtc process to connect to and register itself
+  void set_sig_server_port(int port);
+  int sig_server_port() const;
+
+  // The address of the signaling server
+  void set_sig_server_address(const std::string& addr);
+  std::string sig_server_address() const;
+
+  // The path section of the url where the webrtc process registers itself with
+  // the signaling server
+  void set_sig_server_path(const std::string& path);
+  std::string sig_server_path() const;
+
+  // Whether the webrtc process should attempt to verify the authenticity of the
+  // signaling server (reject self signed certificates)
+  void set_sig_server_strict(bool strict);
+  bool sig_server_strict() const;
+
+  // The dns address of mobile network (RIL)
+  void set_ril_dns(const std::string& ril_dns);
+  std::string ril_dns() const;
 
   class InstanceSpecific;
   class MutableInstanceSpecific;
@@ -336,6 +361,8 @@
     int host_port() const;
     // Port number to connect to the tpm server on the host
     int tpm_port() const;
+    // Port number to connect to the keymaster server on the host
+    int keymaster_vsock_port() const;
     std::string adb_ip_and_port() const;
     std::string adb_device_name() const;
     std::string device_title() const;
@@ -372,6 +399,15 @@
     std::string launcher_log_path() const;
 
     std::string launcher_monitor_socket_path() const;
+
+    std::string sdcard_path() const;
+
+    // The device id the webrtc process should use to register with the
+    // signaling server
+    std::string webrtc_device_id() const;
+
+    // Whether this instance should start the webrtc signaling server
+    bool start_webrtc_sig_server() const;
   };
 
   // A view into an existing CuttlefishConfig object for a particular instance.
@@ -393,6 +429,7 @@
     void set_frames_server_port(int config_server_port);
     void set_touch_server_port(int config_server_port);
     void set_keyboard_server_port(int config_server_port);
+    void set_keymaster_vsock_port(int keymaster_vsock_port);
     void set_host_port(int host_port);
     void set_tpm_port(int tpm_port);
     void set_adb_ip_and_port(const std::string& ip_port);
@@ -404,6 +441,8 @@
     void set_uuid(const std::string& uuid);
     void set_instance_dir(const std::string& instance_dir);
     void set_virtual_disk_paths(const std::vector<std::string>& disk_paths);
+    void set_webrtc_device_id(const std::string& id);
+    void set_start_webrtc_signaling_server(bool start);
   };
 
  private:
@@ -430,7 +469,6 @@
 std::string ForCurrentInstance(const char* prefix);
 int ForCurrentInstance(int base);
 
-std::string GetDefaultPerInstanceDir();
 std::string GetDefaultMempath();
 int GetDefaultPerInstanceVsockCid();
 
diff --git a/host/libs/config/kernel_args.cpp b/host/libs/config/kernel_args.cpp
index 531fc99..b6590ba 100644
--- a/host/libs/config/kernel_args.cpp
+++ b/host/libs/config/kernel_args.cpp
@@ -87,7 +87,7 @@
     kernel_cmdline.push_back(concat("androidboot.cuttlefish_config_server_port=", instance.config_server_port()));
   }
 
-  if (instance.tpm_port()) {
+  if (config.tpm_binary() != "" && instance.tpm_port()) {
     kernel_cmdline.push_back(concat("androidboot.tpm_vsock_port=", instance.tpm_port()));
   }
 
@@ -103,6 +103,9 @@
     kernel_cmdline.push_back(concat("androidboot.vsock_frames_port=", instance.frames_server_port()));
   }
 
+  kernel_cmdline.push_back(concat("androidboot.vsock_keymaster_port=",
+                                  instance.keymaster_vsock_port()));
+
   AppendVector(&kernel_cmdline, config.extra_kernel_cmdline());
 
   return kernel_cmdline;
diff --git a/host/libs/vm_manager/crosvm_manager.cpp b/host/libs/vm_manager/crosvm_manager.cpp
index fe49981..206789f 100644
--- a/host/libs/vm_manager/crosvm_manager.cpp
+++ b/host/libs/vm_manager/crosvm_manager.cpp
@@ -169,7 +169,7 @@
   if (!config_->final_ramdisk_path().empty()) {
     crosvm_cmd.AddParameter("--initrd=", config_->final_ramdisk_path());
   }
-  crosvm_cmd.AddParameter("--null-audio");
+  // crosvm_cmd.AddParameter("--null-audio");
   crosvm_cmd.AddParameter("--mem=", config_->memory_mb());
   crosvm_cmd.AddParameter("--cpus=", config_->cpus());
   crosvm_cmd.AddParameter("--params=", kernel_cmdline_);
diff --git a/host_package.mk b/host_package.mk
index 18176c5..5f266e8 100644
--- a/host_package.mk
+++ b/host_package.mk
@@ -45,9 +45,6 @@
     x86_64-linux-gnu/libandroid-emu-shared.so \
     x86_64-linux-gnu/libemugl_common.so \
     x86_64-linux-gnu/libOpenglRender.so \
-    x86_64-linux-gnu/libEGL_translator.so \
-    x86_64-linux-gnu/libGLES_CM_translator.so \
-    x86_64-linux-gnu/libGLES_V2_translator.so \
     x86_64-linux-gnu/libgfxstream_backend.so \
     logcat_receiver \
     config_server \
@@ -57,6 +54,7 @@
     run_cvd \
     cvd_status \
     webRTC \
+    webrtc_sig_server \
     metrics \
     fsck.f2fs \
     resize.f2fs \
@@ -64,7 +62,11 @@
     tpm_simulator_manager \
     vtpm_passthrough \
     ms-tpm-20-ref \
-    lz4
+    lz4 \
+    mkenvimage \
+    tapsetiff \
+    newfs_msdos \
+    secure_env \
 
 cvd_host_tests := \
     monotonic_time_test \
@@ -97,6 +99,10 @@
     libopus.so \
     libyuv.so \
     libjpeg.so \
+    libkeymaster_messages.so \
+    libkeymaster_portable.so \
+    libsoft_attestation_cert.so \
+    libcuttlefish_security.so \
 
 webrtc_assets := \
     index.html \
diff --git a/recovery/Android.bp b/recovery/Android.bp
new file mode 100644
index 0000000..5e495d7
--- /dev/null
+++ b/recovery/Android.bp
@@ -0,0 +1,34 @@
+//
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_library_static {
+    name: "librecovery_ui_cuttlefish",
+    owner: "google",
+    cflags: [
+        "-Wall",
+        "-Wextra",
+        "-Werror",
+        "-pedantic",
+    ],
+    srcs: [
+        "recovery_ui.cpp",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "librecovery_ui",
+    ],
+}
diff --git a/recovery/recovery_ui.cpp b/recovery/recovery_ui.cpp
new file mode 100644
index 0000000..479a85f
--- /dev/null
+++ b/recovery/recovery_ui.cpp
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <recovery_ui/device.h>
+#include <recovery_ui/screen_ui.h>
+
+class CuttlefishRecoveryUI : public ScreenRecoveryUI {
+  public:
+    CuttlefishRecoveryUI() : ScreenRecoveryUI() {}
+
+    bool IsUsbConnected() override {
+      return true;
+    }
+};
+
+Device* make_device() {
+    return new Device(new CuttlefishRecoveryUI);
+}
diff --git a/required_images b/required_images
new file mode 100644
index 0000000..d07652e
--- /dev/null
+++ b/required_images
@@ -0,0 +1,7 @@
+boot.img
+cache.img
+super.img
+userdata.img
+vbmeta.img
+vbmeta_system.img
+vendor_boot.img
diff --git a/shared/BoardConfig.mk b/shared/BoardConfig.mk
index 542ada7..1d79fd9 100644
--- a/shared/BoardConfig.mk
+++ b/shared/BoardConfig.mk
@@ -20,13 +20,6 @@
 
 include build/make/target/board/BoardConfigMainlineCommon.mk
 
-# Reset CF unsupported settings
-TARGET_NO_RECOVERY := false
-BOARD_USES_SYSTEM_OTHER_ODEX :=
-WITH_DEXPREOPT := true
-BOARD_AVB_ENABLE := false
-
-
 TARGET_BOOTLOADER_BOARD_NAME := cutf
 
 # Boot partition size: 32M
@@ -34,6 +27,7 @@
 # will not change (as is it not a filesystem.)
 BOARD_BOOTIMAGE_PARTITION_SIZE := 67108864
 BOARD_RECOVERYIMAGE_PARTITION_SIZE := 67108864
+BOARD_VENDOR_BOOTIMAGE_PARTITION_SIZE := 67108864
 
 # Build a separate vendor.img partition
 BOARD_USES_VENDORIMAGE := true
@@ -55,6 +49,16 @@
 BOARD_ODMIMAGE_FILE_SYSTEM_TYPE := ext4
 TARGET_COPY_OUT_ODM := odm
 
+# FIXME: Remove this once we generate the vbmeta digest correctly
+BOARD_AVB_MAKE_VBMETA_IMAGE_ARGS += --flag 2
+
+# Enable chained vbmeta for system image mixing
+BOARD_AVB_VBMETA_SYSTEM := product system system_ext
+BOARD_AVB_VBMETA_SYSTEM_KEY_PATH := external/avb/test/data/testkey_rsa2048.pem
+BOARD_AVB_VBMETA_SYSTEM_ALGORITHM := SHA256_RSA2048
+BOARD_AVB_VBMETA_SYSTEM_ROLLBACK_INDEX := $(PLATFORM_SECURITY_PATCH_TIMESTAMP)
+BOARD_AVB_VBMETA_SYSTEM_ROLLBACK_INDEX_LOCATION := 1
+
 BOARD_USES_GENERIC_AUDIO := false
 USE_CAMERA_STUB := true
 TARGET_USERIMAGES_SPARSE_EXT_DISABLED := true
@@ -75,9 +79,6 @@
 BOARD_CACHEIMAGE_PARTITION_SIZE := 67108864
 BOARD_CACHEIMAGE_FILE_SYSTEM_TYPE := ext4
 
-# Use ext4 block sharing on read-only partitions
-BOARD_EXT4_SHARE_DUP_BLOCKS := true
-
 BOARD_GPU_DRIVERS := virgl
 
 # Enable goldfish's encoder.
@@ -107,9 +108,15 @@
 WIFI_DRIVER_FW_PATH_STA     := "/dev/null"
 WIFI_DRIVER_FW_PATH_AP      := "/dev/null"
 
-BOARD_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/vendor
-BOARD_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/vendor/google
-PRODUCT_PRIVATE_SEPOLICY_DIRS := device/google/cuttlefish/shared/sepolicy/private
+# vendor sepolicy
+BOARD_VENDOR_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/vendor
+BOARD_VENDOR_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/vendor/google
+# product sepolicy, allow other layers to append
+PRODUCT_PRIVATE_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/product/private
+# PRODUCT_PUBLIC_SEPOLICY_DIRS += device/google/cuttlefish/shared/sepolicy/product/public
+# system_ext sepolicy
+# BOARD_PLAT_PRIVATE_SEPOLICY_DIR += device/google/cuttlefish/shared/sepolicy/system_ext/private
+# BOARD_PLAT_PUBLIC_SEPOLICY_DIR := device/google/cuttlefish/shared/sepolicy/system_ext/public
 
 VSOC_STLPORT_INCLUDES :=
 VSOC_STLPORT_LIBS :=
@@ -144,7 +151,7 @@
 
 
 TARGET_RECOVERY_PIXEL_FORMAT := ABGR_8888
-
+TARGET_RECOVERY_UI_LIB := librecovery_ui_cuttlefish
 TARGET_RECOVERY_FSTAB ?= device/google/cuttlefish/shared/config/fstab
 
 BOARD_SUPER_PARTITION_SIZE := 6442450944
@@ -169,5 +176,8 @@
 BOARD_BOOT_HEADER_VERSION := 3
 BOARD_USES_RECOVERY_AS_BOOT := true
 BOARD_MKBOOTIMG_ARGS += --header_version $(BOARD_BOOT_HEADER_VERSION)
-PRODUCT_COPY_FILES += device/google/cuttlefish/dtb.img:dtb.img
+PRODUCT_COPY_FILES += \
+    device/google/cuttlefish/dtb.img:dtb.img \
+    device/google/cuttlefish/required_images:required_images \
+
 BOARD_BUILD_SYSTEM_ROOT_IMAGE := false
diff --git a/shared/auto/device.mk b/shared/auto/device.mk
index e0693e5..17991aa 100644
--- a/shared/auto/device.mk
+++ b/shared/auto/device.mk
@@ -22,6 +22,9 @@
 
 $(call inherit-product, device/google/cuttlefish/shared/device.mk)
 
+# Extend cuttlefish common sepolicy with auto-specific functionality
+BOARD_SEPOLICY_DIRS += device/google/cuttlefish/shared/auto/sepolicy/vendor
+
 ################################################
 # Begin general Android Auto Embedded configurations
 
@@ -29,31 +32,19 @@
     packages/services/Car/car_product/init/init.bootstat.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw//init.bootstat.rc \
     packages/services/Car/car_product/init/init.car.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw//init.car.rc
 
-# Auto core hardware permissions
 PRODUCT_COPY_FILES += \
+    frameworks/native/data/etc/android.hardware.broadcastradio.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.broadcastradio.xml \
+    frameworks/native/data/etc/android.hardware.screen.landscape.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.screen.landscape.xml \
+    frameworks/native/data/etc/android.hardware.sensor.accelerometer.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.accelerometer.xml \
+    frameworks/native/data/etc/android.hardware.sensor.compass.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.compass.xml \
+    frameworks/native/data/etc/android.software.activities_on_secondary_displays.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.activities_on_secondary_displays.xml \
     frameworks/native/data/etc/car_core_hardware.xml:system/etc/permissions/car_core_hardware.xml \
-    frameworks/native/data/etc/android.hardware.type.automotive.xml:system/etc/permissions/android.hardware.type.automotive.xml \
-
-# Enable landscape
-PRODUCT_COPY_FILES += \
-    frameworks/native/data/etc/android.hardware.screen.landscape.xml:system/etc/permissions/android.hardware.screen.landscape.xml
-
-# Used to embed a map in an activity view
-PRODUCT_COPY_FILES += \
-    frameworks/native/data/etc/android.software.activities_on_secondary_displays.xml:system/etc/permissions/android.software.activities_on_secondary_displays.xml
-
-# Location permissions
-PRODUCT_COPY_FILES += \
-    frameworks/native/data/etc/android.hardware.location.gps.xml:system/etc/permissions/android.hardware.location.gps.xml
-
-# Broadcast Radio permissions
-PRODUCT_COPY_FILES += \
-    frameworks/native/data/etc/android.hardware.broadcastradio.xml:system/etc/permissions/android.hardware.broadcastradio.xml
 
 PRODUCT_PROPERTY_OVERRIDES += \
     keyguard.no_require_sim=true \
     ro.cdma.home.operator.alpha=Android \
     ro.cdma.home.operator.numeric=302780 \
+    ro.com.android.dataroaming=true \
     vendor.rild.libpath=libcuttlefish-ril.so \
 
 # vehicle HAL
@@ -65,17 +56,6 @@
 # Broadcast Radio
 PRODUCT_PACKAGES += android.hardware.broadcastradio@2.0-service
 
-# DRM HAL
-PRODUCT_PACKAGES += android.hardware.drm@1.2-service.clearkey
-
-# GPS HAL
-PRODUCT_PACKAGES += \
-    android.hardware.gnss@2.0-service
-
-# DRM Properities
-PRODUCT_PROPERTY_OVERRIDES += \
-    drm.service.enabled=true
-
 BOARD_IS_AUTOMOTIVE := true
 
 $(call inherit-product, $(SRC_TARGET_DIR)/product/aosp_base.mk)
diff --git a/shared/auto/sepolicy/vendor/file_contexts b/shared/auto/sepolicy/vendor/file_contexts
new file mode 100644
index 0000000..e5fcacd
--- /dev/null
+++ b/shared/auto/sepolicy/vendor/file_contexts
@@ -0,0 +1 @@
+/vendor/lib(64)?/cuttlefish_auto_resources.so  u:object_r:same_process_hal_file:s0
diff --git a/shared/sepolicy/vendor/hal_vehicle_default.te b/shared/auto/sepolicy/vendor/hal_vehicle_default.te
similarity index 100%
rename from shared/sepolicy/vendor/hal_vehicle_default.te
rename to shared/auto/sepolicy/vendor/hal_vehicle_default.te
diff --git a/shared/config/fstab b/shared/config/fstab
index 715c68b..a9a6718 100644
--- a/shared/config/fstab
+++ b/shared/config/fstab
@@ -1,14 +1,16 @@
-boot /boot emmc defaults recoveryonly
-system /system ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
+/dev/block/by-name/boot /boot emmc defaults recoveryonly,slotselect
+/dev/block/by-name/vendor_boot /vendor_boot emmc defaults recoveryonly,slotselect
+system /system ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb=vbmeta_system
 # Add all non-dynamic partitions except system, after this comment
 /dev/block/by-name/userdata /data f2fs nodev,noatime,nosuid,inlinecrypt,reserve_root=32768 latemount,wait,fileencryption=aes-256-xts:aes-256-cts:v2+inlinecrypt_optimized,fsverity,keydirectory=/metadata/vold/metadata_encryption
 /dev/block/by-name/cache /cache ext4 nodev,noatime,nosuid,errors=panic wait
 /dev/block/by-name/metadata /metadata ext4 nodev,noatime,nosuid,errors=panic wait,formattable,first_stage_mount
 /dev/block/by-name/misc /misc emmc defaults defaults
 # Add all dynamic partitions except system, after this comment
-odm /odm ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
-product /product ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
-system_ext /system_ext ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
-vendor /vendor ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
+odm /odm ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb
+product /product ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb
+system_ext /system_ext ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb=vbmeta_system
+vendor /vendor ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb=vbmeta
 /dev/block/zram0 none swap defaults zramsize=75%
 /tmp /sdcard none defaults,bind recoveryonly
+/devices/*/block/vdb auto auto defaults voldmanaged=sdcard1:auto,encryptable=userdata
diff --git a/shared/config/fstab.ext4 b/shared/config/fstab.ext4
index 3e3672f..f2ffbfa 100644
--- a/shared/config/fstab.ext4
+++ b/shared/config/fstab.ext4
@@ -1,14 +1,16 @@
-boot /boot emmc defaults recoveryonly
-system /system ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
+boot /boot emmc defaults recoveryonly,slotselect
+vendor_boot /vendor_boot emmc defaults recoveryonly,slotselect
+system /system ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb=vbmeta_system
 # Add all non-dynamic partitions except system, after this comment
 /dev/block/by-name/userdata /data ext4 nodev,noatime,nosuid,errors=panic wait,fileencryption=aes-256-xts:aes-256-cts,fsverity
 /dev/block/by-name/cache /cache ext4 nodev,noatime,nosuid,errors=panic wait
 /dev/block/by-name/metadata /metadata ext4 nodev,noatime,nosuid,errors=panic wait,formattable,first_stage_mount
 /dev/block/by-name/misc /misc emmc defaults defaults
 # Add all dynamic partitions except system, after this comment
-odm /odm ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
-product /product ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
-system_ext /system_ext ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
-vendor /vendor ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect
+odm /odm ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb
+product /product ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb
+system_ext /system_ext ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb=vbmeta_system
+vendor /vendor ext4 noatime,ro,errors=panic wait,logical,first_stage_mount,slotselect,avb=vbmeta
 /dev/block/zram0 none swap defaults zramsize=75%
 /tmp /sdcard none defaults,bind recoveryonly
+/devices/*/block/vdb auto auto defaults voldmanaged=sdcard1:auto,encryptable=userdata
diff --git a/shared/config/init.vendor.rc b/shared/config/init.vendor.rc
index 885d76b..bd555c8 100644
--- a/shared/config/init.vendor.rc
+++ b/shared/config/init.vendor.rc
@@ -1,15 +1,17 @@
 on early-init
 #    loglevel 8
-    symlink /sdcard /storage/sdcard0
     mkdir /var/run 0755 root root
     mkdir /var/run/media 0755 media root
     mkdir /var/run/system 0755 system root
     mkdir /dev/gce 0750
     chown system system /dev/gce
 
-    mount tracefs tracefs /sys/kernel/tracing
     mount securityfs securityfs /sys/kernel/security
 
+    # For KCOV
+    mount debugfs debugfs /sys/kernel/debug
+    chmod 0755 /sys/kernel/debug
+
     setprop ro.sf.lcd_density ${ro.boot.lcd_density}
     setprop ro.hardware.egl ${ro.boot.hardware.egl}
     setprop ro.hardware.gralloc ${ro.boot.hardware.gralloc}
diff --git a/shared/config/manifest.xml b/shared/config/manifest.xml
index 2216680..dff0d66 100644
--- a/shared/config/manifest.xml
+++ b/shared/config/manifest.xml
@@ -17,7 +17,6 @@
 */
 -->
 <manifest version="1.0" type="device" target-level="6">
-    <kernel target-level="6" />
     <hal format="hidl">
         <name>android.hardware.audio</name>
         <transport>hwbinder</transport>
diff --git a/shared/device.mk b/shared/device.mk
index c0023c6..236482a 100644
--- a/shared/device.mk
+++ b/shared/device.mk
@@ -20,8 +20,7 @@
 # Enable updating of APEXes
 $(call inherit-product, $(SRC_TARGET_DIR)/product/updatable_apex.mk)
 
-# Enable userspace reboot
-$(call inherit-product, $(SRC_TARGET_DIR)/product/userspace_reboot.mk)
+PRODUCT_SOONG_NAMESPACES += device/generic/goldfish-opengl # for vulkan
 
 PRODUCT_SHIPPING_API_LEVEL := 31
 PRODUCT_BUILD_BOOT_IMAGE := true
@@ -36,6 +35,8 @@
     product \
     system \
     system_ext \
+    vbmeta \
+    vbmeta_system \
     vendor
 
 # Enable Virtual A/B
@@ -68,19 +69,28 @@
     ro.opengles.version=196608 \
     wifi.interface=wlan0 \
     persist.sys.zram_enabled=1 \
-    ro.apk_verity.mode=2 \
     ro.rebootescrow.device=/dev/block/pmem0 \
 
 # Below is a list of properties we probably should get rid of.
 PRODUCT_PROPERTY_OVERRIDES += \
     wlan.driver.status=ok
 
+# Codec 2.0 is unstable on x86
+PRODUCT_PROPERTY_OVERRIDES += \
+    debug.stagefright.ccodec=0
+
 # Enforce privapp-permissions whitelist.
 PRODUCT_PROPERTY_OVERRIDES += ro.control_privapp_permissions=enforce
 
 # aes-256-heh default is not supported in standard kernels.
 PRODUCT_PROPERTY_OVERRIDES += ro.crypto.volume.filenames_mode=aes-256-cts
 
+# Copy preopted files from system_b on first boot
+PRODUCT_PROPERTY_OVERRIDES += ro.cp_system_other_odex=1
+
+# DRM service opt-in
+PRODUCT_PROPERTY_OVERRIDES += drm.service.enabled=true
+
 # Packages for various GCE-specific utilities
 #
 PRODUCT_PACKAGES += \
@@ -167,6 +177,7 @@
     device/google/cuttlefish/shared/config/media_codecs_google_video.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_codecs_google_video.xml \
     device/google/cuttlefish/shared/config/media_codecs_performance.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_codecs_performance.xml \
     device/google/cuttlefish/shared/config/media_profiles.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_profiles_V1_0.xml \
+    device/google/cuttlefish/shared/permissions/cuttlefish_excluded_hardware.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/cuttlefish_excluded_hardware.xml \
     device/google/cuttlefish/shared/permissions/privapp-permissions-cuttlefish.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/privapp-permissions-cuttlefish.xml \
     frameworks/av/media/libeffects/data/audio_effects.xml:$(TARGET_COPY_OUT_VENDOR)/etc/audio_effects.xml \
     frameworks/av/media/libstagefright/data/media_codecs_google_audio.xml:$(TARGET_COPY_OUT_VENDOR)/etc/media_codecs_google_audio.xml \
@@ -179,25 +190,25 @@
     frameworks/av/services/audiopolicy/config/surround_sound_configuration_5_0.xml:$(TARGET_COPY_OUT_VENDOR)/etc/surround_sound_configuration_5_0.xml \
     frameworks/native/data/etc/android.hardware.audio.low_latency.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.audio.low_latency.xml \
     frameworks/native/data/etc/android.hardware.bluetooth_le.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.bluetooth_le.xml \
-    frameworks/native/data/etc/android.hardware.bluetooth.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.bluetooth.xml \
-    frameworks/native/data/etc/android.hardware.camera.flash-autofocus.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.camera.xml \
-    frameworks/native/data/etc/android.hardware.camera.full.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.camera.full.xml \
+    frameworks/native/data/etc/android.hardware.camera.flash-autofocus.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.camera.flash-autofocus.xml \
     frameworks/native/data/etc/android.hardware.camera.front.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.camera.front.xml \
+    frameworks/native/data/etc/android.hardware.camera.full.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.camera.full.xml \
     frameworks/native/data/etc/android.hardware.camera.raw.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.camera.raw.xml \
-    frameworks/native/data/etc/android.hardware.ethernet.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.ethernet.xml \
+    frameworks/native/data/etc/android.hardware.faketouch.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.faketouch.xml \
     frameworks/native/data/etc/android.hardware.location.gps.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.location.gps.xml \
-    frameworks/native/data/etc/android.hardware.sensor.accelerometer.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.accelerometer.xml \
+    frameworks/native/data/etc/android.hardware.sensor.ambient_temperature.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.ambient_temperature.xml \
     frameworks/native/data/etc/android.hardware.sensor.barometer.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.barometer.xml \
-    frameworks/native/data/etc/android.hardware.sensor.compass.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.compass.xml \
     frameworks/native/data/etc/android.hardware.sensor.gyroscope.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.gyroscope.xml \
     frameworks/native/data/etc/android.hardware.sensor.light.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.light.xml \
     frameworks/native/data/etc/android.hardware.sensor.proximity.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.proximity.xml \
-    frameworks/native/data/etc/android.hardware.touchscreen.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.touchscreen.xml \
+    frameworks/native/data/etc/android.hardware.sensor.relative_humidity.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.relative_humidity.xml \
     frameworks/native/data/etc/android.hardware.usb.accessory.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.usb.accessory.xml \
     frameworks/native/data/etc/android.hardware.vulkan.level-0.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.vulkan.level.xml \
     frameworks/native/data/etc/android.hardware.vulkan.version-1_0_3.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.vulkan.version.xml \
     frameworks/native/data/etc/android.hardware.wifi.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.wifi.xml \
-    frameworks/native/data/etc/android.software.app_widgets.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.app_widgets.xml \
+    frameworks/native/data/etc/android.software.ipsec_tunnels.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.ipsec_tunnels.xml \
+    frameworks/native/data/etc/android.software.sip.voip.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.sip.voip.xml \
+    frameworks/native/data/etc/android.software.verified_boot.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.verified_boot.xml \
     frameworks/native/data/etc/android.software.vulkan.deqp.level-2020-03-01.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.vulkan.deqp.level.xml \
     system/bt/vendor_libs/test_vendor_lib/data/controller_properties.json:vendor/etc/bluetooth/controller_properties.json \
     device/google/cuttlefish/shared/config/task_profiles.json:$(TARGET_COPY_OUT_VENDOR)/etc/task_profiles.json \
diff --git a/shared/permissions/cuttlefish_excluded_hardware.xml b/shared/permissions/cuttlefish_excluded_hardware.xml
new file mode 100644
index 0000000..3660289
--- /dev/null
+++ b/shared/permissions/cuttlefish_excluded_hardware.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2020 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<permissions>
+    <unavailable-feature name="android.hardware.microphone" />
+    <unavailable-feature name="android.software.print" />
+    <unavailable-feature name="android.software.voice_recognizers" />
+</permissions>
diff --git a/shared/phone/device.mk b/shared/phone/device.mk
index 4e183f7..6f377eb 100644
--- a/shared/phone/device.mk
+++ b/shared/phone/device.mk
@@ -22,8 +22,6 @@
 $(call inherit-product, frameworks/native/build/phone-xhdpi-2048-dalvik-heap.mk)
 $(call inherit-product, device/google/cuttlefish/shared/device.mk)
 
-PRODUCT_CHARACTERISTICS := nosdcard
-
 PRODUCT_PROPERTY_OVERRIDES += \
     keyguard.no_require_sim=true \
     ro.cdma.home.operator.alpha=Android \
diff --git a/shared/phone/device_vendor.mk b/shared/phone/device_vendor.mk
index 7785e7d..1eb9ee8 100644
--- a/shared/phone/device_vendor.mk
+++ b/shared/phone/device_vendor.mk
@@ -27,8 +27,6 @@
 $(call inherit-product, frameworks/native/build/phone-xhdpi-2048-dalvik-heap.mk)
 $(call inherit-product, device/google/cuttlefish/shared/device.mk)
 
-PRODUCT_CHARACTERISTICS := nosdcard
-
 PRODUCT_PROPERTY_OVERRIDES += \
     keyguard.no_require_sim=true \
     ro.cdma.home.operator.alpha=Android \
diff --git a/shared/sepolicy/private/file_contexts b/shared/sepolicy/product/private/file_contexts
similarity index 100%
rename from shared/sepolicy/private/file_contexts
rename to shared/sepolicy/product/private/file_contexts
diff --git a/shared/sepolicy/private/property_contexts b/shared/sepolicy/product/private/property_contexts
similarity index 100%
rename from shared/sepolicy/private/property_contexts
rename to shared/sepolicy/product/private/property_contexts
diff --git a/shared/sepolicy/private/suspend_blocker.te b/shared/sepolicy/product/private/suspend_blocker.te
similarity index 62%
rename from shared/sepolicy/private/suspend_blocker.te
rename to shared/sepolicy/product/private/suspend_blocker.te
index 41c72b9..fd2fcdc 100644
--- a/shared/sepolicy/private/suspend_blocker.te
+++ b/shared/sepolicy/product/private/suspend_blocker.te
@@ -1,5 +1,5 @@
 type suspend_blocker, domain, coredomain;
-type suspend_blocker_exec, exec_type, file_type;
+type suspend_blocker_exec, exec_type, system_file_type, file_type;
 
 init_daemon_domain(suspend_blocker);
 
diff --git a/shared/sepolicy/private/tombstone_transmit.te b/shared/sepolicy/product/private/tombstone_transmit.te
similarity index 84%
rename from shared/sepolicy/private/tombstone_transmit.te
rename to shared/sepolicy/product/private/tombstone_transmit.te
index a17ed10..c182270 100644
--- a/shared/sepolicy/private/tombstone_transmit.te
+++ b/shared/sepolicy/product/private/tombstone_transmit.te
@@ -1,5 +1,5 @@
 type tombstone_transmit, domain, coredomain;
-type tombstone_transmit_exec, exec_type, file_type;
+type tombstone_transmit_exec, exec_type, system_file_type, file_type;
 
 init_daemon_domain(tombstone_transmit);
 
diff --git a/shared/sepolicy/vendor/bug_map b/shared/sepolicy/vendor/bug_map
index 81351af..e8f546e 100644
--- a/shared/sepolicy/vendor/bug_map
+++ b/shared/sepolicy/vendor/bug_map
@@ -1,11 +1,7 @@
-gsid gsid capability b/146356992
 init system_lib_file dir b/133444385
 init system_lib_file file b/133444385
 logpersist logpersist capability b/132911257
 logpersist device file b/143108875
 migrate_legacy_obb_data dalvikcache_data_file file b/152338071
-platform_app radio_prop property_service b/140284352
-priv_app proc_net file b/124422390
 shell adbd vsock_socket b/131904985
 system_server system_server process b/65201432
-zygote ramdump_app process b/139558100
diff --git a/shared/sepolicy/vendor/device.te b/shared/sepolicy/vendor/device.te
index 38f0a2f..e63eddf 100644
--- a/shared/sepolicy/vendor/device.te
+++ b/shared/sepolicy/vendor/device.te
@@ -1,7 +1,3 @@
 # Device types
-type input_events_device, dev_type;
-type libcuttlefish_rild_device, dev_type;
-type region_e2e_test_device, dev_type;
-type region_screen_device, dev_type;
-type socket_forward_device, dev_type;
+type ab_block_device, dev_type;
 type virtual_serial_device, dev_type;
diff --git a/shared/sepolicy/vendor/dumpstate.te b/shared/sepolicy/vendor/dumpstate.te
new file mode 100644
index 0000000..34caf72
--- /dev/null
+++ b/shared/sepolicy/vendor/dumpstate.te
@@ -0,0 +1 @@
+allow dumpstate hal_neuralnetworks_sample:process signal;
diff --git a/shared/sepolicy/vendor/file_contexts b/shared/sepolicy/vendor/file_contexts
index 7041961..2f6afe9 100644
--- a/shared/sepolicy/vendor/file_contexts
+++ b/shared/sepolicy/vendor/file_contexts
@@ -3,40 +3,41 @@
 #
 
 # crosvm (x86) block devices
-/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/boot u:object_r:boot_block_device:s0
-/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/metadata u:object_r:metadata_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:01\.0/by-name/misc u:object_r:misc_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/boot_[ab] u:object_r:boot_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/vendor_boot_[ab] u:object_r:boot_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/verity_[ab] u:object_r:ab_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/verity_system_[ab] u:object_r:ab_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:01\.0/by-name/super u:object_r:super_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:01\.0/by-name/userdata u:object_r:userdata_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:01\.0/by-name/cache u:object_r:cache_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:01\.0/by-name/metadata u:object_r:metadata_block_device:s0
 # crosvm (arm64) block devices
-/dev/block/platform/10000.pci/by-name/boot u:object_r:boot_block_device:s0
-/dev/block/platform/10000.pci/by-name/metadata u:object_r:metadata_block_device:s0
 /dev/block/platform/10000.pci/by-name/misc u:object_r:misc_block_device:s0
+/dev/block/platform/10000.pci/by-name/boot_[ab] u:object_r:boot_block_device:s0
+/dev/block/platform/10000.pci/by-name/vendor_boot_[ab] u:object_r:boot_block_device:s0
+/dev/block/platform/10000.pci/by-name/verity_[ab] u:object_r:ab_block_device:s0
+/dev/block/platform/10000.pci/by-name/verity_system_[ab] u:object_r:ab_block_device:s0
 /dev/block/platform/10000.pci/by-name/super u:object_r:super_block_device:s0
 /dev/block/platform/10000.pci/by-name/userdata u:object_r:userdata_block_device:s0
 /dev/block/platform/10000.pci/by-name/cache u:object_r:cache_block_device:s0
+/dev/block/platform/10000.pci/by-name/metadata u:object_r:metadata_block_device:s0
 # qemu block devices
-/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/boot u:object_r:boot_block_device:s0
-/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/metadata u:object_r:metadata_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:03\.0/by-name/misc u:object_r:misc_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/boot_[ab] u:object_r:boot_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/vendor_boot_[ab] u:object_r:boot_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/verity_[ab] u:object_r:ab_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/verity_system_[ab] u:object_r:ab_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:03\.0/by-name/super u:object_r:super_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:03\.0/by-name/userdata u:object_r:userdata_block_device:s0
 /dev/block/pci/pci0000:00/0000:00:03\.0/by-name/cache u:object_r:cache_block_device:s0
+/dev/block/pci/pci0000:00/0000:00:03\.0/by-name/metadata u:object_r:metadata_block_device:s0
 
 /dev/block/pmem0  u:object_r:rebootescrow_device:s0
 /dev/block/zram0  u:object_r:swap_block_device:s0
 /dev/dri u:object_r:gpu_device:s0
 /dev/dri/card0  u:object_r:graphics_device:s0
 /dev/dri/renderD128  u:object_r:gpu_device:s0
-/dev/e2e_managed  u:object_r:region_e2e_test_device:s0
-/dev/e2e_manager  u:object_r:region_e2e_test_device:s0
-/dev/e2e_primary  u:object_r:region_e2e_test_device:s0
-/dev/e2e_secondary  u:object_r:region_e2e_test_device:s0
-/dev/input_events  u:object_r:input_events_device:s0
-/dev/ril  u:object_r:libcuttlefish_rild_device:s0
-/dev/screen  u:object_r:region_screen_device:s0
-/dev/socket_forward  u:object_r:socket_forward_device:s0
 /dev/vport[0-9]p[0-9]*  u:object_r:virtual_serial_device:s0
 /dev/vtpmx  u:object_r:vtpm_creation_device:s0
 /dev/tpmrm0  u:object_r:tpm_resource_manager:s0
@@ -75,7 +76,6 @@
 /vendor/bin/hw/android\.hardware\.lights-service\.example u:object_r:hal_light_default_exec:s0
 /vendor/bin/hw/android\.hardware\.neuralnetworks@1\.3-service-sample-.*   u:object_r:hal_neuralnetworks_sample_exec:s0
 /vendor/bin/hw/android\.hardware\.vibrator@1\.x-service\.example u:object_r:hal_vibrator_default_exec:s0
-/vendor/bin/hw/android\.hardware\.tv\.cec@1\.0-service\.mock u:object_r:hal_tv_cec_mock_exec:s0
 /vendor/bin/ip_link_add  u:object_r:ip_link_add_exec:s0
 /vendor/bin/setup_wifi  u:object_r:setup_wifi_exec:s0
 /vendor/bin/hw/android\.hardware\.sensors@2\.0-service\.mock  u:object_r:hal_sensors_default_exec:s0
@@ -84,7 +84,6 @@
 /vendor/bin/hw/android\.hardware\.authsecret@1\.0-service  u:object_r:hal_authsecret_default_exec:s0
 /vendor/bin/init\.insmod\.sh  u:object_r:init_insmod_sh_exec:s0
 
-/vendor/lib(64)?/cuttlefish_auto_resources.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/libdrm.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/libglapi.so  u:object_r:same_process_hal_file:s0
 /vendor/lib(64)?/dri/.* u:object_r:same_process_hal_file:s0
diff --git a/shared/sepolicy/vendor/genfs_contexts b/shared/sepolicy/vendor/genfs_contexts
index 5ac5257..ba60da8 100644
--- a/shared/sepolicy/vendor/genfs_contexts
+++ b/shared/sepolicy/vendor/genfs_contexts
@@ -1,35 +1,52 @@
 # crosvm (x86)
-genfscon sysfs /devices/pci0000:00/0000:00:07.0/virtio6/net u:object_r:sysfs_net:s0 # buried_eth0 & wlan0
-genfscon sysfs /devices/pci0000:00/0000:00:08.0/virtio7/net u:object_r:sysfs_net:s0 # rmnet0
-genfscon sysfs /devices/pci0000:00/0000:00:0a.0/device u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/pci0000:00/0000:00:0a.0/subsystem_device u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/pci0000:00/0000:00:0a.0/subsystem_vendor u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/pci0000:00/0000:00:0a.0/uevent u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/pci0000:00/0000:00:0a.0/vendor u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/pnp0/00:00/rtc  u:object_r:sysfs_rtc:s0 # also used by qemu
+genfscon sysfs /devices/pci0000:00/0000:00:08.0/virtio7/net u:object_r:sysfs_net:s0 # buried_eth0 & wlan0
+genfscon sysfs /devices/pci0000:00/0000:00:09.0/virtio8/net u:object_r:sysfs_net:s0 # rmnet0
+genfscon sysfs /devices/pci0000:00/0000:00:0b.0/device u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/pci0000:00/0000:00:0b.0/subsystem_device u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/pci0000:00/0000:00:0b.0/subsystem_vendor u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/pci0000:00/0000:00:0b.0/uevent u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/pci0000:00/0000:00:0b.0/vendor u:object_r:sysfs_gpu:s0
+## find /sys/devices/platform/* -type d -name 'rtc[0-9]' | sed 's,/rtc[0-9],,'
+## x86 rtc_cmos on crosvm does not currently expose rtcN/hctosys
+## find /sys/devices/platform/* -type d -name 'wakeup[0-9]'
+genfscon sysfs /devices/platform/rtc_cmos/wakeup/wakeup0 u:object_r:sysfs_wakeup:s0
+genfscon sysfs /devices/platform/rtc-test.1/wakeup/wakeup1 u:object_r:sysfs_wakeup:s0
+genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc1/wakeup2 u:object_r:sysfs_wakeup:s0 # <= 5.5
+genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc1/alarmtimer.0.auto/wakeup/wakeup2 u:object_r:sysfs_wakeup:s0 # >5.5
+genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup3 u:object_r:sysfs_wakeup:s0
+
 # crosvm (arm64)
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:07.0/virtio6/net u:object_r:sysfs_net:s0 # buried_eth0 & wlan0
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:08.0/virtio7/net u:object_r:sysfs_net:s0 # rmnet0
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0a.0/device u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0a.0/subsystem_device u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0a.0/subsystem_vendor u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0a.0/uevent u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0a.0/vendor u:object_r:sysfs_gpu:s0
-genfscon sysfs /devices/platform/2000.rtc/rtc  u:object_r:sysfs_rtc:s0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:08.0/virtio7/net u:object_r:sysfs_net:s0 # buried_eth0 & wlan0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:09.0/virtio8/net u:object_r:sysfs_net:s0 # rmnet0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0b.0/device u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0b.0/subsystem_device u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0b.0/subsystem_vendor u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0b.0/uevent u:object_r:sysfs_gpu:s0
+genfscon sysfs /devices/platform/10000.pci/pci0000:00/0000:00:0b.0/vendor u:object_r:sysfs_gpu:s0
+## find /sys/devices/platform/* -type d -name 'rtc[0-9]' | sed 's,/rtc[0-9],,'
+genfscon sysfs /devices/platform/2000.rtc/rtc u:object_r:sysfs_rtc:s0
+## find /sys/devices/platform/* -type d -name 'wakeup[0-9]'
+## arm64 2000.rtc on crosvm does not currently expose a wakeup node
+genfscon sysfs /devices/platform/rtc-test.1/wakeup/wakeup0 u:object_r:sysfs_wakeup:s0
+genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc2/wakeup1 u:object_r:sysfs_wakeup:s0 # <= 5.5
+genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc2/alarmtimer.0.auto/wakeup/wakeup1 u:object_r:sysfs_wakeup:s0 # >5.5
+genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup2 u:object_r:sysfs_wakeup:s0
+
 # qemu (x86)
-genfscon sysfs /devices/pci0000:00/0000:00:04.0/virtio2/net u:object_r:sysfs_net:s0 # buried_eth0 & wlan0
-genfscon sysfs /devices/pci0000:00/0000:00:05.0/virtio3/net u:object_r:sysfs_net:s0 # rmnet0
+genfscon sysfs /devices/pci0000:00/0000:00:05.0/virtio3/net u:object_r:sysfs_net:s0 # buried_eth0 & wlan0
+genfscon sysfs /devices/pci0000:00/0000:00:06.0/virtio4/net u:object_r:sysfs_net:s0 # rmnet0
+# FIXME: Add sysfs_gpu labels for qemu
+## find /sys/devices/platform/* -type d -name 'rtc[0-9]' | sed 's,/rtc[0-9],,'
+genfscon sysfs /devices/pnp0/00:00/rtc u:object_r:sysfs_rtc:s0
+## find /sys/devices/platform/* -type d -name 'wakeup[0-9][0-9]'
+genfscon sysfs /devices/pnp0/00:00/wakeup/wakeup13 u:object_r:sysfs_wakeup:s0
+genfscon sysfs /devices/pnp0/00:00/rtc/rtc0/wakeup14 u:object_r:sysfs_wakeup:s0 # <= 5.5
+genfscon sysfs /devices/pnp0/00:00/rtc/rtc0/alarmtimer.0.auto/wakeup/wakeup14 u:object_r:sysfs_wakeup:s0 # >5.5
+genfscon sysfs /devices/platform/rtc-test.1/wakeup/wakeup15 u:object_r:sysfs_wakeup:s0
+genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup16 u:object_r:sysfs_wakeup:s0
 
 # common on all platforms / vm managers
+genfscon sysfs /devices/platform/rtc-test.0/rtc u:object_r:sysfs_rtc:s0
+genfscon sysfs /devices/platform/rtc-test.1/rtc u:object_r:sysfs_rtc:s0
+genfscon sysfs /devices/platform/rtc-test.2/rtc u:object_r:sysfs_rtc:s0
 genfscon sysfs /bus/iio/devices u:object_r:sysfs_iio_devices:s0
-genfscon sysfs /devices/platform/rtc-test.0/rtc/rtc0/hctosys u:object_r:sysfs_rtc:s0
-genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc1/hctosys u:object_r:sysfs_rtc:s0
-genfscon sysfs /devices/platform/rtc-test.2/rtc/rtc2/hctosys u:object_r:sysfs_rtc:s0
-# TODO(b/148802006): Work around core policy sysfs_wakeup label not working
-# All kernels
-genfscon sysfs /devices/platform/rtc-test.1/wakeup/wakeup0  u:object_r:sysfs_wakeup:s0
-genfscon sysfs /devices/platform/rtc-test.2/wakeup/wakeup2 u:object_r:sysfs_wakeup:s0
-# Kernels <=5.5
-genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc1/wakeup1  u:object_r:sysfs_wakeup:s0
-# Kernels >5.5
-genfscon sysfs /devices/platform/rtc-test.1/rtc/rtc1/alarmtimer.0.auto/wakeup/wakeup1  u:object_r:sysfs_wakeup:s0
diff --git a/shared/sepolicy/vendor/google/bug_map b/shared/sepolicy/vendor/google/bug_map
new file mode 100644
index 0000000..26c5581
--- /dev/null
+++ b/shared/sepolicy/vendor/google/bug_map
@@ -0,0 +1 @@
+zygote ramdump_app process b/139558100
diff --git a/shared/sepolicy/vendor/hal_bootctl_default.te b/shared/sepolicy/vendor/hal_bootctl_default.te
new file mode 100644
index 0000000..e727add
--- /dev/null
+++ b/shared/sepolicy/vendor/hal_bootctl_default.te
@@ -0,0 +1 @@
+allow hal_bootctl_default boot_block_device:blk_file getattr;
diff --git a/shared/sepolicy/vendor/hal_graphics_allocator.te b/shared/sepolicy/vendor/hal_graphics_allocator.te
index 3136386..5975599 100644
--- a/shared/sepolicy/vendor/hal_graphics_allocator.te
+++ b/shared/sepolicy/vendor/hal_graphics_allocator.te
@@ -3,4 +3,3 @@
 
 # Read GCE initial metadata file
 allow hal_graphics_allocator_server initial_metadata_file:file r_file_perms;
-allow hal_graphics_allocator_server region_screen_device:chr_file rw_file_perms;
diff --git a/shared/sepolicy/vendor/hal_graphics_composer.te b/shared/sepolicy/vendor/hal_graphics_composer.te
index 5b4f974..6bba59d 100644
--- a/shared/sepolicy/vendor/hal_graphics_composer.te
+++ b/shared/sepolicy/vendor/hal_graphics_composer.te
@@ -1,7 +1,6 @@
 type vsock_frames_port_prop, property_type;
 
 allow hal_graphics_composer_server hal_graphics_allocator_default_tmpfs:file read;
-allow hal_graphics_composer_server region_screen_device:chr_file rw_file_perms;
 allow hal_graphics_composer_server self:{ socket vsock_socket } create_socket_perms_no_ioctl;
 gpu_access(hal_graphics_composer_server)
 
diff --git a/shared/sepolicy/vendor/libcuttlefish_rild.te b/shared/sepolicy/vendor/libcuttlefish_rild.te
index 8894919..49b7d50 100644
--- a/shared/sepolicy/vendor/libcuttlefish_rild.te
+++ b/shared/sepolicy/vendor/libcuttlefish_rild.te
@@ -5,8 +5,6 @@
 
 hal_server_domain(libcuttlefish_rild, hal_telephony)
 
-allow libcuttlefish_rild libcuttlefish_rild_device:chr_file rw_file_perms;
-
 # Failing to create these sockets appears to be non-fatal
 net_domain(libcuttlefish_rild)
 
diff --git a/shared/sepolicy/vendor/vendor_init.te b/shared/sepolicy/vendor/vendor_init.te
index e57bec0..f4fffe6 100644
--- a/shared/sepolicy/vendor/vendor_init.te
+++ b/shared/sepolicy/vendor/vendor_init.te
@@ -4,9 +4,6 @@
 
 allow vendor_init {
   audio_device
-  input_events_device
-  libcuttlefish_rild_device
-  region_screen_device
 }:chr_file { getattr };
 
 set_prop(vendor_init, hal_bluetooth_sim_prop)
diff --git a/shared/sepolicy/vendor/vsoc_input_service.te b/shared/sepolicy/vendor/vsoc_input_service.te
index 341e503..ec20985 100644
--- a/shared/sepolicy/vendor/vsoc_input_service.te
+++ b/shared/sepolicy/vendor/vsoc_input_service.te
@@ -8,11 +8,6 @@
 # I/O with /dev/uinput
 allow vsoc_input_service uhid_device:chr_file rw_file_perms;
 
-# Framebuffer I/O (needed to obtain the screen size)
-allow vsoc_input_service region_screen_device:chr_file rw_file_perms;
-
-allow vsoc_input_service input_events_device:chr_file rw_file_perms;
-
 net_domain(vsoc_input_service)
 
 get_prop(vsoc_input_service, cuttlefish_config_server_port_prop)
diff --git a/shared/tv/device.mk b/shared/tv/device.mk
index 25eb85e..71b5d70 100644
--- a/shared/tv/device.mk
+++ b/shared/tv/device.mk
@@ -20,9 +20,19 @@
 $(call inherit-product, $(SRC_TARGET_DIR)/product/core_minimal.mk)
 $(call inherit-product, device/google/cuttlefish/shared/device.mk)
 
+# Extend cuttlefish common sepolicy with tv-specific functionality
+BOARD_SEPOLICY_DIRS += device/google/cuttlefish/shared/tv/sepolicy/vendor
+
+PRODUCT_COPY_FILES += \
+    device/google/atv/permissions/tv_core_hardware.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/tv_core_hardware.xml \
+    frameworks/native/data/etc/android.hardware.bluetooth.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.bluetooth.xml \
+    frameworks/native/data/etc/android.hardware.hdmi.cec.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.hdmi.cec.xml \
+    frameworks/native/data/etc/android.hardware.sensor.accelerometer.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.accelerometer.xml \
+    frameworks/native/data/etc/android.hardware.sensor.compass.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.sensor.compass.xml \
+    frameworks/native/data/etc/android.hardware.touchscreen.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.touchscreen.xml \
+
 # HDMI CEC HAL
 PRODUCT_PACKAGES += android.hardware.tv.cec@1.0-service.mock
 
 # Enabling managed profiles
-PRODUCT_COPY_FILES += frameworks/native/data/etc/android.software.managed_users.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.software.managed_users.xml
 DEVICE_PACKAGE_OVERLAYS += device/google/cuttlefish/shared/tv/overlay
diff --git a/shared/tv/sepolicy/vendor/file_contexts b/shared/tv/sepolicy/vendor/file_contexts
new file mode 100644
index 0000000..88b1cf5
--- /dev/null
+++ b/shared/tv/sepolicy/vendor/file_contexts
@@ -0,0 +1 @@
+/vendor/bin/hw/android\.hardware\.tv\.cec@1\.0-service\.mock u:object_r:hal_tv_cec_mock_exec:s0
diff --git a/shared/sepolicy/vendor/hal_tv_cec_mock.te b/shared/tv/sepolicy/vendor/hal_tv_cec_mock.te
similarity index 100%
rename from shared/sepolicy/vendor/hal_tv_cec_mock.te
rename to shared/tv/sepolicy/vendor/hal_tv_cec_mock.te
diff --git a/tools/create_base_image_gce.sh b/tools/create_base_image_gce.sh
index 7c693c5..6db2463 100755
--- a/tools/create_base_image_gce.sh
+++ b/tools/create_base_image_gce.sh
@@ -99,6 +99,8 @@
   exit 1
 fi
 
+# Vulkan loader
+sudo chroot /mnt/image /usr/bin/apt install -y libvulkan1
 
 # Clean up the builder's version of resolv.conf
 sudo rm /mnt/image/etc/resolv.conf