patchpanel: Extract patchpanel-client into its own subdir

Move patchpanel-client into its own package. By doing so,
patchpanel-util will be removed from patchpanel-client.
Systems that previously use patchpanel-util need to update
its build rule to use patchpanel-util.

This is done to avoid dependency loops (e.g. chromium:2359478).
Other system can depend on patchpanel-client instead of
patchpanel after this patch.

BUG=b:166193772
TEST=./build_packages --board=atlas;
TEST=FEATURES=test emerge-atlas patchpanel-client \
     patchpanel permission_broker system-proxy \
     vm_host_tools
TEST=/usr/libexec/fuzzers/patchpanel_client_fuzzer
TEST=tryjob --hwtest
TEST=tast run <DUT> platform.Firewall
TEST=Crostini and ARC running

Cq-Depend: chromium:2382997
Change-Id: I6244b4808c75a75b69b0276aa10489b1d2501025
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2384496
Tested-by: Jason Jeremy Iman <jasongustaman@chromium.org>
Reviewed-by: Jorge Lucangeli Obes <jorgelo@chromium.org>
Reviewed-by: Yusuke Sato <yusukes@chromium.org>
Reviewed-by: Chirantan Ekbote <chirantan@chromium.org>
Reviewed-by: Hugo Benichi <hugobenichi@google.com>
Reviewed-by: Garrick Evans <garrick@chromium.org>
Commit-Queue: Jason Jeremy Iman <jasongustaman@chromium.org>
diff --git a/patchpanel/dbus/BUILD.gn b/patchpanel/dbus/BUILD.gn
new file mode 100644
index 0000000..1ee5077
--- /dev/null
+++ b/patchpanel/dbus/BUILD.gn
@@ -0,0 +1,97 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//common-mk/pkg_config.gni")
+import("//common-mk/proto_library.gni")
+
+group("all") {
+  deps = [ ":libpatchpanel-client" ]
+  if (use.fuzzer) {
+    deps += [ ":patchpanel_client_fuzzer" ]
+  }
+  if (use.test) {
+    deps += [ ":patchpanel-client_testrunner" ]
+  }
+}
+
+pkg_config("target_defaults") {
+  pkg_deps = [
+    "libbrillo",
+    "libchrome-${libbase_ver}",
+    "system_api",
+  ]
+  if (use.fuzzer) {
+    pkg_deps += [ "protobuf" ]
+  } else {
+    pkg_deps += [ "protobuf-lite" ]
+  }
+  defines = [ "USE_ARCVM=${use.arcvm}" ]
+}
+
+proto_library("protos") {
+  configs = [ ":target_defaults" ]
+  proto_in_dir = ".."
+  proto_out_dir = "include/patchpanel"
+  sources = [ "${proto_in_dir}/ipc.proto" ]
+}
+
+libpatchpanel_client_sources = [
+  "../net_util.cc",
+  "client.cc",
+]
+
+static_library("libpatchpanel-client_test") {
+  configs += [ ":target_defaults" ]
+  all_dependent_pkg_deps = [
+    "protobuf",
+    "system_api",
+  ]
+  sources = libpatchpanel_client_sources
+  deps = [ ":protos" ]
+}
+
+shared_library("libpatchpanel-client") {
+  configs += [ ":target_defaults" ]
+  all_dependent_pkg_deps = [
+    "protobuf",
+    "system_api",
+  ]
+  sources = libpatchpanel_client_sources
+}
+
+if (use.fuzzer) {
+  pkg_config("fuzzing_config") {
+    pkg_deps = [ "libchrome-test-${libbase_ver}" ]
+  }
+
+  executable("patchpanel_client_fuzzer") {
+    configs += [
+      "//common-mk/common_fuzzer",
+      ":target_defaults",
+      ":fuzzing_config",
+    ]
+    sources = [ "client_fuzzer.cc" ]
+    deps = [ ":libpatchpanel-client" ]
+  }
+}
+
+if (use.test) {
+  pkg_config("test_config") {
+    pkg_deps = [ "libchrome-test-${libbase_ver}" ]
+  }
+
+  executable("patchpanel-client_testrunner") {
+    sources = [ "client_test.cc" ]
+    configs += [
+      "//common-mk:test",
+      ":target_defaults",
+      ":test_config",
+    ]
+    defines = [ "UNIT_TEST" ]
+    deps = [
+      ":libpatchpanel-client_test",
+      "//common-mk/testrunner",
+    ]
+  }
+}
diff --git a/patchpanel/dbus/OWNERS b/patchpanel/dbus/OWNERS
new file mode 100644
index 0000000..c9cce26
--- /dev/null
+++ b/patchpanel/dbus/OWNERS
@@ -0,0 +1 @@
+include /patchpanel/OWNERS
diff --git a/patchpanel/dbus/client.cc b/patchpanel/dbus/client.cc
new file mode 100644
index 0000000..8f55543
--- /dev/null
+++ b/patchpanel/dbus/client.cc
@@ -0,0 +1,523 @@
+// Copyright 2016 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "patchpanel/dbus/client.h"
+
+#include <fcntl.h>
+
+#include <base/logging.h>
+#include <chromeos/dbus/service_constants.h>
+#include <dbus/message.h>
+#include <dbus/object_path.h>
+
+#include "patchpanel/net_util.h"
+
+namespace patchpanel {
+
+namespace {
+
+std::ostream& operator<<(std::ostream& stream,
+                         const ModifyPortRuleRequest& request) {
+  stream << "{ operation: "
+         << ModifyPortRuleRequest::Operation_Name(request.op())
+         << ", rule type: "
+         << ModifyPortRuleRequest::RuleType_Name(request.type())
+         << ", protocol: "
+         << ModifyPortRuleRequest::Protocol_Name(request.proto());
+  if (!request.input_ifname().empty()) {
+    stream << ", input interface name: " << request.input_ifname();
+  }
+  if (!request.input_dst_ip().empty()) {
+    stream << ", input destination IP: " << request.input_dst_ip();
+  }
+  stream << ", input destination port: " << request.input_dst_port();
+  if (!request.dst_ip().empty()) {
+    stream << ", destination IP: " << request.dst_ip();
+  }
+  if (request.dst_port() != 0) {
+    stream << ", destination port: " << request.dst_port();
+  }
+  stream << " }";
+  return stream;
+}
+
+}  // namespace
+
+// static
+std::unique_ptr<Client> Client::New() {
+  dbus::Bus::Options opts;
+  opts.bus_type = dbus::Bus::SYSTEM;
+  scoped_refptr<dbus::Bus> bus(new dbus::Bus(std::move(opts)));
+
+  if (!bus->Connect()) {
+    LOG(ERROR) << "Failed to connect to system bus";
+    return nullptr;
+  }
+
+  dbus::ObjectProxy* proxy = bus->GetObjectProxy(
+      kPatchPanelServiceName, dbus::ObjectPath(kPatchPanelServicePath));
+  if (!proxy) {
+    LOG(ERROR) << "Unable to get dbus proxy for " << kPatchPanelServiceName;
+    return nullptr;
+  }
+
+  return std::make_unique<Client>(std::move(bus), proxy);
+}
+
+Client::~Client() {
+  if (bus_)
+    bus_->ShutdownAndBlock();
+}
+
+bool Client::NotifyArcStartup(pid_t pid) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kArcStartupMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  ArcStartupRequest request;
+  request.set_pid(static_cast<uint32_t>(pid));
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode ArcStartupRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  ArcStartupResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  return true;
+}
+
+bool Client::NotifyArcShutdown() {
+  dbus::MethodCall method_call(kPatchPanelInterface, kArcShutdownMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  ArcShutdownRequest request;
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode ArcShutdownRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  ArcShutdownResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  return true;
+}
+
+std::vector<NetworkDevice> Client::NotifyArcVmStartup(uint32_t cid) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kArcVmStartupMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  ArcVmStartupRequest request;
+  request.set_cid(cid);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode ArcVmStartupRequest proto";
+    return {};
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return {};
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  ArcVmStartupResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return {};
+  }
+
+  std::vector<NetworkDevice> devices;
+  for (const auto& d : response.devices()) {
+    devices.emplace_back(d);
+  }
+  return devices;
+}
+
+bool Client::NotifyArcVmShutdown(uint32_t cid) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kArcVmShutdownMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  ArcVmShutdownRequest request;
+  request.set_cid(cid);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode ArcVmShutdownRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  ArcVmShutdownResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  return true;
+}
+
+bool Client::NotifyTerminaVmStartup(uint32_t cid,
+                                    NetworkDevice* device,
+                                    IPv4Subnet* container_subnet) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kTerminaVmStartupMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  TerminaVmStartupRequest request;
+  request.set_cid(cid);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode TerminaVmStartupRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  TerminaVmStartupResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  if (!response.has_device()) {
+    LOG(ERROR) << "No device found";
+    return false;
+  }
+  *device = response.device();
+
+  if (response.has_container_subnet()) {
+    *container_subnet = response.container_subnet();
+  } else {
+    LOG(WARNING) << "No container subnet found";
+  }
+
+  return true;
+}
+
+bool Client::NotifyTerminaVmShutdown(uint32_t cid) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kTerminaVmShutdownMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  TerminaVmShutdownRequest request;
+  request.set_cid(cid);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode TerminaVmShutdownRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  TerminaVmShutdownResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  return true;
+}
+
+bool Client::NotifyPluginVmStartup(uint64_t vm_id,
+                                   int subnet_index,
+                                   NetworkDevice* device) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kPluginVmStartupMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  PluginVmStartupRequest request;
+  request.set_id(vm_id);
+  request.set_subnet_index(subnet_index);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode PluginVmStartupRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  PluginVmStartupResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  if (!response.has_device()) {
+    LOG(ERROR) << "No device found";
+    return false;
+  }
+  *device = response.device();
+
+  return true;
+}
+
+bool Client::NotifyPluginVmShutdown(uint64_t vm_id) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kPluginVmShutdownMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  PluginVmShutdownRequest request;
+  request.set_id(vm_id);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode PluginVmShutdownRequest proto";
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send dbus message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  PluginVmShutdownResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse response proto";
+    return false;
+  }
+
+  return true;
+}
+
+bool Client::DefaultVpnRouting(int socket) {
+  return SendSetVpnIntentRequest(socket, SetVpnIntentRequest::DEFAULT_ROUTING);
+}
+
+bool Client::RouteOnVpn(int socket) {
+  return SendSetVpnIntentRequest(socket, SetVpnIntentRequest::ROUTE_ON_VPN);
+}
+
+bool Client::BypassVpn(int socket) {
+  return SendSetVpnIntentRequest(socket, SetVpnIntentRequest::BYPASS_VPN);
+}
+
+bool Client::SendSetVpnIntentRequest(
+    int socket, SetVpnIntentRequest::VpnRoutingPolicy policy) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kSetVpnIntentMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  SetVpnIntentRequest request;
+  SetVpnIntentResponse response;
+  request.set_policy(policy);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode SetVpnIntentRequest proto";
+    return false;
+  }
+  writer.AppendFileDescriptor(socket);
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR)
+        << "Failed to send SetVpnIntentRequest message to patchpanel service";
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse SetVpnIntentResponse proto";
+    return false;
+  }
+
+  if (!response.success()) {
+    LOG(ERROR) << "SetVpnIntentRequest failed";
+    return false;
+  }
+  return true;
+}
+
+std::pair<base::ScopedFD, patchpanel::ConnectNamespaceResponse>
+Client::ConnectNamespace(pid_t pid,
+                         const std::string& outbound_ifname,
+                         bool forward_user_traffic) {
+  // Prepare and serialize the request proto.
+  ConnectNamespaceRequest request;
+  request.set_pid(static_cast<int32_t>(pid));
+  request.set_outbound_physical_device(outbound_ifname);
+  request.set_allow_user_traffic(forward_user_traffic);
+
+  dbus::MethodCall method_call(kPatchPanelInterface, kConnectNamespaceMethod);
+  dbus::MessageWriter writer(&method_call);
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode ConnectNamespaceRequest proto";
+    return {};
+  }
+
+  // Prepare an fd pair and append one fd directly after the serialized request.
+  int pipe_fds[2] = {-1, -1};
+  if (pipe2(pipe_fds, O_CLOEXEC) < 0) {
+    PLOG(ERROR) << "Failed to create a pair of fds with pipe2()";
+    return {};
+  }
+  base::ScopedFD fd_local(pipe_fds[0]);
+  // MessageWriter::AppendFileDescriptor duplicates the fd, so use ScopeFD to
+  // make sure the original fd is closed eventually.
+  base::ScopedFD fd_remote(pipe_fds[1]);
+  writer.AppendFileDescriptor(pipe_fds[1]);
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send ConnectNamespace message to patchpanel";
+    return {};
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  ConnectNamespaceResponse response;
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse ConnectNamespaceResponse proto";
+    return {};
+  }
+
+  if (response.peer_ifname().empty() || response.host_ifname().empty()) {
+    LOG(ERROR) << "ConnectNamespace for netns pid " << pid << " failed";
+    return {};
+  }
+
+  std::string subnet_info = IPv4AddressToCidrString(
+      response.ipv4_subnet().base_addr(), response.ipv4_subnet().prefix_len());
+  LOG(INFO) << "ConnectNamespace for netns pid " << pid
+            << " succeeded: peer_ifname=" << response.peer_ifname()
+            << " peer_ipv4_address="
+            << IPv4AddressToString(response.peer_ipv4_address())
+            << " host_ifname=" << response.host_ifname()
+            << " host_ipv4_address="
+            << IPv4AddressToString(response.host_ipv4_address())
+            << " subnet=" << subnet_info;
+
+  return std::make_pair(std::move(fd_local), std::move(response));
+}
+
+std::vector<TrafficCounter> Client::GetTrafficCounters(
+    const std::set<std::string>& devices) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kGetTrafficCountersMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  TrafficCountersRequest request;
+  for (const auto& device : devices) {
+    request.add_devices(device);
+  }
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode TrafficCountersRequest proto";
+    return {};
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR) << "Failed to send TrafficCountersRequest message to patchpanel "
+                  "service";
+    return {};
+  }
+
+  TrafficCountersResponse response;
+  dbus::MessageReader reader(dbus_response.get());
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse TrafficCountersResponse proto";
+    return {};
+  }
+
+  return {response.counters().begin(), response.counters().end()};
+}
+
+bool Client::ModifyPortRule(ModifyPortRuleRequest::Operation op,
+                            ModifyPortRuleRequest::RuleType type,
+                            ModifyPortRuleRequest::Protocol proto,
+                            const std::string& input_ifname,
+                            const std::string& input_dst_ip,
+                            uint32_t input_dst_port,
+                            const std::string& dst_ip,
+                            uint32_t dst_port) {
+  dbus::MethodCall method_call(kPatchPanelInterface, kModifyPortRuleMethod);
+  dbus::MessageWriter writer(&method_call);
+
+  ModifyPortRuleRequest request;
+  ModifyPortRuleResponse response;
+
+  request.set_op(op);
+  request.set_type(type);
+  request.set_proto(proto);
+  request.set_input_ifname(input_ifname);
+  request.set_input_dst_ip(input_dst_ip);
+  request.set_input_dst_port(input_dst_port);
+  request.set_dst_ip(dst_ip);
+  request.set_dst_port(dst_port);
+
+  if (!writer.AppendProtoAsArrayOfBytes(request)) {
+    LOG(ERROR) << "Failed to encode ModifyPortRuleRequest proto " << request;
+    return false;
+  }
+
+  std::unique_ptr<dbus::Response> dbus_response = proxy_->CallMethodAndBlock(
+      &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT);
+  if (!dbus_response) {
+    LOG(ERROR)
+        << "Failed to send ModifyPortRuleRequest message to patchpanel service "
+        << request;
+    return false;
+  }
+
+  dbus::MessageReader reader(dbus_response.get());
+  if (!reader.PopArrayOfBytesAsProto(&response)) {
+    LOG(ERROR) << "Failed to parse ModifyPortRuleResponse proto " << request;
+    return false;
+  }
+
+  if (!response.success()) {
+    LOG(ERROR) << "ModifyPortRuleRequest failed " << request;
+    return false;
+  }
+  return true;
+}
+
+}  // namespace patchpanel
diff --git a/patchpanel/dbus/client.h b/patchpanel/dbus/client.h
new file mode 100644
index 0000000..cefeddc
--- /dev/null
+++ b/patchpanel/dbus/client.h
@@ -0,0 +1,102 @@
+// Copyright 2019 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PATCHPANEL_DBUS_CLIENT_H_
+#define PATCHPANEL_DBUS_CLIENT_H_
+
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/files/scoped_file.h"
+#include <brillo/brillo_export.h>
+#include <dbus/bus.h>
+#include <dbus/object_proxy.h>
+#include <patchpanel/proto_bindings/patchpanel_service.pb.h>
+
+namespace patchpanel {
+
+// Simple wrapper around patchpanel DBus API. All public functions are blocking
+// DBus calls to patchpaneld. The method names and protobuf schema used by
+// patchpanel DBus API are defined in platform2/system_api/dbus/patchpanel.
+// Access control for clients is defined in platform2/patchpanel/dbus.
+class BRILLO_EXPORT Client {
+ public:
+  static std::unique_ptr<Client> New();
+
+  Client(const scoped_refptr<dbus::Bus>& bus, dbus::ObjectProxy* proxy)
+      : bus_(std::move(bus)), proxy_(proxy) {}
+  ~Client();
+
+  bool NotifyArcStartup(pid_t pid);
+  bool NotifyArcShutdown();
+
+  std::vector<NetworkDevice> NotifyArcVmStartup(uint32_t cid);
+  bool NotifyArcVmShutdown(uint32_t cid);
+
+  bool NotifyTerminaVmStartup(uint32_t cid,
+                              NetworkDevice* device,
+                              IPv4Subnet* container_subnet);
+  bool NotifyTerminaVmShutdown(uint32_t cid);
+
+  bool NotifyPluginVmStartup(uint64_t vm_id,
+                             int subnet_index,
+                             NetworkDevice* device);
+  bool NotifyPluginVmShutdown(uint64_t vm_id);
+
+  // Reset the VPN routing intent mark on a socket to the default policy for
+  // the current uid. This is in general incorrect to call this method for
+  // a socket that is already connected.
+  bool DefaultVpnRouting(int socket);
+
+  // Mark a socket to be always routed through a VPN if there is one.
+  // Must be called before the socket is connected.
+  bool RouteOnVpn(int socket);
+
+  // Mark a socket to be always routed through the physical network.
+  // Must be called before the socket is connected.
+  bool BypassVpn(int socket);
+
+  // Sends a ConnectNamespaceRequest for the given namespace pid. Returns a
+  // pair with a valid ScopedFD and the ConnectNamespaceResponse proto message
+  // received if the request succeeded. Closing the ScopedFD will teardown the
+  // veth and routing setup and free the allocated IPv4 subnet.
+  std::pair<base::ScopedFD, patchpanel::ConnectNamespaceResponse>
+  ConnectNamespace(pid_t pid,
+                   const std::string& outbound_ifname,
+                   bool forward_user_traffic);
+
+  // Gets the traffic counters kept by patchpanel. |devices| is the set of
+  // interfaces (shill devices) for which counters should be returned, any
+  // unknown interfaces will be ignored. If |devices| is empty, counters for all
+  // known interfaces will be returned.
+  std::vector<TrafficCounter> GetTrafficCounters(
+      const std::set<std::string>& devices);
+
+  // Sends a ModifyPortRuleRequest to modify iptables ingress rules.
+  // This should only be called by permission_broker's 'devbroker'.
+  bool ModifyPortRule(patchpanel::ModifyPortRuleRequest::Operation op,
+                      patchpanel::ModifyPortRuleRequest::RuleType type,
+                      patchpanel::ModifyPortRuleRequest::Protocol proto,
+                      const std::string& input_ifname,
+                      const std::string& input_dst_ip,
+                      uint32_t input_dst_port,
+                      const std::string& dst_ip,
+                      uint32_t dst_port);
+
+ private:
+  scoped_refptr<dbus::Bus> bus_;
+  dbus::ObjectProxy* proxy_ = nullptr;  // owned by bus_
+
+  bool SendSetVpnIntentRequest(int socket,
+                               SetVpnIntentRequest::VpnRoutingPolicy policy);
+
+  DISALLOW_COPY_AND_ASSIGN(Client);
+};
+
+}  // namespace patchpanel
+
+#endif  // PATCHPANEL_DBUS_CLIENT_H_
diff --git a/patchpanel/dbus/client_fuzzer.cc b/patchpanel/dbus/client_fuzzer.cc
new file mode 100644
index 0000000..75a328f
--- /dev/null
+++ b/patchpanel/dbus/client_fuzzer.cc
@@ -0,0 +1,112 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <net/if.h>
+
+#include <base/logging.h>
+#include <dbus/message.h>
+#include <fuzzer/FuzzedDataProvider.h>
+
+#include "patchpanel/dbus/client.h"
+
+namespace patchpanel {
+
+class Environment {
+ public:
+  Environment() {
+    logging::SetMinLogLevel(logging::LOG_FATAL);  // <- DISABLE LOGGING.
+  }
+};
+
+class FakeObjectProxy : public dbus::ObjectProxy {
+ public:
+  explicit FakeObjectProxy(dbus::Bus* bus)
+      : dbus::ObjectProxy(bus, "svc", dbus::ObjectPath("/obj/path"), 0) {}
+
+  std::unique_ptr<dbus::Response> CallMethodAndBlockWithErrorDetails(
+      dbus::MethodCall* method_call,
+      int timeout_ms,
+      dbus::ScopedDBusError* error) override {
+    return nullptr;
+  }
+
+  std::unique_ptr<dbus::Response> CallMethodAndBlock(
+      dbus::MethodCall* method_call, int timeout_ms) override {
+    return nullptr;
+  }
+
+  void CallMethod(dbus::MethodCall* method_call,
+                  int timeout_ms,
+                  ResponseCallback callback) override {}
+
+  void CallMethodWithErrorResponse(dbus::MethodCall* method_call,
+                                   int timeout_ms,
+                                   ResponseOrErrorCallback callback) override {}
+
+  void CallMethodWithErrorCallback(dbus::MethodCall* method_call,
+                                   int timeout_ms,
+                                   ResponseCallback callback,
+                                   ErrorCallback error_callback) override {}
+
+  void ConnectToSignal(const std::string& interface_name,
+                       const std::string& signal_name,
+                       SignalCallback signal_callback,
+                       OnConnectedCallback on_connected_callback) override {}
+
+  void WaitForServiceToBeAvailable(
+      WaitForServiceToBeAvailableCallback callback) override {}
+
+  void Detach() override {}
+};
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+  static Environment env;
+  dbus::Bus::Options options;
+  scoped_refptr<dbus::Bus> bus = new dbus::Bus(options);
+  scoped_refptr<dbus::ObjectProxy> proxy(new FakeObjectProxy(bus.get()));
+  Client client(bus, proxy.get());
+  FuzzedDataProvider provider(data, size);
+
+  while (provider.remaining_bytes() > 0) {
+    client.NotifyArcStartup(provider.ConsumeIntegral<pid_t>());
+    client.NotifyArcVmStartup(provider.ConsumeIntegral<uint32_t>());
+    client.NotifyArcVmShutdown(provider.ConsumeIntegral<uint32_t>());
+    NetworkDevice device;
+    device.set_ifname(provider.ConsumeRandomLengthString(IFNAMSIZ * 2));
+    device.set_ipv4_addr(provider.ConsumeIntegral<uint32_t>());
+    device.mutable_ipv4_subnet()->set_base_addr(
+        provider.ConsumeIntegral<uint32_t>());
+    device.mutable_ipv4_subnet()->set_prefix_len(
+        provider.ConsumeIntegral<uint32_t>());
+    IPv4Subnet subnet;
+    subnet.set_base_addr(provider.ConsumeIntegral<uint32_t>());
+    subnet.set_prefix_len(provider.ConsumeIntegral<uint32_t>());
+    client.NotifyTerminaVmStartup(provider.ConsumeIntegral<uint32_t>(), &device,
+                                  &subnet);
+    client.NotifyTerminaVmShutdown(provider.ConsumeIntegral<uint32_t>());
+    client.NotifyPluginVmStartup(provider.ConsumeIntegral<uint64_t>(),
+                                 provider.ConsumeIntegral<int>(), &device);
+    client.NotifyPluginVmShutdown(provider.ConsumeIntegral<uint64_t>());
+    // TODO(garrick): Enable the following once the memory leaks in Chrome OS
+    // DBus are resolved.
+    //    client.DefaultVpnRouting(provider.ConsumeIntegral<int>());
+    //    client.RouteOnVpn(provider.ConsumeIntegral<int>());
+    //    client.BypassVpn(provider.ConsumeIntegral<int>());
+    client.ConnectNamespace(provider.ConsumeIntegral<pid_t>(),
+                            provider.ConsumeRandomLengthString(100),
+                            provider.ConsumeBool());
+    std::set<std::string> devices_for_counters;
+    for (int i = 0; i < 10; i++) {
+      if (provider.ConsumeBool()) {
+        devices_for_counters.insert(
+            provider.ConsumeRandomLengthString(IFNAMSIZ * 2));
+      }
+    }
+    client.GetTrafficCounters(devices_for_counters);
+  }
+  bus->ShutdownAndBlock();
+  return 0;
+}
+
+}  // namespace patchpanel
diff --git a/patchpanel/dbus/client_test.cc b/patchpanel/dbus/client_test.cc
new file mode 100644
index 0000000..01c7b30
--- /dev/null
+++ b/patchpanel/dbus/client_test.cc
@@ -0,0 +1,80 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "patchpanel/dbus/client.h"
+
+#include <chromeos/dbus/service_constants.h>
+#include <dbus/message.h>
+#include <dbus/mock_bus.h>
+#include <dbus/mock_object_proxy.h>
+#include <dbus/object_path.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "patchpanel/net_util.h"
+
+namespace patchpanel {
+
+using ::testing::_;
+using ::testing::ByMove;
+using ::testing::Return;
+
+namespace {
+
+scoped_refptr<dbus::MockBus> MockDBus() {
+  return new dbus::MockBus{dbus::Bus::Options{}};
+}
+
+scoped_refptr<dbus::MockObjectProxy> PatchPanelMockProxy(dbus::MockBus* dbus) {
+  return new dbus::MockObjectProxy(dbus, kPatchPanelServiceName,
+                                   dbus::ObjectPath(kPatchPanelServicePath));
+}
+
+}  // namespace
+
+TEST(ClientTest, ConnectNamespace) {
+  auto dbus = MockDBus();
+  auto proxy = PatchPanelMockProxy(dbus.get());
+  pid_t pid = 3456;
+  std::string outboud_ifname = "";
+
+  Client client(dbus, proxy.get());
+
+  // Failure case
+  auto result = client.ConnectNamespace(pid, outboud_ifname, false);
+  EXPECT_FALSE(result.first.is_valid());
+  EXPECT_TRUE(result.second.peer_ifname().empty());
+  EXPECT_TRUE(result.second.host_ifname().empty());
+  EXPECT_EQ(0, result.second.peer_ipv4_address());
+  EXPECT_EQ(0, result.second.host_ipv4_address());
+  EXPECT_EQ(0, result.second.ipv4_subnet().base_addr());
+  EXPECT_EQ(0, result.second.ipv4_subnet().prefix_len());
+
+  // Success case
+  patchpanel::ConnectNamespaceResponse response_proto;
+  response_proto.set_peer_ifname("veth0");
+  response_proto.set_host_ifname("arc_ns0");
+  response_proto.set_peer_ipv4_address(Ipv4Addr(100, 115, 92, 130));
+  response_proto.set_host_ipv4_address(Ipv4Addr(100, 115, 92, 129));
+  auto* response_subnet = response_proto.mutable_ipv4_subnet();
+  response_subnet->set_prefix_len(30);
+  response_subnet->set_base_addr(Ipv4Addr(100, 115, 92, 128));
+  std::unique_ptr<dbus::Response> response = dbus::Response::CreateEmpty();
+  dbus::MessageWriter response_writer(response.get());
+  response_writer.AppendProtoAsArrayOfBytes(response_proto);
+  EXPECT_CALL(*proxy, CallMethodAndBlock(_, _))
+      .WillOnce(Return(ByMove(std::move(response))));
+
+  result = client.ConnectNamespace(pid, outboud_ifname, false);
+  EXPECT_TRUE(result.first.is_valid());
+  EXPECT_EQ("arc_ns0", result.second.host_ifname());
+  EXPECT_EQ("veth0", result.second.peer_ifname());
+  EXPECT_EQ(30, result.second.ipv4_subnet().prefix_len());
+  EXPECT_EQ(Ipv4Addr(100, 115, 92, 128),
+            result.second.ipv4_subnet().base_addr());
+  EXPECT_EQ(Ipv4Addr(100, 115, 92, 129), result.second.host_ipv4_address());
+  EXPECT_EQ(Ipv4Addr(100, 115, 92, 130), result.second.peer_ipv4_address());
+}
+
+}  // namespace patchpanel
diff --git a/patchpanel/dbus/libpatchpanel-client.pc.in b/patchpanel/dbus/libpatchpanel-client.pc.in
new file mode 100644
index 0000000..6912657
--- /dev/null
+++ b/patchpanel/dbus/libpatchpanel-client.pc.in
@@ -0,0 +1,9 @@
+bslot=@BSLOT@
+include_dir=@INCLUDE_DIR@
+
+Name: libpatchpanel-client
+Description: PatchPanel networking client library
+Version: ${bslot}
+CFlags: -I${include_dir}
+Libs: -lpatchpanel-client
+
diff --git a/patchpanel/dbus/preinstall.sh b/patchpanel/dbus/preinstall.sh
new file mode 100755
index 0000000..7d2484f
--- /dev/null
+++ b/patchpanel/dbus/preinstall.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+set -e
+
+v=$1
+include_dir=$2
+out=$3
+
+sed \
+  -e "s/@BSLOT@/${v}/g" \
+  -e "s:@INCLUDE_DIR@:${include_dir}:g" \
+  "libpatchpanel-client.pc.in" > "${out}/libpatchpanel-client.pc"