gfe-relnote: Default-initialize QUIC BBRv2 loss event threshold for exiting STARTUP from a flag. Protected by --gfe2_reloadable_flag_quic_default_to_bbr_v2.

PiperOrigin-RevId: 264298542
Change-Id: I304ab19e4820dec51d3f8ef53762a393f6b175fd
diff --git a/quic/core/congestion_control/bbr2_misc.h b/quic/core/congestion_control/bbr2_misc.h
index 0dee438..4ad351e 100644
--- a/quic/core/congestion_control/bbr2_misc.h
+++ b/quic/core/congestion_control/bbr2_misc.h
@@ -87,7 +87,8 @@
   QuicRoundTripCount startup_full_bw_rounds = 3;
 
   // The minimum number of loss marking events to exit STARTUP.
-  int64_t startup_full_loss_count = 8;
+  int64_t startup_full_loss_count =
+      GetQuicFlag(FLAGS_quic_bbr2_default_startup_full_loss_count);
 
   /*
    * DRAIN parameters.
diff --git a/quic/qbone/bonnet/icmp_reachable.cc b/quic/qbone/bonnet/icmp_reachable.cc
new file mode 100644
index 0000000..7779a8b
--- /dev/null
+++ b/quic/qbone/bonnet/icmp_reachable.cc
@@ -0,0 +1,212 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/bonnet/icmp_reachable.h"
+
+#include <netinet/ip6.h>
+
+#include "net/third_party/quiche/src/quic/core/crypto/quic_random.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_endian.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_mutex.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/icmp_packet.h"
+
+namespace quic {
+namespace {
+
+constexpr int kEpollFlags = EPOLLIN | EPOLLET;
+constexpr size_t kMtu = 1280;
+
+constexpr size_t kIPv6AddrSize = sizeof(in6_addr);
+
+}  // namespace
+
+const char kUnknownSource[] = "UNKNOWN";
+const char kNoSource[] = "N/A";
+
+IcmpReachable::IcmpReachable(QuicIpAddress source,
+                             QuicIpAddress destination,
+                             absl::Duration timeout,
+                             KernelInterface* kernel,
+                             QuicEpollServer* epoll_server,
+                             StatsInterface* stats)
+    : timeout_(timeout),
+      cb_(this),
+      kernel_(kernel),
+      epoll_server_(epoll_server),
+      stats_(stats),
+      send_fd_(0),
+      recv_fd_(0) {
+  src_.sin6_family = AF_INET6;
+  dst_.sin6_family = AF_INET6;
+
+  memcpy(&src_.sin6_addr, source.ToPackedString().data(), kIPv6AddrSize);
+  memcpy(&dst_.sin6_addr, destination.ToPackedString().data(), kIPv6AddrSize);
+}
+
+IcmpReachable::~IcmpReachable() {
+  if (send_fd_ > 0) {
+    kernel_->close(send_fd_);
+  }
+  if (recv_fd_ > 0) {
+    if (!epoll_server_->ShutdownCalled()) {
+      epoll_server_->UnregisterFD(recv_fd_);
+    }
+
+    kernel_->close(recv_fd_);
+  }
+}
+
+bool IcmpReachable::Init() {
+  send_fd_ = kernel_->socket(PF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_RAW);
+  if (send_fd_ < 0) {
+    QUIC_LOG(ERROR) << "Unable to open socket: " << errno;
+    return false;
+  }
+
+  if (kernel_->bind(send_fd_, reinterpret_cast<struct sockaddr*>(&src_),
+                    sizeof(sockaddr_in6)) < 0) {
+    QUIC_LOG(ERROR) << "Unable to bind socket: " << errno;
+    return false;
+  }
+
+  recv_fd_ =
+      kernel_->socket(PF_INET6, SOCK_RAW | SOCK_NONBLOCK, IPPROTO_ICMPV6);
+  if (recv_fd_ < 0) {
+    QUIC_LOG(ERROR) << "Unable to open socket: " << errno;
+    return false;
+  }
+
+  if (kernel_->bind(recv_fd_, reinterpret_cast<struct sockaddr*>(&src_),
+                    sizeof(sockaddr_in6)) < 0) {
+    QUIC_LOG(ERROR) << "Unable to bind socket: " << errno;
+    return false;
+  }
+
+  icmp6_filter filter;
+  ICMP6_FILTER_SETBLOCKALL(&filter);
+  ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter);
+  if (kernel_->setsockopt(recv_fd_, SOL_ICMPV6, ICMP6_FILTER, &filter,
+                          sizeof(filter)) < 0) {
+    QUIC_LOG(ERROR) << "Unable to set ICMP6 filter.";
+    return false;
+  }
+
+  epoll_server_->RegisterFD(recv_fd_, &cb_, kEpollFlags);
+  epoll_server_->RegisterAlarm(0, this);
+
+  epoll_server_->set_timeout_in_us(50000);
+
+  QuicWriterMutexLock mu(&header_lock_);
+  icmp_header_.icmp6_type = ICMP6_ECHO_REQUEST;
+  icmp_header_.icmp6_code = 0;
+
+  QuicRandom::GetInstance()->RandBytes(&icmp_header_.icmp6_id,
+                                       sizeof(uint16_t));
+
+  return true;
+}
+
+bool IcmpReachable::OnEvent(int fd) {
+  char buffer[kMtu];
+
+  sockaddr_in6 source_addr{};
+  socklen_t source_addr_len = sizeof(source_addr);
+
+  ssize_t size = kernel_->recvfrom(fd, &buffer, kMtu, 0,
+                                   reinterpret_cast<sockaddr*>(&source_addr),
+                                   &source_addr_len);
+
+  if (size < 0) {
+    if (errno != EAGAIN && errno != EWOULDBLOCK) {
+      stats_->OnReadError(errno);
+    }
+    return false;
+  }
+
+  QUIC_VLOG(2) << QuicTextUtils::HexDump(QuicStringPiece(buffer, size));
+
+  auto* header = reinterpret_cast<const icmp6_hdr*>(&buffer);
+  QuicWriterMutexLock mu(&header_lock_);
+  if (header->icmp6_data32[0] != icmp_header_.icmp6_data32[0]) {
+    QUIC_VLOG(2) << "Unexpected response. id: " << header->icmp6_id
+                 << " seq: " << header->icmp6_seq
+                 << " Expected id: " << icmp_header_.icmp6_id
+                 << " seq: " << icmp_header_.icmp6_seq;
+    return true;
+  }
+  end_ = absl::Now();
+  QUIC_VLOG(1) << "Received ping response in "
+               << absl::ToInt64Microseconds(end_ - start_) << "us.";
+
+  string source;
+  QuicIpAddress source_ip;
+  if (!source_ip.FromPackedString(
+          reinterpret_cast<char*>(&source_addr.sin6_addr), sizeof(in6_addr))) {
+    QUIC_LOG(WARNING) << "Unable to parse source address.";
+    source = kUnknownSource;
+  } else {
+    source = source_ip.ToString();
+  }
+  stats_->OnEvent({Status::REACHABLE, end_ - start_, source});
+  return true;
+}
+
+int64 /* allow-non-std-int */ IcmpReachable::OnAlarm() {
+  EpollAlarm::OnAlarm();
+
+  QuicWriterMutexLock mu(&header_lock_);
+
+  if (end_ < start_) {
+    QUIC_VLOG(1) << "Timed out on sequence: " << icmp_header_.icmp6_seq;
+    stats_->OnEvent({Status::UNREACHABLE, absl::ZeroDuration(), kNoSource});
+  }
+
+  icmp_header_.icmp6_seq++;
+  CreateIcmpPacket(src_.sin6_addr, dst_.sin6_addr, icmp_header_, "",
+                   [this](QuicStringPiece packet) {
+                     QUIC_VLOG(2) << QuicTextUtils::HexDump(packet);
+
+                     ssize_t size = kernel_->sendto(
+                         send_fd_, packet.data(), packet.size(), 0,
+                         reinterpret_cast<struct sockaddr*>(&dst_),
+                         sizeof(sockaddr_in6));
+
+                     if (size < packet.size()) {
+                       stats_->OnWriteError(errno);
+                     }
+                     start_ = absl::Now();
+                   });
+
+  return absl::ToUnixMicros(absl::Now() + timeout_);
+}
+
+QuicStringPiece IcmpReachable::StatusName(IcmpReachable::Status status) {
+  switch (status) {
+    case REACHABLE:
+      return "REACHABLE";
+    case UNREACHABLE:
+      return "UNREACHABLE";
+    default:
+      return "UNKNOWN";
+  }
+}
+
+void IcmpReachable::EpollCallback::OnEvent(int fd, QuicEpollEvent* event) {
+  bool can_read_more = reachable_->OnEvent(fd);
+  if (can_read_more) {
+    event->out_ready_mask |= EPOLLIN;
+  }
+}
+
+void IcmpReachable::EpollCallback::OnShutdown(QuicEpollServer* eps, int fd) {
+  eps->UnregisterFD(fd);
+}
+
+string IcmpReachable::EpollCallback::Name() const {
+  return "ICMP Reachable";
+}
+
+}  // namespace quic
diff --git a/quic/qbone/bonnet/icmp_reachable.h b/quic/qbone/bonnet/icmp_reachable.h
new file mode 100644
index 0000000..4b6403e
--- /dev/null
+++ b/quic/qbone/bonnet/icmp_reachable.h
@@ -0,0 +1,139 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_ICMP_REACHABLE_H_
+#define QUICHE_QUIC_QBONE_BONNET_ICMP_REACHABLE_H_
+
+#include <netinet/icmp6.h>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_mutex.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_string_piece.h"
+#include "net/third_party/quiche/src/quic/qbone/bonnet/icmp_reachable_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/kernel_interface.h"
+
+namespace quic {
+
+extern const char kUnknownSource[];
+extern const char kNoSource[];
+
+// IcmpReachable schedules itself with an EpollServer, periodically sending
+// ICMPv6 Echo Requests to the given |destination| on the interface that the
+// given |source| is bound to. Echo Requests are sent once every |timeout|.
+// On Echo Replies, timeouts, and I/O errors, the given |stats| object will
+// be called back with details of the event.
+class IcmpReachable : public IcmpReachableInterface {
+ public:
+  enum Status { REACHABLE, UNREACHABLE };
+
+  struct ReachableEvent {
+    Status status;
+    absl::Duration response_time;
+    string source;
+  };
+
+  class StatsInterface {
+   public:
+    StatsInterface() = default;
+
+    StatsInterface(const StatsInterface&) = delete;
+    StatsInterface& operator=(const StatsInterface&) = delete;
+
+    StatsInterface(StatsInterface&&) = delete;
+    StatsInterface& operator=(StatsInterface&&) = delete;
+
+    virtual ~StatsInterface() = default;
+
+    virtual void OnEvent(ReachableEvent event) = 0;
+
+    virtual void OnReadError(int error) = 0;
+
+    virtual void OnWriteError(int error) = 0;
+  };
+
+  // |source| is the IPv6 address bound to the interface that IcmpReachable will
+  //          send Echo Requests on.
+  // |destination| is the IPv6 address of the destination of the Echo Requests.
+  // |timeout| is the duration IcmpReachable will wait between Echo Requests.
+  //           If no Echo Response is received by the next Echo Request, it will
+  //           be considered a timeout.
+  // |kernel| is not owned, but should outlive this instance.
+  // |epoll_server| is not owned, but should outlive this instance.
+  //                IcmpReachable's Init() must be called from within the Epoll
+  //                Server's thread.
+  // |stats| is not owned, but should outlive this instance. It will be called
+  //         back on Echo Replies, timeouts, and I/O errors.
+  IcmpReachable(QuicIpAddress source,
+                QuicIpAddress destination,
+                absl::Duration timeout,
+                KernelInterface* kernel,
+                QuicEpollServer* epoll_server,
+                StatsInterface* stats);
+
+  ~IcmpReachable() override;
+
+  // Initializes this reachability probe. Must be called from within the
+  // |epoll_server|'s thread.
+  bool Init() LOCKS_EXCLUDED(header_lock_) override;
+
+  int64 /* allow-non-std-int */ OnAlarm() LOCKS_EXCLUDED(header_lock_) override;
+
+  static QuicStringPiece StatusName(Status status);
+
+ private:
+  class EpollCallback : public QuicEpollCallbackInterface {
+   public:
+    explicit EpollCallback(IcmpReachable* reachable) : reachable_(reachable) {}
+
+    EpollCallback(const EpollCallback&) = delete;
+    EpollCallback& operator=(const EpollCallback&) = delete;
+
+    EpollCallback(EpollCallback&&) = delete;
+    EpollCallback& operator=(EpollCallback&&) = delete;
+
+    void OnRegistration(QuicEpollServer* eps,
+                        int fd,
+                        int event_mask) override{};
+
+    void OnModification(int fd, int event_mask) override{};
+
+    void OnEvent(int fd, QuicEpollEvent* event) override;
+
+    void OnUnregistration(int fd, bool replaced) override{};
+
+    void OnShutdown(QuicEpollServer* eps, int fd) override;
+
+    string Name() const override;
+
+   private:
+    IcmpReachable* reachable_;
+  };
+
+  bool OnEvent(int fd) LOCKS_EXCLUDED(header_lock_);
+
+  const absl::Duration timeout_;
+
+  EpollCallback cb_;
+
+  sockaddr_in6 src_{};
+  sockaddr_in6 dst_{};
+
+  KernelInterface* kernel_;
+  QuicEpollServer* epoll_server_;
+
+  StatsInterface* stats_;
+
+  int send_fd_;
+  int recv_fd_;
+
+  QuicMutex header_lock_;
+  icmp6_hdr icmp_header_ GUARDED_BY(header_lock_){};
+
+  absl::Time start_ = absl::InfinitePast();
+  absl::Time end_ = absl::InfinitePast();
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_ICMP_REACHABLE_H_
diff --git a/quic/qbone/bonnet/icmp_reachable_interface.h b/quic/qbone/bonnet/icmp_reachable_interface.h
new file mode 100644
index 0000000..e766a89
--- /dev/null
+++ b/quic/qbone/bonnet/icmp_reachable_interface.h
@@ -0,0 +1,28 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_ICMP_REACHABLE_INTERFACE_H_
+#define QUICHE_QUIC_QBONE_BONNET_ICMP_REACHABLE_INTERFACE_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_epoll.h"
+
+namespace quic {
+
+class IcmpReachableInterface : public QuicEpollAlarmBase {
+ public:
+  IcmpReachableInterface() = default;
+
+  IcmpReachableInterface(const IcmpReachableInterface&) = delete;
+  IcmpReachableInterface& operator=(const IcmpReachableInterface&) = delete;
+
+  IcmpReachableInterface(IcmpReachableInterface&&) = delete;
+  IcmpReachableInterface& operator=(IcmpReachableInterface&&) = delete;
+
+  // Initializes this reachability probe.
+  virtual bool Init() = 0;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_ICMP_REACHABLE_INTERFACE_H_
diff --git a/quic/qbone/bonnet/icmp_reachable_test.cc b/quic/qbone/bonnet/icmp_reachable_test.cc
new file mode 100644
index 0000000..303f0e2
--- /dev/null
+++ b/quic/qbone/bonnet/icmp_reachable_test.cc
@@ -0,0 +1,266 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/bonnet/icmp_reachable.h"
+
+#include <netinet/ip6.h>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_containers.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_epoll.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/mock_kernel.h"
+
+namespace quic {
+namespace {
+
+using ::testing::_;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::StrictMock;
+
+constexpr char kSourceAddress[] = "fe80:1:2:3:4::1";
+constexpr char kDestinationAddress[] = "fe80:4:3:2:1::1";
+
+constexpr int kFakeWriteFd = 0;
+
+icmp6_hdr GetHeaderFromPacket(const void* buf, size_t len) {
+  CHECK_GE(len, sizeof(ip6_hdr) + sizeof(icmp6_hdr));
+
+  auto* buffer = reinterpret_cast<const char*>(buf);
+  return *reinterpret_cast<const icmp6_hdr*>(&buffer[sizeof(ip6_hdr)]);
+}
+
+class StatsInterface : public IcmpReachable::StatsInterface {
+ public:
+  void OnEvent(IcmpReachable::ReachableEvent event) override {
+    switch (event.status) {
+      case IcmpReachable::REACHABLE: {
+        reachable_count_++;
+        break;
+      }
+      case IcmpReachable::UNREACHABLE: {
+        unreachable_count_++;
+        break;
+      }
+    }
+    current_source_ = event.source;
+  }
+
+  void OnReadError(int error) override { read_errors_[error]++; }
+
+  void OnWriteError(int error) override { write_errors_[error]++; }
+
+  bool HasWriteErrors() { return !write_errors_.empty(); }
+
+  int WriteErrorCount(int error) { return write_errors_[error]; }
+
+  bool HasReadErrors() { return !read_errors_.empty(); }
+
+  int ReadErrorCount(int error) { return read_errors_[error]; }
+
+  int reachable_count() { return reachable_count_; }
+
+  int unreachable_count() { return unreachable_count_; }
+
+  string current_source() { return current_source_; }
+
+ private:
+  int reachable_count_ = 0;
+  int unreachable_count_ = 0;
+
+  string current_source_{};
+
+  QuicUnorderedMap<int, int> read_errors_;
+  QuicUnorderedMap<int, int> write_errors_;
+};
+
+class IcmpReachableTest : public QuicTest {
+ public:
+  IcmpReachableTest() {
+    CHECK(source_.FromString(kSourceAddress));
+    CHECK(destination_.FromString(kDestinationAddress));
+
+    int pipe_fds[2];
+    CHECK(pipe(pipe_fds) >= 0) << "pipe() failed";
+
+    read_fd_ = pipe_fds[0];
+    read_src_fd_ = pipe_fds[1];
+  }
+
+  void SetFdExpectations() {
+    InSequence seq;
+    EXPECT_CALL(kernel_, socket(_, _, _)).WillOnce(Return(kFakeWriteFd));
+    EXPECT_CALL(kernel_, bind(kFakeWriteFd, _, _)).WillOnce(Return(0));
+
+    EXPECT_CALL(kernel_, socket(_, _, _)).WillOnce(Return(read_fd_));
+    EXPECT_CALL(kernel_, bind(read_fd_, _, _)).WillOnce(Return(0));
+
+    EXPECT_CALL(kernel_, setsockopt(read_fd_, SOL_ICMPV6, ICMP6_FILTER, _, _));
+
+    EXPECT_CALL(kernel_, close(read_fd_)).WillOnce(Invoke([](int fd) {
+      return close(fd);
+    }));
+  }
+
+ protected:
+  QuicIpAddress source_;
+  QuicIpAddress destination_;
+
+  int read_fd_;
+  int read_src_fd_;
+
+  StrictMock<MockKernel> kernel_;
+  QuicEpollServer epoll_server_;
+  StatsInterface stats_;
+};
+
+TEST_F(IcmpReachableTest, SendsPings) {
+  IcmpReachable reachable(source_, destination_, absl::Seconds(0), &kernel_,
+                          &epoll_server_, &stats_);
+
+  SetFdExpectations();
+  ASSERT_TRUE(reachable.Init());
+
+  EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _))
+      .WillOnce(Invoke([](int sockfd, const void* buf, size_t len, int flags,
+                          const struct sockaddr* dest_addr, socklen_t addrlen) {
+        auto icmp_header = GetHeaderFromPacket(buf, len);
+        EXPECT_EQ(icmp_header.icmp6_type, ICMP6_ECHO_REQUEST);
+        EXPECT_EQ(icmp_header.icmp6_seq, 1);
+        return len;
+      }));
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_FALSE(stats_.HasWriteErrors());
+
+  epoll_server_.Shutdown();
+}
+
+TEST_F(IcmpReachableTest, HandlesUnreachableEvents) {
+  IcmpReachable reachable(source_, destination_, absl::Seconds(0), &kernel_,
+                          &epoll_server_, &stats_);
+
+  SetFdExpectations();
+  ASSERT_TRUE(reachable.Init());
+
+  EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _))
+      .Times(2)
+      .WillRepeatedly(Invoke([](int sockfd, const void* buf, size_t len,
+                                int flags, const struct sockaddr* dest_addr,
+                                socklen_t addrlen) { return len; }));
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_EQ(stats_.unreachable_count(), 0);
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_FALSE(stats_.HasWriteErrors());
+  EXPECT_EQ(stats_.unreachable_count(), 1);
+  EXPECT_EQ(stats_.current_source(), kNoSource);
+
+  epoll_server_.Shutdown();
+}
+
+TEST_F(IcmpReachableTest, HandlesReachableEvents) {
+  IcmpReachable reachable(source_, destination_, absl::Seconds(0), &kernel_,
+                          &epoll_server_, &stats_);
+
+  SetFdExpectations();
+  ASSERT_TRUE(reachable.Init());
+
+  icmp6_hdr last_request_hdr{};
+  EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _))
+      .Times(2)
+      .WillRepeatedly(
+          Invoke([&last_request_hdr](
+                     int sockfd, const void* buf, size_t len, int flags,
+                     const struct sockaddr* dest_addr, socklen_t addrlen) {
+            last_request_hdr = GetHeaderFromPacket(buf, len);
+            return len;
+          }));
+
+  sockaddr_in6 source_addr{};
+  string packed_source = source_.ToPackedString();
+  memcpy(&source_addr.sin6_addr, packed_source.data(), packed_source.size());
+
+  EXPECT_CALL(kernel_, recvfrom(read_fd_, _, _, _, _, _))
+      .WillOnce(
+          Invoke([&source_addr](int sockfd, void* buf, size_t len, int flags,
+                                struct sockaddr* src_addr, socklen_t* addrlen) {
+            *reinterpret_cast<sockaddr_in6*>(src_addr) = source_addr;
+            return read(sockfd, buf, len);
+          }));
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_EQ(stats_.reachable_count(), 0);
+
+  icmp6_hdr response = last_request_hdr;
+  response.icmp6_type = ICMP6_ECHO_REPLY;
+
+  write(read_src_fd_, reinterpret_cast<const void*>(&response),
+        sizeof(icmp6_hdr));
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_FALSE(stats_.HasReadErrors());
+  EXPECT_FALSE(stats_.HasWriteErrors());
+  EXPECT_EQ(stats_.reachable_count(), 1);
+  EXPECT_EQ(stats_.current_source(), source_.ToString());
+
+  epoll_server_.Shutdown();
+}
+
+TEST_F(IcmpReachableTest, HandlesWriteErrors) {
+  IcmpReachable reachable(source_, destination_, absl::Seconds(0), &kernel_,
+                          &epoll_server_, &stats_);
+
+  SetFdExpectations();
+  ASSERT_TRUE(reachable.Init());
+
+  EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _))
+      .WillOnce(Invoke([](int sockfd, const void* buf, size_t len, int flags,
+                          const struct sockaddr* dest_addr, socklen_t addrlen) {
+        errno = EAGAIN;
+        return 0;
+      }));
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_EQ(stats_.WriteErrorCount(EAGAIN), 1);
+
+  epoll_server_.Shutdown();
+}
+
+TEST_F(IcmpReachableTest, HandlesReadErrors) {
+  IcmpReachable reachable(source_, destination_, absl::Seconds(0), &kernel_,
+                          &epoll_server_, &stats_);
+
+  SetFdExpectations();
+  ASSERT_TRUE(reachable.Init());
+
+  EXPECT_CALL(kernel_, sendto(kFakeWriteFd, _, _, _, _, _))
+      .WillOnce(Invoke([](int sockfd, const void* buf, size_t len, int flags,
+                          const struct sockaddr* dest_addr,
+                          socklen_t addrlen) { return len; }));
+
+  EXPECT_CALL(kernel_, recvfrom(read_fd_, _, _, _, _, _))
+      .WillOnce(Invoke([](int sockfd, void* buf, size_t len, int flags,
+                          struct sockaddr* src_addr, socklen_t* addrlen) {
+        errno = EIO;
+        return -1;
+      }));
+
+  icmp6_hdr response{};
+
+  write(read_src_fd_, reinterpret_cast<const void*>(&response),
+        sizeof(icmp6_hdr));
+
+  epoll_server_.WaitForEventsAndExecuteCallbacks();
+  EXPECT_EQ(stats_.reachable_count(), 0);
+  EXPECT_EQ(stats_.ReadErrorCount(EIO), 1);
+
+  epoll_server_.Shutdown();
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/bonnet/mock_icmp_reachable.h b/quic/qbone/bonnet/mock_icmp_reachable.h
new file mode 100644
index 0000000..092845e
--- /dev/null
+++ b/quic/qbone/bonnet/mock_icmp_reachable.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_MOCK_ICMP_REACHABLE_H_
+#define QUICHE_QUIC_QBONE_BONNET_MOCK_ICMP_REACHABLE_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/bonnet/icmp_reachable_interface.h"
+
+namespace quic {
+
+class MockIcmpReachable : public IcmpReachableInterface {
+ public:
+  MOCK_METHOD0(Init, bool());
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_MOCK_ICMP_REACHABLE_H_
diff --git a/quic/qbone/bonnet/mock_tun_device.h b/quic/qbone/bonnet/mock_tun_device.h
new file mode 100644
index 0000000..37e852a
--- /dev/null
+++ b/quic/qbone/bonnet/mock_tun_device.h
@@ -0,0 +1,26 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_MOCK_TUN_DEVICE_H_
+#define QUICHE_QUIC_QBONE_BONNET_MOCK_TUN_DEVICE_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/bonnet/tun_device_interface.h"
+
+namespace quic {
+
+class MockTunDevice : public TunDeviceInterface {
+ public:
+  MOCK_METHOD0(Init, bool());
+
+  MOCK_METHOD0(Up, bool());
+
+  MOCK_METHOD0(Down, bool());
+
+  MOCK_CONST_METHOD0(GetFileDescriptor, int());
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_MOCK_TUN_DEVICE_H_
diff --git a/quic/qbone/bonnet/tun_device.cc b/quic/qbone/bonnet/tun_device.cc
new file mode 100644
index 0000000..6c0a8a5
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device.cc
@@ -0,0 +1,201 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/bonnet/tun_device.h"
+
+#include <fcntl.h>
+#include <linux/if_tun.h>
+#include <net/if.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/kernel_interface.h"
+
+namespace quic {
+
+const char kTapTunDevicePath[] = "/dev/net/tun";
+const int kInvalidFd = -1;
+
+TunDevice::TunDevice(const string& interface_name,
+                     int mtu,
+                     bool persist,
+                     KernelInterface* kernel)
+    : interface_name_(interface_name),
+      mtu_(mtu),
+      persist_(persist),
+      file_descriptor_(kInvalidFd),
+      kernel_(*kernel) {}
+
+TunDevice::~TunDevice() {
+  Down();
+  CleanUpFileDescriptor();
+}
+
+bool TunDevice::Init() {
+  if (interface_name_.empty() || interface_name_.size() >= IFNAMSIZ) {
+    QUIC_BUG << "interface_name must be nonempty and shorter than " << IFNAMSIZ;
+    return false;
+  }
+
+  if (!OpenDevice()) {
+    return false;
+  }
+
+  if (!ConfigureInterface()) {
+    return false;
+  }
+
+  return true;
+}
+
+// TODO(pengg): might be better to use netlink socket, once we have a library to
+// use
+bool TunDevice::Up() {
+  if (!is_interface_up_) {
+    struct ifreq if_request;
+    memset(&if_request, 0, sizeof(if_request));
+    // copy does not zero-terminate the result string, but we've memset the
+    // entire struct.
+    interface_name_.copy(if_request.ifr_name, IFNAMSIZ);
+    if_request.ifr_flags = IFF_UP;
+
+    is_interface_up_ =
+        NetdeviceIoctl(SIOCSIFFLAGS, reinterpret_cast<void*>(&if_request));
+    return is_interface_up_;
+  } else {
+    return true;
+  }
+}
+
+// TODO(pengg): might be better to use netlink socket, once we have a library to
+// use
+bool TunDevice::Down() {
+  if (is_interface_up_) {
+    struct ifreq if_request;
+    memset(&if_request, 0, sizeof(if_request));
+    // copy does not zero-terminate the result string, but we've memset the
+    // entire struct.
+    interface_name_.copy(if_request.ifr_name, IFNAMSIZ);
+    if_request.ifr_flags = 0;
+
+    is_interface_up_ =
+        !NetdeviceIoctl(SIOCSIFFLAGS, reinterpret_cast<void*>(&if_request));
+    return !is_interface_up_;
+  } else {
+    return true;
+  }
+}
+
+int TunDevice::GetFileDescriptor() const {
+  return file_descriptor_;
+}
+
+bool TunDevice::OpenDevice() {
+  struct ifreq if_request;
+  memset(&if_request, 0, sizeof(if_request));
+  // copy does not zero-terminate the result string, but we've memset the entire
+  // struct.
+  interface_name_.copy(if_request.ifr_name, IFNAMSIZ);
+
+  // Always set IFF_MULTI_QUEUE since a persistent device does not allow this
+  // flag to be flipped when re-opening it. The only way to flip this flag is to
+  // destroy the device and create a new one, but that deletes any existing
+  // routing associated with the interface, which makes the meaning of the
+  // 'persist' bit ambiguous.
+  if_request.ifr_flags = IFF_TUN | IFF_MULTI_QUEUE | IFF_NO_PI;
+
+  // TODO(pengg): port MakeCleanup to quic/platform? This makes the call to
+  // CleanUpFileDescriptor nicer and less error-prone.
+  // When the device is running with IFF_MULTI_QUEUE set, each call to open will
+  // create a queue which can be used to read/write packets from/to the device.
+  int fd = kernel_.open(kTapTunDevicePath, O_RDWR);
+  if (fd < 0) {
+    QUIC_PLOG(WARNING) << "Failed to open " << kTapTunDevicePath;
+    CleanUpFileDescriptor();
+    return false;
+  }
+  file_descriptor_ = fd;
+  if (!CheckFeatures(fd)) {
+    CleanUpFileDescriptor();
+    return false;
+  }
+
+  if (kernel_.ioctl(fd, TUNSETIFF, reinterpret_cast<void*>(&if_request)) != 0) {
+    QUIC_PLOG(WARNING) << "Failed to TUNSETIFF on fd(" << fd << ")";
+    CleanUpFileDescriptor();
+    return false;
+  }
+
+  if (kernel_.ioctl(
+          fd, TUNSETPERSIST,
+          persist_ ? reinterpret_cast<void*>(&if_request) : nullptr) != 0) {
+    QUIC_PLOG(WARNING) << "Failed to TUNSETPERSIST on fd(" << fd << ")";
+    CleanUpFileDescriptor();
+    return false;
+  }
+
+  return true;
+}
+
+// TODO(pengg): might be better to use netlink socket, once we have a library to
+// use
+bool TunDevice::ConfigureInterface() {
+  struct ifreq if_request;
+  memset(&if_request, 0, sizeof(if_request));
+  // copy does not zero-terminate the result string, but we've memset the entire
+  // struct.
+  interface_name_.copy(if_request.ifr_name, IFNAMSIZ);
+  if_request.ifr_mtu = mtu_;
+
+  if (!NetdeviceIoctl(SIOCSIFMTU, reinterpret_cast<void*>(&if_request))) {
+    CleanUpFileDescriptor();
+    return false;
+  }
+
+  return true;
+}
+
+bool TunDevice::CheckFeatures(int tun_device_fd) {
+  unsigned int actual_features;
+  if (kernel_.ioctl(tun_device_fd, TUNGETFEATURES, &actual_features) != 0) {
+    QUIC_PLOG(WARNING) << "Failed to TUNGETFEATURES";
+    return false;
+  }
+  unsigned int required_features = IFF_TUN | IFF_NO_PI;
+  if ((required_features & actual_features) != required_features) {
+    QUIC_LOG(WARNING)
+        << "Required feature does not exist. required_features: 0x" << std::hex
+        << required_features << " vs actual_features: 0x" << std::hex
+        << actual_features;
+    return false;
+  }
+  return true;
+}
+
+bool TunDevice::NetdeviceIoctl(int request, void* argp) {
+  int fd = kernel_.socket(AF_INET6, SOCK_DGRAM, 0);
+  if (fd < 0) {
+    QUIC_PLOG(WARNING) << "Failed to create AF_INET6 socket.";
+    return false;
+  }
+
+  if (kernel_.ioctl(fd, request, argp) != 0) {
+    QUIC_PLOG(WARNING) << "Failed ioctl request: " << request;
+    kernel_.close(fd);
+    return false;
+  }
+  kernel_.close(fd);
+  return true;
+}
+
+void TunDevice::CleanUpFileDescriptor() {
+  if (file_descriptor_ != kInvalidFd) {
+    kernel_.close(file_descriptor_);
+    file_descriptor_ = kInvalidFd;
+  }
+}
+
+}  // namespace quic
diff --git a/quic/qbone/bonnet/tun_device.h b/quic/qbone/bonnet/tun_device.h
new file mode 100644
index 0000000..1828b81
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device.h
@@ -0,0 +1,82 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_H_
+#define QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_H_
+
+#include <string>
+#include <vector>
+
+#include "net/third_party/quiche/src/quic/qbone/bonnet/tun_device_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/kernel_interface.h"
+
+namespace quic {
+
+class TunDevice : public TunDeviceInterface {
+ public:
+  // This represents a tun device created in the OS kernel, which is a virtual
+  // network interface that any packets sent to it can be read by a user space
+  // program that owns it. The routing rule that routes packets to this
+  // interface should be defined somewhere else.
+  //
+  // Standard read/write system calls can be used to receive/send packets
+  // from/to this interface. The file descriptor is owned by this class.
+  //
+  // If persist is set to true, the device won't be deleted even after
+  // destructing. The device will be picked up when initializing this class with
+  // the same interface_name on the next time.
+  //
+  // Persisting the device is useful if one wants to keep the routing rules
+  // since once a tun device is destroyed by the kernel, all the associated
+  // routing rules go away.
+  //
+  // The caller should own kernel and make sure it outlives this.
+  TunDevice(const string& interface_name,
+            int mtu,
+            bool persist,
+            KernelInterface* kernel);
+
+  ~TunDevice() override;
+
+  // Actually creates/reopens and configures the device.
+  bool Init() override;
+
+  // Marks the interface up to start receiving packets.
+  bool Up() override;
+
+  // Marks the interface down to stop receiving packets.
+  bool Down() override;
+
+  // Gets the file descriptor that can be used to send/receive packets.
+  // This returns -1 when the TUN device is in an invalid state.
+  int GetFileDescriptor() const override;
+
+ private:
+  // Creates or reopens the tun device.
+  bool OpenDevice();
+
+  // Configure the interface.
+  bool ConfigureInterface();
+
+  // Checks if the required kernel features exists.
+  bool CheckFeatures(int tun_device_fd);
+
+  // Closes the opened file descriptor and makes sure the file descriptor
+  // is no longer available from GetFileDescriptor;
+  void CleanUpFileDescriptor();
+
+  // Opens a socket and makes netdevice ioctl call
+  bool NetdeviceIoctl(int request, void* argp);
+
+  const string interface_name_;
+  const int mtu_;
+  const bool persist_;
+  int file_descriptor_;
+  KernelInterface& kernel_;
+  bool is_interface_up_ = false;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_H_
diff --git a/quic/qbone/bonnet/tun_device_interface.h b/quic/qbone/bonnet/tun_device_interface.h
new file mode 100644
index 0000000..e99c547
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device_interface.h
@@ -0,0 +1,33 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_INTERFACE_H_
+#define QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_INTERFACE_H_
+
+#include <vector>
+
+namespace quic {
+
+// An interface with methods for interacting with a TUN device.
+class TunDeviceInterface {
+ public:
+  virtual ~TunDeviceInterface() {}
+
+  // Actually creates/reopens and configures the device.
+  virtual bool Init() = 0;
+
+  // Marks the interface up to start receiving packets.
+  virtual bool Up() = 0;
+
+  // Marks the interface down to stop receiving packets.
+  virtual bool Down() = 0;
+
+  // Gets the file descriptor that can be used to send/receive packets.
+  // This returns -1 when the TUN device is in an invalid state.
+  virtual int GetFileDescriptor() const = 0;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_INTERFACE_H_
diff --git a/quic/qbone/bonnet/tun_device_packet_exchanger.cc b/quic/qbone/bonnet/tun_device_packet_exchanger.cc
new file mode 100644
index 0000000..1d246a2
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device_packet_exchanger.cc
@@ -0,0 +1,82 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/bonnet/tun_device_packet_exchanger.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_str_cat.h"
+
+namespace quic {
+
+TunDevicePacketExchanger::TunDevicePacketExchanger(
+    int fd,
+    size_t mtu,
+    KernelInterface* kernel,
+    QbonePacketExchanger::Visitor* visitor,
+    size_t max_pending_packets, StatsInterface* stats)
+    : QbonePacketExchanger(visitor, max_pending_packets),
+      fd_(fd),
+      mtu_(mtu),
+      kernel_(kernel),
+      stats_(stats) {}
+
+bool TunDevicePacketExchanger::WritePacket(const char* packet,
+                                           size_t size,
+                                           bool* blocked,
+                                           string* error) {
+  *blocked = false;
+  if (fd_ < 0) {
+    *error = QuicStrCat("Invalid file descriptor of the TUN device: ", fd_);
+    stats_->OnWriteError(error);
+    return false;
+  }
+
+  int result = kernel_->write(fd_, packet, size);
+  if (result == -1) {
+    if (errno == EWOULDBLOCK || errno == EAGAIN) {
+      // The tunnel is blocked. Note that this does not mean the receive buffer
+      // of a TCP connection is filled. This simply means the TUN device itself
+      // is blocked on handing packets to the rest part of the kernel.
+      *error = QuicStrCat("Write to the TUN device was blocked: ", errno);
+      *blocked = true;
+      stats_->OnWriteError(error);
+    }
+    return false;
+  }
+  stats_->OnPacketWritten();
+
+  return true;
+}
+
+std::unique_ptr<QuicData> TunDevicePacketExchanger::ReadPacket(bool* blocked,
+                                                               string* error) {
+  *blocked = false;
+  if (fd_ < 0) {
+    *error = QuicStrCat("Invalid file descriptor of the TUN device: ", fd_);
+    stats_->OnReadError(error);
+    return nullptr;
+  }
+  // Reading on a TUN device returns a packet at a time. If the packet is longer
+  // than the buffer, it's truncated.
+  auto read_buffer = QuicMakeUnique<char[]>(mtu_);
+  int result = kernel_->read(fd_, read_buffer.get(), mtu_);
+  // Note that 0 means end of file, but we're talking about a TUN device - there
+  // is no end of file. Therefore 0 also indicates error.
+  if (result <= 0) {
+    if (errno == EAGAIN || errno == EWOULDBLOCK) {
+      *error = QuicStrCat("Read from the TUN device was blocked: ", errno);
+      *blocked = true;
+      stats_->OnReadError(error);
+    }
+    return nullptr;
+  }
+  stats_->OnPacketRead();
+  return QuicMakeUnique<QuicData>(read_buffer.release(), result, true);
+}
+
+int TunDevicePacketExchanger::file_descriptor() const {
+  return fd_;
+}
+
+}  // namespace quic
diff --git a/quic/qbone/bonnet/tun_device_packet_exchanger.h b/quic/qbone/bonnet/tun_device_packet_exchanger.h
new file mode 100644
index 0000000..12d9efa
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device_packet_exchanger.h
@@ -0,0 +1,72 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_PACKET_EXCHANGER_H_
+#define QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_PACKET_EXCHANGER_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_packets.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/kernel_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_client_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_exchanger.h"
+
+namespace quic {
+
+class TunDevicePacketExchanger : public QbonePacketExchanger {
+ public:
+  class StatsInterface {
+   public:
+    StatsInterface() = default;
+
+    StatsInterface(const StatsInterface&) = delete;
+    StatsInterface& operator=(const StatsInterface&) = delete;
+
+    StatsInterface(StatsInterface&&) = delete;
+    StatsInterface& operator=(StatsInterface&&) = delete;
+
+    virtual ~StatsInterface() = default;
+
+    virtual void OnPacketRead() = 0;
+    virtual void OnPacketWritten() = 0;
+    virtual void OnReadError(string* error) = 0;
+    virtual void OnWriteError(string* error) = 0;
+  };
+
+  // |fd| is a open file descriptor on a TUN device that's opened for both read
+  // and write.
+  // |mtu| is the mtu of the TUN device.
+  // |kernel| is not owned but should out live objects of this class.
+  // |visitor| is not owned but should out live objects of this class.
+  // |max_pending_packets| controls the number of packets to be queued should
+  // the TUN device become blocked.
+  // |stats| is notified about packet read/write statistics. It is not owned,
+  // but should outlive objects of this class.
+  TunDevicePacketExchanger(int fd,
+                           size_t mtu,
+                           KernelInterface* kernel,
+                           QbonePacketExchanger::Visitor* visitor,
+                           size_t max_pending_packets,
+                           StatsInterface* stats);
+
+  int file_descriptor() const;
+
+ private:
+  // From QbonePacketExchanger.
+  std::unique_ptr<QuicData> ReadPacket(bool* blocked, string* error) override;
+
+  // From QbonePacketExchanger.
+  bool WritePacket(const char* packet,
+                   size_t size,
+                   bool* blocked,
+                   string* error) override;
+
+  int fd_ = -1;
+  size_t mtu_;
+  KernelInterface* kernel_;
+
+  StatsInterface* stats_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_BONNET_TUN_DEVICE_PACKET_EXCHANGER_H_
diff --git a/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc b/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
new file mode 100644
index 0000000..026ec26
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device_packet_exchanger_test.cc
@@ -0,0 +1,128 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/bonnet/tun_device_packet_exchanger.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/mock_qbone_client.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/mock_kernel.h"
+
+namespace quic {
+namespace {
+
+const size_t kMtu = 1000;
+const size_t kMaxPendingPackets = 5;
+const int kFd = 15;
+
+using ::testing::_;
+using ::testing::Invoke;
+using ::testing::StrEq;
+using ::testing::StrictMock;
+
+class MockVisitor : public QbonePacketExchanger::Visitor {
+ public:
+  MOCK_METHOD1(OnReadError, void(const string&));
+  MOCK_METHOD1(OnWriteError, void(const string&));
+};
+
+class MockStatsInterface : public TunDevicePacketExchanger::StatsInterface {
+ public:
+  MOCK_METHOD0(OnPacketRead, void());
+  MOCK_METHOD0(OnPacketWritten, void());
+
+  MOCK_METHOD1(OnReadError, void(string*));
+  MOCK_METHOD1(OnWriteError, void(string*));
+};
+
+class TunDevicePacketExchangerTest : public QuicTest {
+ protected:
+  TunDevicePacketExchangerTest()
+      : exchanger_(kFd,
+                   kMtu,
+                   &mock_kernel_,
+                   &mock_visitor_,
+                   kMaxPendingPackets,
+                   &mock_stats_) {}
+
+  ~TunDevicePacketExchangerTest() override {}
+
+  MockKernel mock_kernel_;
+  StrictMock<MockVisitor> mock_visitor_;
+  StrictMock<MockQboneClient> mock_client_;
+  StrictMock<MockStatsInterface> mock_stats_;
+  TunDevicePacketExchanger exchanger_;
+};
+
+TEST_F(TunDevicePacketExchangerTest, WritePacketReturnsFalseOnError) {
+  string packet = "fake packet";
+  EXPECT_CALL(mock_kernel_, write(kFd, _, packet.size()))
+      .WillOnce(Invoke([](int fd, const void* buf, size_t count) {
+        errno = ECOMM;
+        return -1;
+      }));
+
+  EXPECT_CALL(mock_visitor_, OnWriteError(_));
+  exchanger_.WritePacketToNetwork(packet.data(), packet.size());
+}
+
+TEST_F(TunDevicePacketExchangerTest,
+       WritePacketReturnFalseAndBlockedOnBlockedTunnel) {
+  string packet = "fake packet";
+  EXPECT_CALL(mock_kernel_, write(kFd, _, packet.size()))
+      .WillOnce(Invoke([](int fd, const void* buf, size_t count) {
+        errno = EAGAIN;
+        return -1;
+      }));
+
+  EXPECT_CALL(mock_stats_, OnWriteError(_)).Times(1);
+  exchanger_.WritePacketToNetwork(packet.data(), packet.size());
+}
+
+TEST_F(TunDevicePacketExchangerTest, WritePacketReturnsTrueOnSuccessfulWrite) {
+  string packet = "fake packet";
+  EXPECT_CALL(mock_kernel_, write(kFd, _, packet.size()))
+      .WillOnce(Invoke([packet](int fd, const void* buf, size_t count) {
+        EXPECT_THAT(reinterpret_cast<const char*>(buf), StrEq(packet));
+        return count;
+      }));
+
+  EXPECT_CALL(mock_stats_, OnPacketWritten()).Times(1);
+  exchanger_.WritePacketToNetwork(packet.data(), packet.size());
+}
+
+TEST_F(TunDevicePacketExchangerTest, ReadPacketReturnsNullOnError) {
+  EXPECT_CALL(mock_kernel_, read(kFd, _, kMtu))
+      .WillOnce(Invoke([](int fd, void* buf, size_t count) {
+        errno = ECOMM;
+        return -1;
+      }));
+  EXPECT_CALL(mock_visitor_, OnReadError(_));
+  exchanger_.ReadAndDeliverPacket(&mock_client_);
+}
+
+TEST_F(TunDevicePacketExchangerTest, ReadPacketReturnsNullOnBlockedRead) {
+  EXPECT_CALL(mock_kernel_, read(kFd, _, kMtu))
+      .WillOnce(Invoke([](int fd, void* buf, size_t count) {
+        errno = EAGAIN;
+        return -1;
+      }));
+  EXPECT_CALL(mock_stats_, OnReadError(_)).Times(1);
+  EXPECT_FALSE(exchanger_.ReadAndDeliverPacket(&mock_client_));
+}
+
+TEST_F(TunDevicePacketExchangerTest,
+       ReadPacketReturnsThePacketOnSuccessfulRead) {
+  string packet = "fake_packet";
+  EXPECT_CALL(mock_kernel_, read(kFd, _, kMtu))
+      .WillOnce(Invoke([packet](int fd, void* buf, size_t count) {
+        memcpy(buf, packet.data(), packet.size());
+        return packet.size();
+      }));
+  EXPECT_CALL(mock_client_, ProcessPacketFromNetwork(StrEq(packet)));
+  EXPECT_CALL(mock_stats_, OnPacketRead()).Times(1);
+  EXPECT_TRUE(exchanger_.ReadAndDeliverPacket(&mock_client_));
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/bonnet/tun_device_test.cc b/quic/qbone/bonnet/tun_device_test.cc
new file mode 100644
index 0000000..e9ae4d3
--- /dev/null
+++ b/quic/qbone/bonnet/tun_device_test.cc
@@ -0,0 +1,208 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/bonnet/tun_device.h"
+
+#include <linux/if.h>
+#include <linux/if_tun.h>
+#include <sys/ioctl.h>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/mock_kernel.h"
+
+namespace quic {
+namespace {
+
+using ::testing::_;
+using ::testing::AnyNumber;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::Unused;
+
+const char kDeviceName[] = "tun0";
+const int kSupportedFeatures =
+    IFF_TUN | IFF_TAP | IFF_MULTI_QUEUE | IFF_ONE_QUEUE | IFF_NO_PI;
+
+// Quite a bit of EXPECT_CALL().Times(AnyNumber()).WillRepeatedly() are used to
+// make sure we can correctly set common expectations and override the
+// expectation with later call to EXPECT_CALL(). ON_CALL cannot be used here
+// since when EPXECT_CALL overrides ON_CALL, it ignores the parameter matcher
+// which results in unexpected call even if ON_CALL exists.
+class TunDeviceTest : public QuicTest {
+ protected:
+  void SetUp() override {
+    EXPECT_CALL(mock_kernel_, socket(AF_INET6, _, _))
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke([this](Unused, Unused, Unused) {
+          EXPECT_CALL(mock_kernel_, close(next_fd_)).WillOnce(Return(0));
+          return next_fd_++;
+        }));
+  }
+
+  // Set the expectations for calling Init().
+  void SetInitExpectations(int mtu, bool persist) {
+    EXPECT_CALL(mock_kernel_, open(StrEq("/dev/net/tun"), _))
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke([this](Unused, Unused) {
+          EXPECT_CALL(mock_kernel_, close(next_fd_)).WillOnce(Return(0));
+          return next_fd_++;
+        }));
+    EXPECT_CALL(mock_kernel_, ioctl(_, TUNGETFEATURES, _))
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke([](Unused, Unused, void* argp) {
+          auto* actual_flags = reinterpret_cast<int*>(argp);
+          *actual_flags = kSupportedFeatures;
+          return 0;
+        }));
+    EXPECT_CALL(mock_kernel_, ioctl(_, TUNSETIFF, _))
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke([](Unused, Unused, void* argp) {
+          auto* ifr = reinterpret_cast<struct ifreq*>(argp);
+          EXPECT_EQ(IFF_TUN | IFF_MULTI_QUEUE | IFF_NO_PI, ifr->ifr_flags);
+          EXPECT_THAT(ifr->ifr_name, StrEq(kDeviceName));
+          return 0;
+        }));
+    EXPECT_CALL(mock_kernel_, ioctl(_, TUNSETPERSIST, _))
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke([persist](Unused, Unused, void* argp) {
+          auto* ifr = reinterpret_cast<struct ifreq*>(argp);
+          if (persist) {
+            EXPECT_THAT(ifr->ifr_name, StrEq(kDeviceName));
+          } else {
+            EXPECT_EQ(nullptr, ifr);
+          }
+          return 0;
+        }));
+    EXPECT_CALL(mock_kernel_, ioctl(_, SIOCSIFMTU, _))
+        .Times(AnyNumber())
+        .WillRepeatedly(Invoke([mtu](Unused, Unused, void* argp) {
+          auto* ifr = reinterpret_cast<struct ifreq*>(argp);
+          EXPECT_EQ(mtu, ifr->ifr_mtu);
+          EXPECT_THAT(ifr->ifr_name, StrEq(kDeviceName));
+          return 0;
+        }));
+  }
+
+  // Expect that Up() will be called. Force the call to fail when fail == true.
+  void ExpectUp(bool fail) {
+    EXPECT_CALL(mock_kernel_, ioctl(_, SIOCSIFFLAGS, _))
+        .WillOnce(Invoke([fail](Unused, Unused, void* argp) {
+          auto* ifr = reinterpret_cast<struct ifreq*>(argp);
+          EXPECT_TRUE(ifr->ifr_flags & IFF_UP);
+          EXPECT_THAT(ifr->ifr_name, StrEq(kDeviceName));
+          if (fail) {
+            return -1;
+          } else {
+            return 0;
+          }
+        }));
+  }
+
+  // Expect that Down() will be called *after* the interface is up. Force the
+  // call to fail when fail == true.
+  void ExpectDown(bool fail) {
+    EXPECT_CALL(mock_kernel_, ioctl(_, SIOCSIFFLAGS, _))
+        .WillOnce(Invoke([fail](Unused, Unused, void* argp) {
+          auto* ifr = reinterpret_cast<struct ifreq*>(argp);
+          EXPECT_FALSE(ifr->ifr_flags & IFF_UP);
+          EXPECT_THAT(ifr->ifr_name, StrEq(kDeviceName));
+          if (fail) {
+            return -1;
+          } else {
+            return 0;
+          }
+        }));
+  }
+
+  MockKernel mock_kernel_;
+  int next_fd_ = 100;
+};
+
+// A TunDevice can be initialized and up
+TEST_F(TunDeviceTest, BasicWorkFlow) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
+  TunDevice tun_device(kDeviceName, 1500, false, &mock_kernel_);
+  EXPECT_TRUE(tun_device.Init());
+  EXPECT_GT(tun_device.GetFileDescriptor(), -1);
+
+  ExpectUp(/* fail = */ false);
+  EXPECT_TRUE(tun_device.Up());
+  ExpectDown(/* fail = */ false);
+}
+
+TEST_F(TunDeviceTest, FailToOpenTunDevice) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
+  EXPECT_CALL(mock_kernel_, open(StrEq("/dev/net/tun"), _))
+      .WillOnce(Return(-1));
+  TunDevice tun_device(kDeviceName, 1500, false, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, FailToCheckFeature) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
+  EXPECT_CALL(mock_kernel_, ioctl(_, TUNGETFEATURES, _)).WillOnce(Return(-1));
+  TunDevice tun_device(kDeviceName, 1500, false, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, TooFewFeature) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ false);
+  EXPECT_CALL(mock_kernel_, ioctl(_, TUNGETFEATURES, _))
+      .WillOnce(Invoke([](Unused, Unused, void* argp) {
+        int* actual_features = reinterpret_cast<int*>(argp);
+        *actual_features = IFF_TUN | IFF_ONE_QUEUE;
+        return 0;
+      }));
+  TunDevice tun_device(kDeviceName, 1500, false, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, FailToSetFlag) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
+  EXPECT_CALL(mock_kernel_, ioctl(_, TUNSETIFF, _)).WillOnce(Return(-1));
+  TunDevice tun_device(kDeviceName, 1500, true, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, FailToPersistDevice) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
+  EXPECT_CALL(mock_kernel_, ioctl(_, TUNSETPERSIST, _)).WillOnce(Return(-1));
+  TunDevice tun_device(kDeviceName, 1500, true, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, FailToOpenSocket) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
+  EXPECT_CALL(mock_kernel_, socket(AF_INET6, _, _)).WillOnce(Return(-1));
+  TunDevice tun_device(kDeviceName, 1500, true, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, FailToSetMtu) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
+  EXPECT_CALL(mock_kernel_, ioctl(_, SIOCSIFMTU, _)).WillOnce(Return(-1));
+  TunDevice tun_device(kDeviceName, 1500, true, &mock_kernel_);
+  EXPECT_FALSE(tun_device.Init());
+  EXPECT_EQ(tun_device.GetFileDescriptor(), -1);
+}
+
+TEST_F(TunDeviceTest, FailToUp) {
+  SetInitExpectations(/* mtu = */ 1500, /* persist = */ true);
+  TunDevice tun_device(kDeviceName, 1500, true, &mock_kernel_);
+  EXPECT_TRUE(tun_device.Init());
+  EXPECT_GT(tun_device.GetFileDescriptor(), -1);
+
+  ExpectUp(/* fail = */ true);
+  EXPECT_FALSE(tun_device.Up());
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/mock_qbone_client.h b/quic/qbone/mock_qbone_client.h
new file mode 100644
index 0000000..37df26d
--- /dev/null
+++ b/quic/qbone/mock_qbone_client.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_MOCK_QBONE_CLIENT_H_
+#define QUICHE_QUIC_QBONE_MOCK_QBONE_CLIENT_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_client_interface.h"
+
+namespace quic {
+
+class MockQboneClient : public QboneClientInterface {
+ public:
+  MOCK_METHOD1(ProcessPacketFromNetwork, void(QuicStringPiece packet));
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_MOCK_QBONE_CLIENT_H_
diff --git a/quic/qbone/mock_qbone_server_session.h b/quic/qbone/mock_qbone_server_session.h
new file mode 100644
index 0000000..652c017
--- /dev/null
+++ b/quic/qbone/mock_qbone_server_session.h
@@ -0,0 +1,36 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_MOCK_QBONE_SERVER_SESSION_H_
+#define QUICHE_QUIC_QBONE_MOCK_QBONE_SERVER_SESSION_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_server_session.h"
+
+namespace quic {
+
+class MockQboneServerSession : public QboneServerSession {
+ public:
+  explicit MockQboneServerSession(QuicConnection* connection)
+      : QboneServerSession(CurrentSupportedVersions(),
+                           connection,
+                           /*owner=*/nullptr,
+                           /*config=*/{},
+                           /*quic_crypto_server_config=*/nullptr,
+                           /*compressed_certs_cache=*/nullptr,
+                           /*writer=*/nullptr,
+                           /*self_ip=*/QuicIpAddress::Loopback6(),
+                           /*client_ip=*/QuicIpAddress::Loopback6(),
+                           /*client_ip_subnet_length=*/0,
+                           /*handler=*/nullptr) {}
+
+  MOCK_METHOD1(SendClientRequest, bool(const QboneClientRequest&));
+
+  MOCK_METHOD1(ProcessPacketFromNetwork, void(QuicStringPiece));
+  MOCK_METHOD1(ProcessPacketFromPeer, void(QuicStringPiece));
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_MOCK_QBONE_SERVER_SESSION_H_
diff --git a/quic/qbone/platform/icmp_packet.cc b/quic/qbone/platform/icmp_packet.cc
new file mode 100644
index 0000000..8ba3916
--- /dev/null
+++ b/quic/qbone/platform/icmp_packet.cc
@@ -0,0 +1,84 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/icmp_packet.h"
+
+#include <netinet/ip6.h>
+#include "net/third_party/quiche/src/quic/platform/api/quic_endian.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/internet_checksum.h"
+
+namespace quic {
+namespace {
+
+constexpr size_t kIPv6AddressSize = sizeof(in6_addr);
+constexpr size_t kIPv6HeaderSize = sizeof(ip6_hdr);
+constexpr size_t kICMPv6HeaderSize = sizeof(icmp6_hdr);
+constexpr size_t kIPv6MinPacketSize = 1280;
+constexpr size_t kIcmpTtl = 64;
+constexpr size_t kICMPv6BodyMaxSize =
+    kIPv6MinPacketSize - kIPv6HeaderSize - kICMPv6HeaderSize;
+
+struct ICMPv6Packet {
+  ip6_hdr ip_header;
+  icmp6_hdr icmp_header;
+  uint8_t body[kICMPv6BodyMaxSize];
+};
+
+// pseudo header as described in RFC 2460 Section 8.1 (excluding addresses)
+struct IPv6PseudoHeader {
+  uint32_t payload_size{};
+  uint8_t zeros[3] = {0, 0, 0};
+  uint8_t next_header = IPPROTO_ICMPV6;
+};
+
+}  // namespace
+
+void CreateIcmpPacket(in6_addr src,
+                      in6_addr dst,
+                      const icmp6_hdr& icmp_header,
+                      QuicStringPiece body,
+                      const std::function<void(QuicStringPiece)>& cb) {
+  const size_t body_size = std::min(body.size(), kICMPv6BodyMaxSize);
+  const size_t payload_size = kICMPv6HeaderSize + body_size;
+
+  ICMPv6Packet icmp_packet{};
+  // Set version to 6.
+  icmp_packet.ip_header.ip6_vfc = 0x6 << 4;
+  // Set the payload size, protocol and TTL.
+  icmp_packet.ip_header.ip6_plen = QuicEndian::HostToNet16(payload_size);
+  icmp_packet.ip_header.ip6_nxt = IPPROTO_ICMPV6;
+  icmp_packet.ip_header.ip6_hops = kIcmpTtl;
+  // Set the source address to the specified self IP.
+  icmp_packet.ip_header.ip6_src = src;
+  icmp_packet.ip_header.ip6_dst = dst;
+
+  icmp_packet.icmp_header = icmp_header;
+  // Per RFC 4443 Section 2.3, set checksum field to 0 prior to computing it
+  icmp_packet.icmp_header.icmp6_cksum = 0;
+
+  IPv6PseudoHeader pseudo_header{};
+  pseudo_header.payload_size = QuicEndian::HostToNet32(payload_size);
+
+  InternetChecksum checksum;
+  // Pseudoheader.
+  checksum.Update(icmp_packet.ip_header.ip6_src.s6_addr, kIPv6AddressSize);
+  checksum.Update(icmp_packet.ip_header.ip6_dst.s6_addr, kIPv6AddressSize);
+  checksum.Update(reinterpret_cast<char*>(&pseudo_header),
+                  sizeof(pseudo_header));
+  // ICMP header.
+  checksum.Update(reinterpret_cast<const char*>(&icmp_packet.icmp_header),
+                  sizeof(icmp_packet.icmp_header));
+  // Body.
+  checksum.Update(body.data(), body_size);
+  icmp_packet.icmp_header.icmp6_cksum = checksum.Value();
+
+  memcpy(icmp_packet.body, body.data(), body_size);
+
+  const char* packet = reinterpret_cast<char*>(&icmp_packet);
+  const size_t packet_size = offsetof(ICMPv6Packet, body) + body_size;
+
+  cb(QuicStringPiece(packet, packet_size));
+}
+
+}  // namespace quic
diff --git a/quic/qbone/platform/icmp_packet.h b/quic/qbone/platform/icmp_packet.h
new file mode 100644
index 0000000..ae440d2
--- /dev/null
+++ b/quic/qbone/platform/icmp_packet.h
@@ -0,0 +1,29 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_ICMP_PACKET_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_ICMP_PACKET_H_
+
+#include <netinet/icmp6.h>
+#include <netinet/in.h>
+
+#include <functional>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_string_piece.h"
+
+namespace quic {
+
+// Creates an ICMPv6 packet, returning a packed string representation of the
+// packet to |cb|. The resulting packet is given to a callback because it's
+// stack allocated inside CreateIcmpPacket.
+void CreateIcmpPacket(in6_addr src,
+                      in6_addr dst,
+                      const icmp6_hdr& icmp_header,
+                      quic::QuicStringPiece body,
+                      const std::function<void(quic::QuicStringPiece)>& cb);
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_ICMP_PACKET_H_
diff --git a/quic/qbone/platform/icmp_packet_test.cc b/quic/qbone/platform/icmp_packet_test.cc
new file mode 100644
index 0000000..1aeabe0
--- /dev/null
+++ b/quic/qbone/platform/icmp_packet_test.cc
@@ -0,0 +1,127 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/icmp_packet.h"
+
+#include <netinet/ip6.h>
+
+#include <cstdint>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h"
+
+namespace quic {
+namespace {
+
+constexpr char kReferenceSourceAddress[] = "fe80:1:2:3:4::1";
+constexpr char kReferenceDestinationAddress[] = "fe80:4:3:2:1::1";
+
+// clang-format off
+constexpr  uint8_t kReferenceICMPMessageBody[] {
+    0xd2, 0x61, 0x29, 0x5b, 0x00, 0x00, 0x00, 0x00,
+    0x0d, 0x59, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+    0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+    0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
+    0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37
+};
+
+constexpr uint8_t kReferenceICMPPacket[] = {
+    // START IPv6 Header
+    // IPv6 with zero TOS and flow label.
+    0x60, 0x00, 0x00, 0x00,
+    // Payload is 64 bytes
+    0x00, 0x40,
+    // Next header is 58
+    0x3a,
+    // Hop limit is 64
+    0x40,
+    // Source address of fe80:1:2:3:4::1
+    0xfe, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03,
+    0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // Destination address of fe80:4:3:2:1::1
+    0xfe, 0x80, 0x00, 0x04, 0x00, 0x03, 0x00, 0x02,
+    0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // END IPv6 Header
+    // START ICMPv6 Header
+    // Echo Request, zero code
+    0x80, 0x00,
+    // Checksum
+    0xec, 0x00,
+    // Identifier
+    0xcb, 0x82,
+    // Sequence Number
+    0x00, 0x01,
+    // END ICMPv6 Header
+    // Message body
+    0xd2, 0x61, 0x29, 0x5b, 0x00, 0x00, 0x00, 0x00,
+    0x0d, 0x59, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
+    0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+    0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+    0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
+    0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+    0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37
+};
+// clang-format on
+
+}  // namespace
+
+TEST(IcmpPacketTest, CreatedPacketMatchesReference) {
+  QuicIpAddress src;
+  ASSERT_TRUE(src.FromString(kReferenceSourceAddress));
+  in6_addr src_addr;
+  memcpy(src_addr.s6_addr, src.ToPackedString().data(), sizeof(in6_addr));
+
+  QuicIpAddress dst;
+  ASSERT_TRUE(dst.FromString(kReferenceDestinationAddress));
+  in6_addr dst_addr;
+  memcpy(dst_addr.s6_addr, dst.ToPackedString().data(), sizeof(in6_addr));
+
+  icmp6_hdr icmp_header{};
+  icmp_header.icmp6_type = ICMP6_ECHO_REQUEST;
+  icmp_header.icmp6_id = 0x82cb;
+  icmp_header.icmp6_seq = 0x0100;
+
+  QuicStringPiece message_body = QuicStringPiece(
+      reinterpret_cast<const char*>(kReferenceICMPMessageBody), 56);
+  QuicStringPiece expected_packet =
+      QuicStringPiece(reinterpret_cast<const char*>(kReferenceICMPPacket), 104);
+  CreateIcmpPacket(src_addr, dst_addr, icmp_header, message_body,
+                   [&expected_packet](QuicStringPiece packet) {
+                     QUIC_LOG(INFO) << QuicTextUtils::HexDump(packet);
+                     ASSERT_EQ(packet, expected_packet);
+                   });
+}
+
+TEST(IcmpPacketTest, NonZeroChecksumIsIgnored) {
+  QuicIpAddress src;
+  ASSERT_TRUE(src.FromString(kReferenceSourceAddress));
+  in6_addr src_addr;
+  memcpy(src_addr.s6_addr, src.ToPackedString().data(), sizeof(in6_addr));
+
+  QuicIpAddress dst;
+  ASSERT_TRUE(dst.FromString(kReferenceDestinationAddress));
+  in6_addr dst_addr;
+  memcpy(dst_addr.s6_addr, dst.ToPackedString().data(), sizeof(in6_addr));
+
+  icmp6_hdr icmp_header{};
+  icmp_header.icmp6_type = ICMP6_ECHO_REQUEST;
+  icmp_header.icmp6_id = 0x82cb;
+  icmp_header.icmp6_seq = 0x0100;
+  // Set the checksum to a bogus value
+  icmp_header.icmp6_cksum = 0x1234;
+
+  QuicStringPiece message_body = QuicStringPiece(
+      reinterpret_cast<const char*>(kReferenceICMPMessageBody), 56);
+  QuicStringPiece expected_packet =
+      QuicStringPiece(reinterpret_cast<const char*>(kReferenceICMPPacket), 104);
+  CreateIcmpPacket(src_addr, dst_addr, icmp_header, message_body,
+                   [&expected_packet](QuicStringPiece packet) {
+                     QUIC_LOG(INFO) << QuicTextUtils::HexDump(packet);
+                     ASSERT_EQ(packet, expected_packet);
+                   });
+}
+
+}  // namespace quic
diff --git a/quic/qbone/platform/internet_checksum.cc b/quic/qbone/platform/internet_checksum.cc
new file mode 100644
index 0000000..9cbe227
--- /dev/null
+++ b/quic/qbone/platform/internet_checksum.cc
@@ -0,0 +1,32 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/internet_checksum.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_endian.h"
+
+namespace quic {
+
+void InternetChecksum::Update(const char* data, size_t size) {
+  const char* current;
+  for (current = data; current + 1 < data + size; current += 2) {
+    accumulator_ += *reinterpret_cast<const uint16_t*>(current);
+  }
+  if (current < data + size) {
+    accumulator_ += *reinterpret_cast<const uint8_t*>(current);
+  }
+}
+
+void InternetChecksum::Update(const uint8_t* data, size_t size) {
+  Update(reinterpret_cast<const char*>(data), size);
+}
+
+uint16_t InternetChecksum::Value() const {
+  uint32_t total = accumulator_;
+  while (total & 0xffff0000u) {
+    total = (total >> 16u) + (total & 0xffffu);
+  }
+  return ~static_cast<uint16_t>(total);
+}
+
+}  // namespace quic
diff --git a/quic/qbone/platform/internet_checksum.h b/quic/qbone/platform/internet_checksum.h
new file mode 100644
index 0000000..85d2415
--- /dev/null
+++ b/quic/qbone/platform/internet_checksum.h
@@ -0,0 +1,32 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_INTERNET_CHECKSUM_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_INTERNET_CHECKSUM_H_
+
+#include <cstddef>
+#include <cstdint>
+
+namespace quic {
+
+// Incrementally compute an Internet header checksum as described in RFC 1071.
+class InternetChecksum {
+ public:
+  // Update the checksum with the specified data.  Note that while the checksum
+  // is commutative, the data has to be supplied in the units of two-byte words.
+  // If there is an extra byte at the end, the function has to be called on it
+  // last.
+  void Update(const char* data, size_t size);
+
+  void Update(const uint8_t* data, size_t size);
+
+  uint16_t Value() const;
+
+ private:
+  uint32_t accumulator_ = 0;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_INTERNET_CHECKSUM_H_
diff --git a/quic/qbone/platform/internet_checksum_test.cc b/quic/qbone/platform/internet_checksum_test.cc
new file mode 100644
index 0000000..a4736e2
--- /dev/null
+++ b/quic/qbone/platform/internet_checksum_test.cc
@@ -0,0 +1,67 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/internet_checksum.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+
+namespace quic {
+namespace {
+
+// From the Numerical Example described in RFC 1071
+// https://tools.ietf.org/html/rfc1071#section-3
+TEST(InternetChecksumTest, MatchesRFC1071Example) {
+  uint8_t data[] = {0x00, 0x01, 0xf2, 0x03, 0xf4, 0xf5, 0xf6, 0xf7};
+
+  InternetChecksum checksum;
+  checksum.Update(data, 8);
+  uint16_t result = checksum.Value();
+  auto* result_bytes = reinterpret_cast<uint8_t*>(&result);
+  ASSERT_EQ(0x22, result_bytes[0]);
+  ASSERT_EQ(0x0d, result_bytes[1]);
+}
+
+// Same as above, except 7 bytes. Should behave as if there was an 8th byte
+// that equals 0.
+TEST(InternetChecksumTest, MatchesRFC1071ExampleWithOddByteCount) {
+  uint8_t data[] = {0x00, 0x01, 0xf2, 0x03, 0xf4, 0xf5, 0xf6};
+
+  InternetChecksum checksum;
+  checksum.Update(data, 7);
+  uint16_t result = checksum.Value();
+  auto* result_bytes = reinterpret_cast<uint8_t*>(&result);
+  ASSERT_EQ(0x23, result_bytes[0]);
+  ASSERT_EQ(0x04, result_bytes[1]);
+}
+
+// From the example described at:
+// http://www.cs.berkeley.edu/~kfall/EE122/lec06/tsld023.htm
+TEST(InternetChecksumTest, MatchesBerkleyExample) {
+  uint8_t data[] = {0xe3, 0x4f, 0x23, 0x96, 0x44, 0x27, 0x99, 0xf3};
+
+  InternetChecksum checksum;
+  checksum.Update(data, 8);
+  uint16_t result = checksum.Value();
+  auto* result_bytes = reinterpret_cast<uint8_t*>(&result);
+  ASSERT_EQ(0x1a, result_bytes[0]);
+  ASSERT_EQ(0xff, result_bytes[1]);
+}
+
+TEST(InternetChecksumTest, ChecksumRequiringMultipleCarriesInLittleEndian) {
+  uint8_t data[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x02, 0x00};
+
+  // Data will accumulate to 0x0002FFFF
+  // Summing lower and upper halves gives 0x00010001
+  // Second sum of lower and upper halves gives 0x0002
+  // One's complement gives 0xfffd, or [0xfd, 0xff] in network byte order
+  InternetChecksum checksum;
+  checksum.Update(data, 8);
+  uint16_t result = checksum.Value();
+  auto* result_bytes = reinterpret_cast<uint8_t*>(&result);
+  EXPECT_EQ(0xfd, result_bytes[0]);
+  EXPECT_EQ(0xff, result_bytes[1]);
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/platform/ip_range.cc b/quic/qbone/platform/ip_range.cc
new file mode 100644
index 0000000..15ebb72
--- /dev/null
+++ b/quic/qbone/platform/ip_range.cc
@@ -0,0 +1,101 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/ip_range.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_endian.h"
+
+namespace quic {
+
+namespace {
+
+constexpr size_t kIPv4Size = 32;
+constexpr size_t kIPv6Size = 128;
+
+QuicIpAddress TruncateToLength(const QuicIpAddress& input,
+                               size_t* prefix_length) {
+  QuicIpAddress output;
+  if (input.IsIPv4()) {
+    if (*prefix_length > kIPv4Size) {
+      *prefix_length = kIPv4Size;
+      return input;
+    }
+    uint32_t raw_address =
+        *reinterpret_cast<const uint32_t*>(input.ToPackedString().data());
+    raw_address = QuicEndian::NetToHost32(raw_address);
+    raw_address &= ~0U << (kIPv4Size - *prefix_length);
+    raw_address = QuicEndian::HostToNet32(raw_address);
+    output.FromPackedString(reinterpret_cast<const char*>(&raw_address),
+                            sizeof(raw_address));
+    return output;
+  }
+  if (input.IsIPv6()) {
+    if (*prefix_length > kIPv6Size) {
+      *prefix_length = kIPv6Size;
+      return input;
+    }
+    uint64_t raw_address[2];
+    memcpy(raw_address, input.ToPackedString().data(), sizeof(raw_address));
+    // raw_address[0] holds higher 8 bytes in big endian and raw_address[1]
+    // holds lower 8 bytes. Converting each to little endian for us to mask bits
+    // out.
+    // The endianess between raw_address[0] and raw_address[1] is handled
+    // explicitly by handling lower and higher bytes separately.
+    raw_address[0] = QuicEndian::NetToHost64(raw_address[0]);
+    raw_address[1] = QuicEndian::NetToHost64(raw_address[1]);
+    if (*prefix_length <= kIPv6Size / 2) {
+      raw_address[0] &= ~uint64_t{0} << (kIPv6Size / 2 - *prefix_length);
+      raw_address[1] = 0;
+    } else {
+      raw_address[1] &= ~uint64_t{0} << (kIPv6Size - *prefix_length);
+    }
+    raw_address[0] = QuicEndian::HostToNet64(raw_address[0]);
+    raw_address[1] = QuicEndian::HostToNet64(raw_address[1]);
+    output.FromPackedString(reinterpret_cast<const char*>(raw_address),
+                            sizeof(raw_address));
+    return output;
+  }
+  return output;
+}
+
+}  // namespace
+
+IpRange::IpRange(const QuicIpAddress& prefix, size_t prefix_length)
+    : prefix_(prefix), prefix_length_(prefix_length) {
+  prefix_ = TruncateToLength(prefix_, &prefix_length_);
+}
+
+bool IpRange::operator==(IpRange other) const {
+  return prefix_ == other.prefix_ && prefix_length_ == other.prefix_length_;
+}
+
+bool IpRange::operator!=(IpRange other) const {
+  return !(*this == other);
+}
+
+bool IpRange::FromString(const string& range) {
+  size_t slash_pos = range.find('/');
+  if (slash_pos == string::npos) {
+    return false;
+  }
+  QuicIpAddress prefix;
+  bool success = prefix.FromString(range.substr(0, slash_pos));
+  if (!success) {
+    return false;
+  }
+  uint64_t num_processed = 0;
+  size_t prefix_length = std::stoi(range.substr(slash_pos + 1), &num_processed);
+  if (num_processed + 1 + slash_pos != range.length()) {
+    return false;
+  }
+  prefix_ = TruncateToLength(prefix, &prefix_length);
+  prefix_length_ = prefix_length;
+  return true;
+}
+
+QuicIpAddress IpRange::FirstAddressInRange() {
+  return prefix();
+}
+
+}  // namespace quic
diff --git a/quic/qbone/platform/ip_range.h b/quic/qbone/platform/ip_range.h
new file mode 100644
index 0000000..545c32c
--- /dev/null
+++ b/quic/qbone/platform/ip_range.h
@@ -0,0 +1,60 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_IP_RANGE_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_IP_RANGE_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_str_cat.h"
+
+namespace quic {
+
+class IpRange {
+ public:
+  // Default constructor to have an uninitialized IpRange.
+  IpRange() : prefix_length_(0) {}
+
+  // prefix will be automatically truncated to prefix_length, so that any bit
+  // after prefix_length are zero.
+  IpRange(const QuicIpAddress& prefix, size_t prefix_length);
+
+  bool operator==(IpRange other) const;
+  bool operator!=(IpRange other) const;
+
+  // Parses range that looks like "10.0.0.1/8". Tailing bits will be set to zero
+  // after prefix_length. Return false if the parsing failed.
+  bool FromString(const string& range);
+
+  // Returns the string representation of this object.
+  string ToString() const {
+    if (IsInitialized()) {
+      return absl::StrCat(prefix_.ToString(), "/", prefix_length_);
+    }
+    return "(uninitialized)";
+  }
+
+  // Whether this object is initialized.
+  bool IsInitialized() const { return prefix_.IsInitialized(); }
+
+  // Returns the first available IP address in this IpRange. The resulting
+  // address will be uninitialized if there is no available address.
+  QuicIpAddress FirstAddressInRange();
+
+  // The address family of this IpRange.
+  IpAddressFamily address_family() const { return prefix_.address_family(); }
+
+  // The subnet's prefix address.
+  QuicIpAddress prefix() const { return prefix_; }
+
+  // The subnet's prefix length.
+  size_t prefix_length() const { return prefix_length_; }
+
+ private:
+  QuicIpAddress prefix_;
+  size_t prefix_length_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_IP_RANGE_H_
diff --git a/quic/qbone/platform/ip_range_test.cc b/quic/qbone/platform/ip_range_test.cc
new file mode 100644
index 0000000..bac5c96
--- /dev/null
+++ b/quic/qbone/platform/ip_range_test.cc
@@ -0,0 +1,65 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/ip_range.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+
+namespace quic {
+namespace {
+
+TEST(IpRangeTest, TruncateWorksIPv4) {
+  QuicIpAddress before_truncate;
+  before_truncate.FromString("255.255.255.255");
+  EXPECT_EQ("128.0.0.0/1", IpRange(before_truncate, 1).ToString());
+  EXPECT_EQ("192.0.0.0/2", IpRange(before_truncate, 2).ToString());
+  EXPECT_EQ("255.224.0.0/11", IpRange(before_truncate, 11).ToString());
+  EXPECT_EQ("255.255.255.224/27", IpRange(before_truncate, 27).ToString());
+  EXPECT_EQ("255.255.255.254/31", IpRange(before_truncate, 31).ToString());
+  EXPECT_EQ("255.255.255.255/32", IpRange(before_truncate, 32).ToString());
+  EXPECT_EQ("255.255.255.255/32", IpRange(before_truncate, 33).ToString());
+}
+
+TEST(IpRangeTest, TruncateWorksIPv6) {
+  QuicIpAddress before_truncate;
+  before_truncate.FromString("ffff:ffff:ffff:ffff:f903::5");
+  EXPECT_EQ("fe00::/7", IpRange(before_truncate, 7).ToString());
+  EXPECT_EQ("ffff:ffff:ffff::/48", IpRange(before_truncate, 48).ToString());
+  EXPECT_EQ("ffff:ffff:ffff:ffff::/64",
+            IpRange(before_truncate, 64).ToString());
+  EXPECT_EQ("ffff:ffff:ffff:ffff:8000::/65",
+            IpRange(before_truncate, 65).ToString());
+  EXPECT_EQ("ffff:ffff:ffff:ffff:f903::4/127",
+            IpRange(before_truncate, 127).ToString());
+}
+
+TEST(IpRangeTest, FromStringWorksIPv4) {
+  IpRange range;
+  ASSERT_TRUE(range.FromString("127.0.3.249/26"));
+  EXPECT_EQ("127.0.3.192/26", range.ToString());
+}
+
+TEST(IpRangeTest, FromStringWorksIPv6) {
+  IpRange range;
+  ASSERT_TRUE(range.FromString("ff01:8f21:77f9::/33"));
+  EXPECT_EQ("ff01:8f21::/33", range.ToString());
+}
+
+TEST(IpRangeTest, FirstAddressWorksIPv6) {
+  IpRange range;
+  ASSERT_TRUE(range.FromString("ffff:ffff::/64"));
+  QuicIpAddress first_address = range.FirstAddressInRange();
+  EXPECT_EQ("ffff:ffff::", first_address.ToString());
+}
+
+TEST(IpRangeTest, FirstAddressWorksIPv4) {
+  IpRange range;
+  ASSERT_TRUE(range.FromString("10.0.0.0/24"));
+  QuicIpAddress first_address = range.FirstAddressInRange();
+  EXPECT_EQ("10.0.0.0", first_address.ToString());
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/platform/kernel_interface.h b/quic/qbone/platform/kernel_interface.h
new file mode 100644
index 0000000..c96b6e6
--- /dev/null
+++ b/quic/qbone/platform/kernel_interface.h
@@ -0,0 +1,169 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_KERNEL_INTERFACE_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_KERNEL_INTERFACE_H_
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <type_traits>
+#include <utility>
+
+namespace quic {
+
+// A wrapper for making syscalls to the kernel, so that syscalls can be
+// mocked during testing.
+class KernelInterface {
+ public:
+  virtual ~KernelInterface() {}
+  virtual int bind(int fd, const struct sockaddr* addr, socklen_t addr_len) = 0;
+  virtual int close(int fd) = 0;
+  virtual int ioctl(int fd, int request, void* argp) = 0;
+  virtual int open(const char* pathname, int flags) = 0;
+  virtual ssize_t read(int fd, void* buf, size_t count) = 0;
+  virtual ssize_t recvfrom(int sockfd,
+                           void* buf,
+                           size_t len,
+                           int flags,
+                           struct sockaddr* src_addr,
+                           socklen_t* addrlen) = 0;
+  virtual ssize_t sendmsg(int sockfd, const struct msghdr* msg, int flags) = 0;
+  virtual ssize_t sendto(int sockfd,
+                         const void* buf,
+                         size_t len,
+                         int flags,
+                         const struct sockaddr* dest_addr,
+                         socklen_t addrlen) = 0;
+  virtual int socket(int domain, int type, int protocol) = 0;
+  virtual int setsockopt(int fd,
+                         int level,
+                         int optname,
+                         const void* optval,
+                         socklen_t optlen) = 0;
+  virtual ssize_t write(int fd, const void* buf, size_t count) = 0;
+};
+
+// It is unfortunate to have R here, but std::result_of cannot be used.
+template <typename F, typename R, typename... Params>
+auto SyscallRetryOnError(R r, F f, Params&&... params)
+    -> decltype(f(std::forward<Params>(params)...)) {
+  static_assert(
+      std::is_same<decltype(f(std::forward<Params>(params)...)), R>::value,
+      "Return type does not match");
+  decltype(f(std::forward<Params>(params)...)) result;
+  do {
+    result = f(std::forward<Params>(params)...);
+  } while (result == r && errno == EINTR);
+  return result;
+}
+
+template <typename F, typename... Params>
+auto SyscallRetry(F f, Params&&... params)
+    -> decltype(f(std::forward<Params>(params)...)) {
+  return SyscallRetryOnError(-1, f, std::forward<Params>(params)...);
+}
+
+template <typename Runner>
+class ParametrizedKernel final : public KernelInterface {
+ public:
+  static_assert(std::is_trivially_destructible<Runner>::value,
+                "Runner is used as static, must be trivially destructible");
+
+  ~ParametrizedKernel() override {}
+
+  int bind(int fd, const struct sockaddr* addr, socklen_t addr_len) override {
+    static Runner syscall("bind");
+    return syscall.Retry(&::bind, fd, addr, addr_len);
+  }
+  int close(int fd) override {
+    static Runner syscall("close");
+    return syscall.Retry(&::close, fd);
+  }
+  int ioctl(int fd, int request, void* argp) override {
+    static Runner syscall("ioctl");
+    return syscall.Retry(&::ioctl, fd, request, argp);
+  }
+  int open(const char* pathname, int flags) override {
+    static Runner syscall("open");
+    return syscall.Retry(&::open, pathname, flags);
+  }
+  ssize_t read(int fd, void* buf, size_t count) override {
+    static Runner syscall("read");
+    return syscall.Run(&::read, fd, buf, count);
+  }
+  ssize_t recvfrom(int sockfd,
+                   void* buf,
+                   size_t len,
+                   int flags,
+                   struct sockaddr* src_addr,
+                   socklen_t* addrlen) override {
+    static Runner syscall("recvfrom");
+    return syscall.RetryOnError(&::recvfrom, static_cast<ssize_t>(-1), sockfd,
+                                buf, len, flags, src_addr, addrlen);
+  }
+  ssize_t sendmsg(int sockfd, const struct msghdr* msg, int flags) override {
+    static Runner syscall("sendmsg");
+    return syscall.RetryOnError(&::sendmsg, static_cast<ssize_t>(-1), sockfd,
+                                msg, flags);
+  }
+  ssize_t sendto(int sockfd,
+                 const void* buf,
+                 size_t len,
+                 int flags,
+                 const struct sockaddr* dest_addr,
+                 socklen_t addrlen) override {
+    static Runner syscall("sendto");
+    return syscall.RetryOnError(&::sendto, static_cast<ssize_t>(-1), sockfd,
+                                buf, len, flags, dest_addr, addrlen);
+  }
+  int socket(int domain, int type, int protocol) override {
+    static Runner syscall("socket");
+    return syscall.Retry(&::socket, domain, type, protocol);
+  }
+  int setsockopt(int fd,
+                 int level,
+                 int optname,
+                 const void* optval,
+                 socklen_t optlen) override {
+    static Runner syscall("setsockopt");
+    return syscall.Retry(&::setsockopt, fd, level, optname, optval, optlen);
+  }
+  ssize_t write(int fd, const void* buf, size_t count) override {
+    static Runner syscall("write");
+    return syscall.Run(&::write, fd, buf, count);
+  }
+};
+
+class DefaultKernelRunner {
+ public:
+  explicit DefaultKernelRunner(const char* name) {}
+
+  template <typename F, typename R, typename... Params>
+  static auto RetryOnError(F f, R r, Params&&... params)
+      -> decltype(f(std::forward<Params>(params)...)) {
+    return SyscallRetryOnError(r, f, std::forward<Params>(params)...);
+  }
+
+  template <typename F, typename... Params>
+  static auto Retry(F f, Params&&... params)
+      -> decltype(f(std::forward<Params>(params)...)) {
+    return SyscallRetry(f, std::forward<Params>(params)...);
+  }
+
+  template <typename F, typename... Params>
+  static auto Run(F f, Params&&... params)
+      -> decltype(f(std::forward<Params>(params)...)) {
+    return f(std::forward<Params>(params)...);
+  }
+};
+
+using Kernel = ParametrizedKernel<DefaultKernelRunner>;
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_KERNEL_INTERFACE_H_
diff --git a/quic/qbone/platform/mock_kernel.h b/quic/qbone/platform/mock_kernel.h
new file mode 100644
index 0000000..c01aad1
--- /dev/null
+++ b/quic/qbone/platform/mock_kernel.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_MOCK_KERNEL_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_MOCK_KERNEL_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/kernel_interface.h"
+
+namespace quic {
+
+class MockKernel : public KernelInterface {
+ public:
+  MockKernel() {}
+
+  MOCK_METHOD3(bind,
+               int(int fd, const struct sockaddr* addr, socklen_t addr_len));
+  MOCK_METHOD1(close, int(int fd));
+  MOCK_METHOD3(ioctl, int(int fd, int request, void* argp));
+  MOCK_METHOD2(open, int(const char* pathname, int flags));
+  MOCK_METHOD3(read, ssize_t(int fd, void* buf, size_t count));
+  MOCK_METHOD6(recvfrom,
+               ssize_t(int sockfd,
+                       void* buf,
+                       size_t len,
+                       int flags,
+                       struct sockaddr* src_addr,
+                       socklen_t* addrlen));
+  MOCK_METHOD3(sendmsg,
+               ssize_t(int sockfd, const struct msghdr* msg, int flags));
+  MOCK_METHOD6(sendto,
+               ssize_t(int sockfd,
+                       const void* buf,
+                       size_t len,
+                       int flags,
+                       const struct sockaddr* dest_addr,
+                       socklen_t addrlen));
+  MOCK_METHOD3(socket, int(int domain, int type, int protocol));
+  MOCK_METHOD5(setsockopt, int(int, int, int, const void*, socklen_t));
+  MOCK_METHOD3(write, ssize_t(int fd, const void* buf, size_t count));
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_MOCK_KERNEL_H_
diff --git a/quic/qbone/platform/mock_netlink.h b/quic/qbone/platform/mock_netlink.h
new file mode 100644
index 0000000..5c1e7bc
--- /dev/null
+++ b/quic/qbone/platform/mock_netlink.h
@@ -0,0 +1,46 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_MOCK_NETLINK_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_MOCK_NETLINK_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/netlink_interface.h"
+
+namespace quic {
+
+class MockNetlink : public NetlinkInterface {
+ public:
+  MOCK_METHOD2(GetLinkInfo, bool(const string&, LinkInfo*));
+
+  MOCK_METHOD4(GetAddresses,
+               bool(int, uint8_t, std::vector<AddressInfo>*, int*));
+
+  MOCK_METHOD7(ChangeLocalAddress,
+               bool(uint32_t,
+                    Verb,
+                    const QuicIpAddress&,
+                    uint8_t,
+                    uint8_t,
+                    uint8_t,
+                    const std::vector<struct rtattr*>&));
+
+  MOCK_METHOD1(GetRouteInfo, bool(std::vector<RoutingRule>*));
+
+  MOCK_METHOD6(
+      ChangeRoute,
+      bool(Verb, uint32_t, const IpRange&, uint8_t, QuicIpAddress, int32_t));
+
+  MOCK_METHOD1(GetRuleInfo, bool(std::vector<IpRule>*));
+
+  MOCK_METHOD3(ChangeRule, bool(Verb, uint32_t, IpRange));
+
+  MOCK_METHOD2(Send, bool(struct iovec*, size_t));
+
+  MOCK_METHOD2(Recv, bool(uint32_t, NetlinkParserInterface*));
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_MOCK_NETLINK_H_
diff --git a/quic/qbone/platform/netlink.cc b/quic/qbone/platform/netlink.cc
new file mode 100644
index 0000000..1a4270e
--- /dev/null
+++ b/quic/qbone/platform/netlink.cc
@@ -0,0 +1,828 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/netlink.h"
+
+#include <linux/fib_rules.h>
+
+#include "net/third_party/quiche/src/quic/core/crypto/quic_random.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_fallthrough.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_str_cat.h"
+#include "net/quic/platform/impl/quic_ip_address_impl.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/rtnetlink_message.h"
+
+namespace quic {
+
+Netlink::Netlink(KernelInterface* kernel) : kernel_(kernel) {
+  seq_ = QuicRandom::GetInstance()->RandUint64();
+}
+
+Netlink::~Netlink() {
+  CloseSocket();
+}
+
+void Netlink::ResetRecvBuf(size_t size) {
+  if (size != 0) {
+    recvbuf_ = QuicMakeUnique<char[]>(size);
+  } else {
+    recvbuf_ = nullptr;
+  }
+  recvbuf_length_ = size;
+}
+
+bool Netlink::OpenSocket() {
+  if (socket_fd_ >= 0) {
+    return true;
+  }
+
+  socket_fd_ = kernel_->socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
+
+  if (socket_fd_ < 0) {
+    QUIC_PLOG(ERROR) << "can't open netlink socket";
+    return false;
+  }
+
+  QUIC_LOG(INFO) << "Opened a new netlink socket fd = " << socket_fd_;
+
+  // bind a local address to the socket
+  sockaddr_nl myaddr;
+  memset(&myaddr, 0, sizeof(myaddr));
+  myaddr.nl_family = AF_NETLINK;
+  if (kernel_->bind(socket_fd_, reinterpret_cast<struct sockaddr*>(&myaddr),
+                    sizeof(myaddr)) < 0) {
+    QUIC_LOG(INFO) << "can't bind address to socket";
+    CloseSocket();
+    return false;
+  }
+
+  return true;
+}
+
+void Netlink::CloseSocket() {
+  if (socket_fd_ >= 0) {
+    QUIC_LOG(INFO) << "Closing netlink socket fd = " << socket_fd_;
+    kernel_->close(socket_fd_);
+  }
+  ResetRecvBuf(0);
+  socket_fd_ = -1;
+}
+
+namespace {
+
+class LinkInfoParser : public NetlinkParserInterface {
+ public:
+  LinkInfoParser(string interface_name, Netlink::LinkInfo* link_info)
+      : interface_name_(std::move(interface_name)), link_info_(link_info) {}
+
+  void Run(struct nlmsghdr* netlink_message) override {
+    if (netlink_message->nlmsg_type != RTM_NEWLINK) {
+      QUIC_LOG(INFO) << QuicStrCat(
+          "Unexpected nlmsg_type: ", netlink_message->nlmsg_type,
+          " expected: ", RTM_NEWLINK);
+      return;
+    }
+
+    struct ifinfomsg* interface_info =
+        reinterpret_cast<struct ifinfomsg*>(NLMSG_DATA(netlink_message));
+
+    // make sure interface_info is what we asked for.
+    if (interface_info->ifi_family != AF_UNSPEC) {
+      QUIC_LOG(INFO) << QuicStrCat(
+          "Unexpected ifi_family: ", interface_info->ifi_family,
+          " expected: ", AF_UNSPEC);
+      return;
+    }
+
+    char hardware_address[kHwAddrSize];
+    size_t hardware_address_length = 0;
+    char broadcast_address[kHwAddrSize];
+    size_t broadcast_address_length = 0;
+    string name;
+
+    // loop through the attributes
+    struct rtattr* rta;
+    int payload_length = IFLA_PAYLOAD(netlink_message);
+    for (rta = IFLA_RTA(interface_info); RTA_OK(rta, payload_length);
+         rta = RTA_NEXT(rta, payload_length)) {
+      int attribute_length;
+      switch (rta->rta_type) {
+        case IFLA_ADDRESS: {
+          attribute_length = RTA_PAYLOAD(rta);
+          if (attribute_length > kHwAddrSize) {
+            QUIC_VLOG(2) << "IFLA_ADDRESS too long: " << attribute_length;
+            break;
+          }
+          memmove(hardware_address, RTA_DATA(rta), attribute_length);
+          hardware_address_length = attribute_length;
+          break;
+        }
+        case IFLA_BROADCAST: {
+          attribute_length = RTA_PAYLOAD(rta);
+          if (attribute_length > kHwAddrSize) {
+            QUIC_VLOG(2) << "IFLA_BROADCAST too long: " << attribute_length;
+            break;
+          }
+          memmove(broadcast_address, RTA_DATA(rta), attribute_length);
+          broadcast_address_length = attribute_length;
+          break;
+        }
+        case IFLA_IFNAME: {
+          name =
+              string(reinterpret_cast<char*>(RTA_DATA(rta)), RTA_PAYLOAD(rta));
+          // The name maybe a 0 terminated c string.
+          name = name.substr(0, name.find('\0'));
+          break;
+        }
+      }
+    }
+
+    QUIC_VLOG(2) << "interface name: " << name
+                 << ", index: " << interface_info->ifi_index;
+
+    if (name == interface_name_) {
+      link_info_->index = interface_info->ifi_index;
+      link_info_->type = interface_info->ifi_type;
+      link_info_->hardware_address_length = hardware_address_length;
+      if (hardware_address_length > 0) {
+        memmove(&link_info_->hardware_address, hardware_address,
+                hardware_address_length);
+      }
+      link_info_->broadcast_address_length = broadcast_address_length;
+      if (broadcast_address_length > 0) {
+        memmove(&link_info_->broadcast_address, broadcast_address,
+                broadcast_address_length);
+      }
+      found_link_ = true;
+    }
+  }
+
+  bool found_link() { return found_link_; }
+
+ private:
+  const string interface_name_;
+  Netlink::LinkInfo* const link_info_;
+  bool found_link_ = false;
+};
+
+}  // namespace
+
+bool Netlink::GetLinkInfo(const string& interface_name, LinkInfo* link_info) {
+  auto message = LinkMessage::New(RtnetlinkMessage::Operation::GET,
+                                  NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST,
+                                  seq_, getpid(), nullptr);
+
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed.";
+    return false;
+  }
+
+  // Pass the parser to the receive routine. It may be called multiple times
+  // since there may be multiple reply packets each with multiple reply
+  // messages.
+  LinkInfoParser parser(interface_name, link_info);
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "recv failed.";
+    return false;
+  }
+
+  return parser.found_link();
+}
+
+namespace {
+
+class LocalAddressParser : public NetlinkParserInterface {
+ public:
+  LocalAddressParser(int interface_index,
+                     uint8_t unwanted_flags,
+                     std::vector<Netlink::AddressInfo>* local_addresses,
+                     int* num_ipv6_nodad_dadfailed_addresses)
+      : interface_index_(interface_index),
+        unwanted_flags_(unwanted_flags),
+        local_addresses_(local_addresses),
+        num_ipv6_nodad_dadfailed_addresses_(
+            num_ipv6_nodad_dadfailed_addresses) {}
+
+  void Run(struct nlmsghdr* netlink_message) override {
+    // each nlmsg contains a header and multiple address attributes.
+    if (netlink_message->nlmsg_type != RTM_NEWADDR) {
+      QUIC_LOG(INFO) << "Unexpected nlmsg_type: " << netlink_message->nlmsg_type
+                     << " expected: " << RTM_NEWADDR;
+      return;
+    }
+
+    struct ifaddrmsg* interface_address =
+        reinterpret_cast<struct ifaddrmsg*>(NLMSG_DATA(netlink_message));
+
+    // Make sure this is for an address family we're interested in.
+    if (interface_address->ifa_family != AF_INET &&
+        interface_address->ifa_family != AF_INET6) {
+      QUIC_VLOG(2) << QuicStrCat("uninteresting ifa family: ",
+                                 interface_address->ifa_family);
+      return;
+    }
+
+    // Keep track of addresses with both 'nodad' and 'dadfailed', this really
+    // should't be possible and is likely a kernel bug.
+    if (num_ipv6_nodad_dadfailed_addresses_ != nullptr &&
+        (interface_address->ifa_flags & IFA_F_NODAD) &&
+        (interface_address->ifa_flags & IFA_F_DADFAILED)) {
+      ++(*num_ipv6_nodad_dadfailed_addresses_);
+    }
+
+    uint8_t unwanted_flags = interface_address->ifa_flags & unwanted_flags_;
+    if (unwanted_flags != 0) {
+      QUIC_VLOG(2) << QuicStrCat("unwanted ifa flags: ", unwanted_flags);
+      return;
+    }
+
+    // loop through the attributes
+    struct rtattr* rta;
+    int payload_length = IFA_PAYLOAD(netlink_message);
+    Netlink::AddressInfo address_info;
+    for (rta = IFA_RTA(interface_address); RTA_OK(rta, payload_length);
+         rta = RTA_NEXT(rta, payload_length)) {
+      // There's quite a lot of confusion in Linux over the use of IFA_LOCAL and
+      // IFA_ADDRESS (source and destination address). For broadcast links, such
+      // as Ethernet, they are identical (see <linux/if_addr.h>), but the kernel
+      // sometimes uses only one or the other. We'll return both so that the
+      // caller can decide which to use.
+      if (rta->rta_type != IFA_LOCAL && rta->rta_type != IFA_ADDRESS) {
+        QUIC_VLOG(2) << "Ignoring uninteresting rta_type: " << rta->rta_type;
+        continue;
+      }
+
+      switch (interface_address->ifa_family) {
+        case AF_INET:
+          QUIC_FALLTHROUGH_INTENDED;
+        case AF_INET6:
+          // QuicIpAddress knows how to parse ip from raw bytes as long as they
+          // are in network byte order.
+          if (RTA_PAYLOAD(rta) == sizeof(struct in_addr) ||
+              RTA_PAYLOAD(rta) == sizeof(struct in6_addr)) {
+            auto* raw_ip = reinterpret_cast<char*>(RTA_DATA(rta));
+            if (rta->rta_type == IFA_LOCAL) {
+              address_info.local_address.FromPackedString(raw_ip,
+                                                          RTA_PAYLOAD(rta));
+            } else {
+              address_info.interface_address.FromPackedString(raw_ip,
+                                                              RTA_PAYLOAD(rta));
+            }
+          }
+          break;
+        default:
+          QUIC_LOG(ERROR) << QuicStrCat("Unknown address family: ",
+                                        interface_address->ifa_family);
+      }
+    }
+
+    QUIC_VLOG(2) << "local_address: " << address_info.local_address.ToString()
+                 << " interface_address: "
+                 << address_info.interface_address.ToString()
+                 << " index: " << interface_address->ifa_index;
+    if (interface_address->ifa_index != interface_index_) {
+      return;
+    }
+
+    address_info.prefix_length = interface_address->ifa_prefixlen;
+    address_info.scope = interface_address->ifa_scope;
+    if (address_info.local_address.IsInitialized() ||
+        address_info.interface_address.IsInitialized()) {
+      local_addresses_->push_back(address_info);
+    }
+  }
+
+ private:
+  const int interface_index_;
+  const uint8_t unwanted_flags_;
+  std::vector<Netlink::AddressInfo>* const local_addresses_;
+  int* const num_ipv6_nodad_dadfailed_addresses_;
+};
+
+}  // namespace
+
+bool Netlink::GetAddresses(int interface_index,
+                           uint8_t unwanted_flags,
+                           std::vector<AddressInfo>* addresses,
+                           int* num_ipv6_nodad_dadfailed_addresses) {
+  // the message doesn't contain the index, we'll have to do the filtering while
+  // parsing the reply. This is because NLM_F_MATCH, which only returns entries
+  // that matches the request criteria, is not yet implemented (see man 3
+  // netlink).
+  auto message = AddressMessage::New(RtnetlinkMessage::Operation::GET,
+                                     NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST,
+                                     seq_, getpid(), nullptr);
+
+  // the send routine returns the socket to listen on.
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed.";
+    return false;
+  }
+
+  addresses->clear();
+  if (num_ipv6_nodad_dadfailed_addresses != nullptr) {
+    *num_ipv6_nodad_dadfailed_addresses = 0;
+  }
+
+  LocalAddressParser parser(interface_index, unwanted_flags, addresses,
+                            num_ipv6_nodad_dadfailed_addresses);
+  // Pass the parser to the receive routine. It may be called multiple times
+  // since there may be multiple reply packets each with multiple reply
+  // messages.
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "recv failed";
+    return false;
+  }
+  return true;
+}
+
+namespace {
+
+class UnknownParser : public NetlinkParserInterface {
+ public:
+  void Run(struct nlmsghdr* netlink_message) override {
+    QUIC_LOG(INFO) << "nlmsg reply type: " << netlink_message->nlmsg_type;
+  }
+};
+
+}  // namespace
+
+bool Netlink::ChangeLocalAddress(
+    uint32_t interface_index,
+    Verb verb,
+    const QuicIpAddress& address,
+    uint8_t prefix_length,
+    uint8_t ifa_flags,
+    uint8_t ifa_scope,
+    const std::vector<struct rtattr*>& additional_attributes) {
+  if (verb == Verb::kReplace) {
+    return false;
+  }
+  auto operation = verb == Verb::kAdd ? RtnetlinkMessage::Operation::NEW
+                                      : RtnetlinkMessage::Operation::DEL;
+  uint8_t address_family;
+  if (address.address_family() == IpAddressFamily::IP_V4) {
+    address_family = AF_INET;
+  } else if (address.address_family() == IpAddressFamily::IP_V6) {
+    address_family = AF_INET6;
+  } else {
+    return false;
+  }
+
+  struct ifaddrmsg address_header = {address_family, prefix_length, ifa_flags,
+                                     ifa_scope, interface_index};
+
+  auto message = AddressMessage::New(operation, NLM_F_REQUEST | NLM_F_ACK, seq_,
+                                     getpid(), &address_header);
+
+  for (const auto& attribute : additional_attributes) {
+    if (attribute->rta_type == IFA_LOCAL) {
+      continue;
+    }
+    message.AppendAttribute(attribute->rta_type, RTA_DATA(attribute),
+                            RTA_PAYLOAD(attribute));
+  }
+
+  message.AppendAttribute(IFA_LOCAL, address.ToPackedString().c_str(),
+                          address.ToPackedString().size());
+
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed";
+    return false;
+  }
+
+  UnknownParser parser;
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "receive failed.";
+    return false;
+  }
+  return true;
+}
+
+namespace {
+
+class RoutingRuleParser : public NetlinkParserInterface {
+ public:
+  explicit RoutingRuleParser(std::vector<Netlink::RoutingRule>* routing_rules)
+      : routing_rules_(routing_rules) {}
+
+  void Run(struct nlmsghdr* netlink_message) override {
+    if (netlink_message->nlmsg_type != RTM_NEWROUTE) {
+      QUIC_LOG(WARNING) << QuicStrCat(
+          "Unexpected nlmsg_type: ", netlink_message->nlmsg_type,
+          " expected: ", RTM_NEWROUTE);
+      return;
+    }
+
+    auto* route = reinterpret_cast<struct rtmsg*>(NLMSG_DATA(netlink_message));
+    int payload_length = RTM_PAYLOAD(netlink_message);
+
+    if (route->rtm_family != AF_INET && route->rtm_family != AF_INET6) {
+      QUIC_VLOG(2) << QuicStrCat("Uninteresting family: ", route->rtm_family);
+      return;
+    }
+
+    Netlink::RoutingRule rule;
+    rule.scope = route->rtm_scope;
+    rule.table = route->rtm_table;
+
+    struct rtattr* rta;
+    for (rta = RTM_RTA(route); RTA_OK(rta, payload_length);
+         rta = RTA_NEXT(rta, payload_length)) {
+      switch (rta->rta_type) {
+        case RTA_TABLE: {
+          rule.table = *reinterpret_cast<uint32_t*>(RTA_DATA(rta));
+          break;
+        }
+        case RTA_DST: {
+          QuicIpAddress destination;
+          destination.FromPackedString(reinterpret_cast<char*> RTA_DATA(rta),
+                                       RTA_PAYLOAD(rta));
+          rule.destination_subnet = IpRange(destination, route->rtm_dst_len);
+          break;
+        }
+        case RTA_PREFSRC: {
+          QuicIpAddress preferred_source;
+          rule.preferred_source.FromPackedString(
+              reinterpret_cast<char*> RTA_DATA(rta), RTA_PAYLOAD(rta));
+          break;
+        }
+        case RTA_OIF: {
+          rule.out_interface = *reinterpret_cast<int*>(RTA_DATA(rta));
+          break;
+        }
+        default: {
+          QUIC_VLOG(2) << QuicStrCat("Uninteresting attribute: ",
+                                     rta->rta_type);
+        }
+      }
+    }
+    routing_rules_->push_back(rule);
+  }
+
+ private:
+  std::vector<Netlink::RoutingRule>* routing_rules_;
+};
+
+}  // namespace
+
+bool Netlink::GetRouteInfo(std::vector<Netlink::RoutingRule>* routing_rules) {
+  rtmsg route_message{};
+  // Only manipulate main routing table.
+  route_message.rtm_table = RT_TABLE_MAIN;
+
+  auto message = RouteMessage::New(RtnetlinkMessage::Operation::GET,
+                                   NLM_F_REQUEST | NLM_F_ROOT | NLM_F_MATCH,
+                                   seq_, getpid(), &route_message);
+
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed";
+    return false;
+  }
+
+  RoutingRuleParser parser(routing_rules);
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "recv failed";
+    return false;
+  }
+
+  return true;
+}
+
+bool Netlink::ChangeRoute(Netlink::Verb verb,
+                          uint32_t table,
+                          const IpRange& destination_subnet,
+                          uint8_t scope,
+                          QuicIpAddress preferred_source,
+                          int32_t interface_index) {
+  if (!destination_subnet.prefix().IsInitialized()) {
+    return false;
+  }
+  if (destination_subnet.address_family() != IpAddressFamily::IP_V4 &&
+      destination_subnet.address_family() != IpAddressFamily::IP_V6) {
+    return false;
+  }
+  if (preferred_source.IsInitialized() &&
+      preferred_source.address_family() !=
+          destination_subnet.address_family()) {
+    return false;
+  }
+
+  RtnetlinkMessage::Operation operation;
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ACK;
+  switch (verb) {
+    case Verb::kAdd:
+      operation = RtnetlinkMessage::Operation::NEW;
+      // Setting NLM_F_EXCL so that an existing entry for this subnet will fail
+      // the request. NLM_F_CREATE is necessary to indicate this is trying to
+      // create a new entry - simply having RTM_NEWROUTE is not enough even the
+      // name suggests so.
+      flags |= NLM_F_EXCL | NLM_F_CREATE;
+      break;
+    case Verb::kRemove:
+      operation = RtnetlinkMessage::Operation::DEL;
+      break;
+    case Verb::kReplace:
+      operation = RtnetlinkMessage::Operation::NEW;
+      // Setting NLM_F_REPLACE to tell the kernel that existing entry for this
+      // subnet should be replaced.
+      flags |= NLM_F_REPLACE | NLM_F_CREATE;
+      break;
+  }
+
+  struct rtmsg route_message;
+  memset(&route_message, 0, sizeof(route_message));
+  route_message.rtm_family =
+      destination_subnet.address_family() == IpAddressFamily::IP_V4 ? AF_INET
+                                                                    : AF_INET6;
+  // rtm_dst_len and rtm_src_len are actually the subnet prefix lengths. Poor
+  // naming.
+  route_message.rtm_dst_len = destination_subnet.prefix_length();
+  // 0 means no source subnet for this rule.
+  route_message.rtm_src_len = 0;
+  // Only program the main table. Other tables are intended for the kernel to
+  // manage.
+  route_message.rtm_table = RT_TABLE_MAIN;
+  // Use RTPROT_UNSPEC to match all the different protocol. Rules added by
+  // kernel have RTPROT_KERNEL. Rules added by the root user have RTPROT_STATIC
+  // instead.
+  route_message.rtm_protocol =
+      verb == Verb::kRemove ? RTPROT_UNSPEC : RTPROT_STATIC;
+  route_message.rtm_scope = scope;
+  // Only add unicast routing rule.
+  route_message.rtm_type = RTN_UNICAST;
+  auto message =
+      RouteMessage::New(operation, flags, seq_, getpid(), &route_message);
+
+  message.AppendAttribute(RTA_TABLE, &table, sizeof(table));
+
+  // RTA_OIF is the target interface for this rule.
+  message.AppendAttribute(RTA_OIF, &interface_index, sizeof(interface_index));
+  // The actual destination subnet must be truncated of all the tailing zeros.
+  message.AppendAttribute(
+      RTA_DST,
+      reinterpret_cast<const void*>(
+          destination_subnet.prefix().ToPackedString().c_str()),
+      destination_subnet.prefix().ToPackedString().size());
+  // This is the source address to use in the IP packet should this routing rule
+  // is used.
+  if (preferred_source.IsInitialized()) {
+    message.AppendAttribute(RTA_PREFSRC,
+                            reinterpret_cast<const void*>(
+                                preferred_source.ToPackedString().c_str()),
+                            preferred_source.ToPackedString().size());
+  }
+
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed";
+    return false;
+  }
+
+  UnknownParser parser;
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "receive failed.";
+    return false;
+  }
+  return true;
+}
+
+namespace {
+
+class IpRuleParser : public NetlinkParserInterface {
+ public:
+  explicit IpRuleParser(std::vector<Netlink::IpRule>* ip_rules)
+      : ip_rules_(ip_rules) {}
+
+  void Run(struct nlmsghdr* netlink_message) override {
+    if (netlink_message->nlmsg_type != RTM_NEWRULE) {
+      QUIC_LOG(WARNING) << QuicStrCat(
+          "Unexpected nlmsg_type: ", netlink_message->nlmsg_type,
+          " expected: ", RTM_NEWRULE);
+      return;
+    }
+
+    auto* rule = reinterpret_cast<rtmsg*>(NLMSG_DATA(netlink_message));
+    int payload_length = RTM_PAYLOAD(netlink_message);
+
+    if (rule->rtm_family != AF_INET6) {
+      QUIC_LOG(ERROR) << QuicStrCat("Unexpected family: ", rule->rtm_family);
+      return;
+    }
+
+    Netlink::IpRule ip_rule;
+    ip_rule.table = rule->rtm_table;
+
+    struct rtattr* rta;
+    for (rta = RTM_RTA(rule); RTA_OK(rta, payload_length);
+         rta = RTA_NEXT(rta, payload_length)) {
+      switch (rta->rta_type) {
+        case RTA_TABLE: {
+          ip_rule.table = *reinterpret_cast<uint32_t*>(RTA_DATA(rta));
+          break;
+        }
+        case RTA_SRC: {
+          QuicIpAddress src_addr;
+          src_addr.FromPackedString(reinterpret_cast<char*>(RTA_DATA(rta)),
+                                    RTA_PAYLOAD(rta));
+          IpRange src_range(src_addr, rule->rtm_src_len);
+          ip_rule.source_range = src_range;
+          break;
+        }
+        default: {
+          QUIC_VLOG(2) << QuicStrCat("Uninteresting attribute: ",
+                                     rta->rta_type);
+        }
+      }
+    }
+    ip_rules_->emplace_back(ip_rule);
+  }
+
+ private:
+  std::vector<Netlink::IpRule>* ip_rules_;
+};
+
+}  // namespace
+
+bool Netlink::GetRuleInfo(std::vector<Netlink::IpRule>* ip_rules) {
+  rtmsg rule_message{};
+  rule_message.rtm_family = AF_INET6;
+
+  auto message = RuleMessage::New(RtnetlinkMessage::Operation::GET,
+                                  NLM_F_REQUEST | NLM_F_DUMP, seq_, getpid(),
+                                  &rule_message);
+
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed";
+    return false;
+  }
+
+  IpRuleParser parser(ip_rules);
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "receive failed.";
+    return false;
+  }
+  return true;
+}
+
+bool Netlink::ChangeRule(Verb verb, uint32_t table, IpRange source_range) {
+  RtnetlinkMessage::Operation operation;
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ACK;
+
+  rtmsg rule_message{};
+  rule_message.rtm_family = AF_INET6;
+  rule_message.rtm_protocol = RTPROT_STATIC;
+  rule_message.rtm_scope = RT_SCOPE_UNIVERSE;
+  rule_message.rtm_table = RT_TABLE_UNSPEC;
+
+  rule_message.rtm_flags |= FIB_RULE_FIND_SADDR;
+
+  switch (verb) {
+    case Verb::kAdd:
+      if (!source_range.IsInitialized()) {
+        QUIC_LOG(ERROR) << "Source range must be initialized.";
+        return false;
+      }
+      operation = RtnetlinkMessage::Operation::NEW;
+      flags |= NLM_F_EXCL | NLM_F_CREATE;
+      rule_message.rtm_type = FRA_DST;
+      rule_message.rtm_src_len = source_range.prefix_length();
+      break;
+    case Verb::kRemove:
+      operation = RtnetlinkMessage::Operation::DEL;
+      break;
+    case Verb::kReplace:
+      QUIC_LOG(ERROR) << "Unsupported verb: kReplace";
+      return false;
+  }
+  auto message =
+      RuleMessage::New(operation, flags, seq_, getpid(), &rule_message);
+
+  message.AppendAttribute(RTA_TABLE, &table, sizeof(table));
+
+  if (source_range.IsInitialized()) {
+    std::string packed_src = source_range.prefix().ToPackedString();
+    message.AppendAttribute(RTA_SRC,
+                            reinterpret_cast<const void*>(packed_src.c_str()),
+                            packed_src.size());
+  }
+
+  if (!Send(message.BuildIoVec().get(), message.IoVecSize())) {
+    QUIC_LOG(ERROR) << "send failed";
+    return false;
+  }
+
+  UnknownParser parser;
+  if (!Recv(seq_++, &parser)) {
+    QUIC_LOG(ERROR) << "receive failed.";
+    return false;
+  }
+  return true;
+}
+
+bool Netlink::Send(struct iovec* iov, size_t iovlen) {
+  if (!OpenSocket()) {
+    QUIC_LOG(ERROR) << "can't open socket";
+    return false;
+  }
+
+  // an address for communicating with the kernel netlink code
+  sockaddr_nl netlink_address;
+  memset(&netlink_address, 0, sizeof(netlink_address));
+  netlink_address.nl_family = AF_NETLINK;
+  netlink_address.nl_pid = 0;     // destination is kernel
+  netlink_address.nl_groups = 0;  // no multicast
+
+  struct msghdr msg = {
+      &netlink_address, sizeof(netlink_address), iov, iovlen, nullptr, 0, 0};
+
+  if (kernel_->sendmsg(socket_fd_, &msg, 0) < 0) {
+    QUIC_LOG(ERROR) << "sendmsg failed";
+    CloseSocket();
+    return false;
+  }
+
+  return true;
+}
+
+bool Netlink::Recv(uint32_t seq, NetlinkParserInterface* parser) {
+  sockaddr_nl netlink_address;
+
+  // replies can span multiple packets
+  for (;;) {
+    socklen_t address_length = sizeof(netlink_address);
+
+    // First, call recvfrom with buffer size of 0 and MSG_PEEK | MSG_TRUNC set
+    // so that we know the size of the incoming packet before actually receiving
+    // it.
+    int next_packet_size = kernel_->recvfrom(
+        socket_fd_, recvbuf_.get(), /* len = */ 0, MSG_PEEK | MSG_TRUNC,
+        reinterpret_cast<struct sockaddr*>(&netlink_address), &address_length);
+    if (next_packet_size < 0) {
+      QUIC_LOG(ERROR)
+          << "error recvfrom with MSG_PEEK | MSG_TRUNC to get packet length.";
+      CloseSocket();
+      return false;
+    }
+    QUIC_VLOG(3) << "netlink packet size: " << next_packet_size;
+    if (next_packet_size > recvbuf_length_) {
+      QUIC_VLOG(2) << "resizing recvbuf to " << next_packet_size;
+      ResetRecvBuf(next_packet_size);
+    }
+
+    // Get the packet for real.
+    memset(recvbuf_.get(), 0, recvbuf_length_);
+    int len = kernel_->recvfrom(
+        socket_fd_, recvbuf_.get(), recvbuf_length_, /* flags = */ 0,
+        reinterpret_cast<struct sockaddr*>(&netlink_address), &address_length);
+    QUIC_VLOG(3) << "recvfrom returned: " << len;
+    if (len < 0) {
+      QUIC_LOG(INFO) << "can't receive netlink packet";
+      CloseSocket();
+      return false;
+    }
+
+    // there may be multiple nlmsg's in each reply packet
+    struct nlmsghdr* netlink_message;
+    for (netlink_message = reinterpret_cast<struct nlmsghdr*>(recvbuf_.get());
+         NLMSG_OK(netlink_message, len);
+         netlink_message = NLMSG_NEXT(netlink_message, len)) {
+      QUIC_VLOG(3) << "netlink_message->nlmsg_type = "
+                   << netlink_message->nlmsg_type;
+      // make sure this is to us
+      if (netlink_message->nlmsg_seq != seq) {
+        QUIC_LOG(INFO) << "netlink_message not meant for us."
+                       << " seq: " << seq
+                       << " nlmsg_seq: " << netlink_message->nlmsg_seq;
+        continue;
+      }
+
+      // done with this whole reply (not just this particular packet)
+      if (netlink_message->nlmsg_type == NLMSG_DONE) {
+        return true;
+      }
+      if (netlink_message->nlmsg_type == NLMSG_ERROR) {
+        struct nlmsgerr* err =
+            reinterpret_cast<struct nlmsgerr*>(NLMSG_DATA(netlink_message));
+        if (netlink_message->nlmsg_len <
+            NLMSG_LENGTH(sizeof(struct nlmsgerr))) {
+          QUIC_LOG(INFO) << "netlink_message ERROR truncated";
+        } else {
+          // an ACK
+          if (err->error == 0) {
+            QUIC_VLOG(3) << "Netlink sent an ACK";
+            return true;
+          }
+          QUIC_LOG(INFO) << "netlink_message ERROR: " << err->error;
+        }
+        return false;
+      }
+
+      parser->Run(netlink_message);
+    }
+  }
+}
+
+}  // namespace quic
diff --git a/quic/qbone/platform/netlink.h b/quic/qbone/platform/netlink.h
new file mode 100644
index 0000000..591da0f
--- /dev/null
+++ b/quic/qbone/platform/netlink.h
@@ -0,0 +1,142 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_NETLINK_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_NETLINK_H_
+
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/ip_range.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/kernel_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/netlink_interface.h"
+
+namespace quic {
+
+// A wrapper class to provide convenient methods of manipulating IP address and
+// routing table using netlink (man 7 netlink) socket. More specifically,
+// rtnetlink is used (man 7 rtnetlink).
+//
+// This class is not thread safe, but thread compatible, as long as callers can
+// make sure Send and Recv pairs are executed in sequence for a particular
+// query.
+class Netlink : public NetlinkInterface {
+ public:
+  explicit Netlink(KernelInterface* kernel);
+  ~Netlink() override;
+
+  // Gets the link information for the interface referred by the given
+  // interface_name.
+  //
+  // This is a synchronous communication. That should not be a problem since the
+  // kernel should answer immediately.
+  bool GetLinkInfo(const string& interface_name, LinkInfo* link_info) override;
+
+  // Gets the addresses for the given interface referred by the given
+  // interface_index.
+  //
+  // This is a synchronous communication. This should not be a problem since the
+  // kernel should answer immediately.
+  bool GetAddresses(int interface_index,
+                    uint8_t unwanted_flags,
+                    std::vector<AddressInfo>* addresses,
+                    int* num_ipv6_nodad_dadfailed_addresses) override;
+
+  // Performs the given verb that modifies local addresses on the given
+  // interface_index.
+  //
+  // additional_attributes are RTAs (man 7 rtnelink) that will be sent together
+  // with the netlink message. Note that rta_len in each RTA is used to decide
+  // the length of the payload. The caller is responsible for making sure
+  // payload bytes are accessible after the RTA header.
+  bool ChangeLocalAddress(
+      uint32_t interface_index,
+      Verb verb,
+      const QuicIpAddress& address,
+      uint8_t prefix_length,
+      uint8_t ifa_flags,
+      uint8_t ifa_scope,
+      const std::vector<struct rtattr*>& additional_attributes) override;
+
+  // Gets the list of routing rules from the main routing table (RT_TABLE_MAIN),
+  // which is programmable.
+  //
+  // This is a synchronous communication. This should not be a problem since the
+  // kernel should answer immediately.
+  bool GetRouteInfo(std::vector<RoutingRule>* routing_rules) override;
+
+  // Performs the given Verb on the matching rule in the main routing table
+  // (RT_TABLE_MAIN).
+  //
+  // preferred_source can be !IsInitialized(), in which case it will be omitted.
+  //
+  // For Verb::kRemove, rule matching is done by (destination_subnet, scope,
+  // preferred_source, interface_index). Return true if a matching rule is
+  // found. interface_index can be 0 for wilecard.
+  //
+  // For Verb::kAdd, rule matching is done by destination_subnet. If a rule for
+  // the given destination_subnet already exists, nothing will happen and false
+  // is returned.
+  //
+  // For Verb::kReplace, rule matching is done by destination_subnet. If no
+  // matching rule is found, a new entry will be created.
+  bool ChangeRoute(Netlink::Verb verb,
+                   uint32_t table,
+                   const IpRange& destination_subnet,
+                   uint8_t scope,
+                   QuicIpAddress preferred_source,
+                   int32_t interface_index) override;
+
+  // Returns the set of all rules in the routing policy database.
+  bool GetRuleInfo(std::vector<Netlink::IpRule>* ip_rules) override;
+
+  // Performs the give verb on the matching rule in the routing policy database.
+  // When deleting a rule, the |source_range| may be unspecified, in which case
+  // the lowest priority rule from |table| will be removed. When adding a rule,
+  // the |source_address| must be specified.
+  bool ChangeRule(Verb verb, uint32_t table, IpRange source_range) override;
+
+  // Sends a netlink message to the kernel. iov and iovlen represents an array
+  // of struct iovec to be fed into sendmsg. The caller needs to make sure the
+  // message conform to what's expected by NLMSG_* macros.
+  //
+  // This can be useful if more flexibility is needed than the provided
+  // convenient methods can provide.
+  bool Send(struct iovec* iov, size_t iovlen) override;
+
+  // Receives a netlink message from the kernel.
+  // parser will be called on the caller's stack.
+  //
+  // This can be useful if more flexibility is needed than the provided
+  // convenient methods can provide.
+  // TODO(b/69412655): vectorize this.
+  bool Recv(uint32_t seq, NetlinkParserInterface* parser) override;
+
+ private:
+  // Reset the size of recvbuf_ to size. If size is 0, recvbuf_ will be nullptr.
+  void ResetRecvBuf(size_t size);
+
+  // Opens a netlink socket if not already opened.
+  bool OpenSocket();
+
+  // Closes the opened netlink socket. Noop if no netlink socket is opened.
+  void CloseSocket();
+
+  KernelInterface* kernel_;
+  int socket_fd_ = -1;
+  std::unique_ptr<char[]> recvbuf_ = nullptr;
+  size_t recvbuf_length_ = 0;
+  uint32_t seq_;  // next msg sequence number
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_NETLINK_H_
diff --git a/quic/qbone/platform/netlink_interface.h b/quic/qbone/platform/netlink_interface.h
new file mode 100644
index 0000000..447c8b2
--- /dev/null
+++ b/quic/qbone/platform/netlink_interface.h
@@ -0,0 +1,146 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_NETLINK_INTERFACE_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_NETLINK_INTERFACE_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/ip_range.h"
+
+namespace quic {
+
+constexpr int kHwAddrSize = 6;
+
+class NetlinkParserInterface {
+ public:
+  virtual ~NetlinkParserInterface() {}
+  virtual void Run(struct nlmsghdr* netlink_message) = 0;
+};
+
+// An interface providing convenience methods for manipulating IP address and
+// routing table using netlink (man 7 netlink) socket.
+class NetlinkInterface {
+ public:
+  virtual ~NetlinkInterface() = default;
+
+  // Link information returned from GetLinkInfo.
+  struct LinkInfo {
+    int index;
+    uint8_t type;
+    uint8_t hardware_address[kHwAddrSize];
+    uint8_t broadcast_address[kHwAddrSize];
+    size_t hardware_address_length;   // 0 if no hardware address found
+    size_t broadcast_address_length;  // 0 if no broadcast address found
+  };
+
+  // Gets the link information for the interface referred by the given
+  // interface_name.
+  virtual bool GetLinkInfo(const string& interface_name,
+                           LinkInfo* link_info) = 0;
+
+  // Address information reported back from GetAddresses.
+  struct AddressInfo {
+    QuicIpAddress local_address;
+    QuicIpAddress interface_address;
+    uint8_t prefix_length = 0;
+    uint8_t scope = 0;
+  };
+
+  // Gets the addresses for the given interface referred by the given
+  // interface_index.
+  virtual bool GetAddresses(int interface_index,
+                            uint8_t unwanted_flags,
+                            std::vector<AddressInfo>* addresses,
+                            int* num_ipv6_nodad_dadfailed_addresses) = 0;
+
+  enum class Verb {
+    kAdd,
+    kRemove,
+    kReplace,
+  };
+
+  // Performs the given verb that modifies local addresses on the given
+  // interface_index.
+  //
+  // additional_attributes are RTAs (man 7 rtnelink) that will be sent together
+  // with the netlink message. Note that rta_len in each RTA is used to decide
+  // the length of the payload. The caller is responsible for making sure
+  // payload bytes are accessible after the RTA header.
+  virtual bool ChangeLocalAddress(
+      uint32_t interface_index,
+      Verb verb,
+      const QuicIpAddress& address,
+      uint8_t prefix_length,
+      uint8_t ifa_flags,
+      uint8_t ifa_scope,
+      const std::vector<struct rtattr*>& additional_attributes) = 0;
+
+  // Routing rule reported back from GetRouteInfo.
+  struct RoutingRule {
+    uint32_t table;
+    IpRange destination_subnet;
+    QuicIpAddress preferred_source;
+    uint8_t scope;
+    int out_interface;
+  };
+
+  struct IpRule {
+    uint32_t table;
+    IpRange source_range;
+  };
+
+  // Gets the list of routing rules from the main routing table (RT_TABLE_MAIN),
+  // which is programmable.
+  virtual bool GetRouteInfo(std::vector<RoutingRule>* routing_rules) = 0;
+
+  // Performs the given Verb on the matching rule in the main routing table
+  // (RT_TABLE_MAIN).
+  //
+  // preferred_source can be !IsInitialized(), in which case it will be omitted.
+  //
+  // For Verb::kRemove, rule matching is done by (destination_subnet, scope,
+  // preferred_source, interface_index). Return true if a matching rule is
+  // found. interface_index can be 0 for wilecard.
+  //
+  // For Verb::kAdd, rule matching is done by destination_subnet. If a rule for
+  // the given destination_subnet already exists, nothing will happen and false
+  // is returned.
+  //
+  // For Verb::kReplace, rule matching is done by destination_subnet. If no
+  // matching rule is found, a new entry will be created.
+  virtual bool ChangeRoute(Verb verb,
+                           uint32_t table,
+                           const IpRange& destination_subnet,
+                           uint8_t scope,
+                           QuicIpAddress preferred_source,
+                           int32_t interface_index) = 0;
+
+  // Returns the set of all rules in the routing policy database.
+  virtual bool GetRuleInfo(std::vector<IpRule>* ip_rules) = 0;
+
+  // Performs the give verb on the matching rule in the routing policy database.
+  // When deleting a rule, the |source_range| may be unspecified, in which case
+  // the lowest priority rule from |table| will be removed. When adding a rule,
+  // the |source_address| must be specified.
+  virtual bool ChangeRule(Verb verb, uint32_t table, IpRange source_range) = 0;
+
+  // Sends a netlink message to the kernel. iov and iovlen represents an array
+  // of struct iovec to be fed into sendmsg. The caller needs to make sure the
+  // message conform to what's expected by NLMSG_* macros.
+  //
+  // This can be useful if more flexibility is needed than the provided
+  // convenient methods can provide.
+  virtual bool Send(struct iovec* iov, size_t iovlen) = 0;
+
+  // Receives a netlink message from the kernel.
+  // parser will be called on the caller's stack.
+  //
+  // This can be useful if more flexibility is needed than the provided
+  // convenient methods can provide.
+  virtual bool Recv(uint32_t seq, NetlinkParserInterface* parser) = 0;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_NETLINK_INTERFACE_H_
diff --git a/quic/qbone/platform/netlink_test.cc b/quic/qbone/platform/netlink_test.cc
new file mode 100644
index 0000000..024e0fb
--- /dev/null
+++ b/quic/qbone/platform/netlink_test.cc
@@ -0,0 +1,763 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/netlink.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_containers.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/mock_kernel.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+
+namespace quic {
+namespace {
+
+using ::testing::_;
+using ::testing::Contains;
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::Unused;
+
+const int kSocketFd = 101;
+
+class NetlinkTest : public QuicTest {
+ protected:
+  NetlinkTest() {
+    ON_CALL(mock_kernel_, socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE))
+        .WillByDefault(Invoke([this](Unused, Unused, Unused) {
+          EXPECT_CALL(mock_kernel_, close(kSocketFd)).WillOnce(Return(0));
+          return kSocketFd;
+        }));
+  }
+
+  void ExpectNetlinkPacket(
+      uint16_t type,
+      uint16_t flags,
+      const std::function<ssize_t(void* buf, size_t len, int seq)>&
+          recv_callback,
+      const std::function<void(const void* buf, size_t len)>& send_callback =
+          nullptr) {
+    static int seq = -1;
+    InSequence s;
+
+    EXPECT_CALL(mock_kernel_, sendmsg(kSocketFd, _, _))
+        .WillOnce(Invoke([this, type, flags, send_callback](
+                             Unused, const struct msghdr* msg, int) {
+          EXPECT_EQ(sizeof(struct sockaddr_nl), msg->msg_namelen);
+          auto* nl_addr =
+              reinterpret_cast<const struct sockaddr_nl*>(msg->msg_name);
+          EXPECT_EQ(AF_NETLINK, nl_addr->nl_family);
+          EXPECT_EQ(0, nl_addr->nl_pid);
+          EXPECT_EQ(0, nl_addr->nl_groups);
+
+          EXPECT_GE(msg->msg_iovlen, 1);
+          EXPECT_GE(msg->msg_iov[0].iov_len, sizeof(struct nlmsghdr));
+
+          string buf;
+          for (int i = 0; i < msg->msg_iovlen; i++) {
+            buf.append(string(reinterpret_cast<char*>(msg->msg_iov[i].iov_base),
+                              msg->msg_iov[i].iov_len));
+          }
+
+          auto* netlink_message =
+              reinterpret_cast<const struct nlmsghdr*>(buf.c_str());
+          EXPECT_EQ(type, netlink_message->nlmsg_type);
+          EXPECT_EQ(flags, netlink_message->nlmsg_flags);
+          EXPECT_GE(buf.size(), netlink_message->nlmsg_len);
+
+          if (send_callback != nullptr) {
+            send_callback(buf.c_str(), buf.size());
+          }
+
+          CHECK_EQ(seq, -1);
+          seq = netlink_message->nlmsg_seq;
+          return buf.size();
+        }));
+
+    EXPECT_CALL(mock_kernel_,
+                recvfrom(kSocketFd, _, 0, MSG_PEEK | MSG_TRUNC, _, _))
+        .WillOnce(Invoke([this, recv_callback](Unused, Unused, Unused, Unused,
+                                               struct sockaddr* src_addr,
+                                               socklen_t* addrlen) {
+          auto* nl_addr = reinterpret_cast<struct sockaddr_nl*>(src_addr);
+          nl_addr->nl_family = AF_NETLINK;
+          nl_addr->nl_pid = 0;     // from kernel
+          nl_addr->nl_groups = 0;  // no multicast
+
+          int ret = recv_callback(reply_packet_, sizeof(reply_packet_), seq);
+          CHECK_LE(ret, sizeof(reply_packet_));
+          return ret;
+        }));
+
+    EXPECT_CALL(mock_kernel_, recvfrom(kSocketFd, _, _, _, _, _))
+        .WillOnce(Invoke([recv_callback](Unused, void* buf, size_t len, Unused,
+                                         struct sockaddr* src_addr,
+                                         socklen_t* addrlen) {
+          auto* nl_addr = reinterpret_cast<struct sockaddr_nl*>(src_addr);
+          nl_addr->nl_family = AF_NETLINK;
+          nl_addr->nl_pid = 0;     // from kernel
+          nl_addr->nl_groups = 0;  // no multicast
+
+          int ret = recv_callback(buf, len, seq);
+          EXPECT_GE(len, ret);
+          seq = -1;
+          return ret;
+        }));
+  }
+
+  char reply_packet_[4096];
+  MockKernel mock_kernel_;
+};
+
+void AddRTA(struct nlmsghdr* netlink_message,
+            uint16_t type,
+            const void* data,
+            size_t len) {
+  auto* next_header_ptr = reinterpret_cast<char*>(netlink_message) +
+                          NLMSG_ALIGN(netlink_message->nlmsg_len);
+
+  auto* rta = reinterpret_cast<struct rtattr*>(next_header_ptr);
+  rta->rta_type = type;
+  rta->rta_len = RTA_LENGTH(len);
+  memcpy(RTA_DATA(rta), data, len);
+
+  netlink_message->nlmsg_len =
+      NLMSG_ALIGN(netlink_message->nlmsg_len) + RTA_LENGTH(len);
+}
+
+void CreateIfinfomsg(struct nlmsghdr* netlink_message,
+                     const string& interface_name,
+                     uint16_t type,
+                     int index,
+                     unsigned int flags,
+                     unsigned int change,
+                     uint8_t address[],
+                     int address_len,
+                     uint8_t broadcast[],
+                     int broadcast_len) {
+  auto* interface_info =
+      reinterpret_cast<struct ifinfomsg*>(NLMSG_DATA(netlink_message));
+  interface_info->ifi_family = AF_UNSPEC;
+  interface_info->ifi_type = type;
+  interface_info->ifi_index = index;
+  interface_info->ifi_flags = flags;
+  interface_info->ifi_change = change;
+  netlink_message->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifinfomsg));
+
+  // Add address
+  AddRTA(netlink_message, IFLA_ADDRESS, address, address_len);
+
+  // Add broadcast address
+  AddRTA(netlink_message, IFLA_BROADCAST, broadcast, broadcast_len);
+
+  // Add name
+  AddRTA(netlink_message, IFLA_IFNAME, interface_name.c_str(),
+         interface_name.size());
+}
+
+struct nlmsghdr* CreateNetlinkMessage(void* buf,  // NOLINT
+                                      struct nlmsghdr* previous_netlink_message,
+                                      uint16_t type,
+                                      int seq) {
+  auto* next_header_ptr = reinterpret_cast<char*>(buf);
+  if (previous_netlink_message != nullptr) {
+    next_header_ptr = reinterpret_cast<char*>(previous_netlink_message) +
+                      NLMSG_ALIGN(previous_netlink_message->nlmsg_len);
+  }
+  auto* netlink_message = reinterpret_cast<nlmsghdr*>(next_header_ptr);
+  netlink_message->nlmsg_len = NLMSG_LENGTH(0);
+  netlink_message->nlmsg_type = type;
+  netlink_message->nlmsg_flags = NLM_F_MULTI;
+  netlink_message->nlmsg_pid = 0;  // from the kernel
+  netlink_message->nlmsg_seq = seq;
+
+  return netlink_message;
+}
+
+void CreateIfaddrmsg(struct nlmsghdr* nlm,
+                     int interface_index,
+                     unsigned char prefixlen,
+                     unsigned char flags,
+                     unsigned char scope,
+                     QuicIpAddress ip) {
+  CHECK(ip.IsInitialized());
+  unsigned char family;
+  switch (ip.address_family()) {
+    case IpAddressFamily::IP_V4:
+      family = AF_INET;
+      break;
+    case IpAddressFamily::IP_V6:
+      family = AF_INET6;
+      break;
+    default:
+      QUIC_BUG << absl::StrCat("unexpected address family: ",
+                               ip.address_family());
+      family = AF_UNSPEC;
+  }
+  auto* msg = reinterpret_cast<struct ifaddrmsg*>(NLMSG_DATA(nlm));
+  msg->ifa_family = family;
+  msg->ifa_prefixlen = prefixlen;
+  msg->ifa_flags = flags;
+  msg->ifa_scope = scope;
+  msg->ifa_index = interface_index;
+  nlm->nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));
+
+  // Add local address
+  AddRTA(nlm, IFA_LOCAL, ip.ToPackedString().c_str(),
+         ip.ToPackedString().size());
+}
+
+void CreateRtmsg(struct nlmsghdr* nlm,
+                 unsigned char family,
+                 unsigned char destination_length,
+                 unsigned char source_length,
+                 unsigned char tos,
+                 unsigned char table,
+                 unsigned char protocol,
+                 unsigned char scope,
+                 unsigned char type,
+                 unsigned int flags,
+                 QuicIpAddress destination,
+                 int interface_index) {
+  auto* msg = reinterpret_cast<struct rtmsg*>(NLMSG_DATA(nlm));
+  msg->rtm_family = family;
+  msg->rtm_dst_len = destination_length;
+  msg->rtm_src_len = source_length;
+  msg->rtm_tos = tos;
+  msg->rtm_table = table;
+  msg->rtm_protocol = protocol;
+  msg->rtm_scope = scope;
+  msg->rtm_type = type;
+  msg->rtm_flags = flags;
+  nlm->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
+
+  // Add destination
+  AddRTA(nlm, RTA_DST, destination.ToPackedString().c_str(),
+         destination.ToPackedString().size());
+
+  // Add egress interface
+  AddRTA(nlm, RTA_OIF, &interface_index, sizeof(interface_index));
+}
+
+TEST_F(NetlinkTest, GetLinkInfoWorks) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  uint8_t hwaddr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
+  uint8_t bcaddr[] = {'c', 'b', 'a', 'f', 'e', 'd'};
+
+  ExpectNetlinkPacket(
+      RTM_GETLINK, NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST,
+      [this, &hwaddr, &bcaddr](void* buf, size_t len, int seq) {
+        int ret = 0;
+
+        struct nlmsghdr* netlink_message =
+            CreateNetlinkMessage(buf, nullptr, RTM_NEWLINK, seq);
+        CreateIfinfomsg(netlink_message, "tun0", /* type = */ 1,
+                        /* index = */ 7,
+                        /* flags = */ 0,
+                        /* change = */ 0xFFFFFFFF, hwaddr, 6, bcaddr, 6);
+        ret += NLMSG_ALIGN(netlink_message->nlmsg_len);
+
+        netlink_message =
+            CreateNetlinkMessage(buf, netlink_message, NLMSG_DONE, seq);
+        ret += NLMSG_ALIGN(netlink_message->nlmsg_len);
+
+        return ret;
+      });
+
+  Netlink::LinkInfo link_info;
+  EXPECT_TRUE(netlink->GetLinkInfo("tun0", &link_info));
+
+  EXPECT_EQ(7, link_info.index);
+  EXPECT_EQ(1, link_info.type);
+
+  for (int i = 0; i < link_info.hardware_address_length; ++i) {
+    EXPECT_EQ(hwaddr[i], link_info.hardware_address[i]);
+  }
+  for (int i = 0; i < link_info.broadcast_address_length; ++i) {
+    EXPECT_EQ(bcaddr[i], link_info.broadcast_address[i]);
+  }
+}
+
+TEST_F(NetlinkTest, GetAddressesWorks) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicUnorderedSet<std::string> addresses = {QuicIpAddress::Any4().ToString(),
+                                             QuicIpAddress::Any6().ToString()};
+
+  ExpectNetlinkPacket(
+      RTM_GETADDR, NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST,
+      [this, &addresses](void* buf, size_t len, int seq) {
+        int ret = 0;
+
+        struct nlmsghdr* nlm = nullptr;
+
+        for (const auto& address : addresses) {
+          QuicIpAddress ip;
+          ip.FromString(address);
+          nlm = CreateNetlinkMessage(buf, nlm, RTM_NEWADDR, seq);
+          CreateIfaddrmsg(nlm, /* interface_index = */ 7, /* prefixlen = */ 24,
+                          /* flags = */ 0, /* scope = */ RT_SCOPE_UNIVERSE, ip);
+
+          ret += NLMSG_ALIGN(nlm->nlmsg_len);
+        }
+
+        // Create IPs with unwanted flags.
+        {
+          QuicIpAddress ip;
+          ip.FromString("10.0.0.1");
+          nlm = CreateNetlinkMessage(buf, nlm, RTM_NEWADDR, seq);
+          CreateIfaddrmsg(nlm, /* interface_index = */ 7, /* prefixlen = */ 16,
+                          /* flags = */ IFA_F_OPTIMISTIC, /* scope = */
+                          RT_SCOPE_UNIVERSE, ip);
+
+          ret += NLMSG_ALIGN(nlm->nlmsg_len);
+
+          ip.FromString("10.0.0.2");
+          nlm = CreateNetlinkMessage(buf, nlm, RTM_NEWADDR, seq);
+          CreateIfaddrmsg(nlm, /* interface_index = */ 7, /* prefixlen = */ 16,
+                          /* flags = */ IFA_F_TENTATIVE, /* scope = */
+                          RT_SCOPE_UNIVERSE, ip);
+
+          ret += NLMSG_ALIGN(nlm->nlmsg_len);
+        }
+
+        nlm = CreateNetlinkMessage(buf, nlm, NLMSG_DONE, seq);
+        ret += NLMSG_ALIGN(nlm->nlmsg_len);
+
+        return ret;
+      });
+
+  std::vector<Netlink::AddressInfo> reported_addresses;
+  int num_ipv6_nodad_dadfailed_addresses = 0;
+  EXPECT_TRUE(netlink->GetAddresses(7, IFA_F_TENTATIVE | IFA_F_OPTIMISTIC,
+                                    &reported_addresses,
+                                    &num_ipv6_nodad_dadfailed_addresses));
+
+  for (const auto& reported_address : reported_addresses) {
+    EXPECT_TRUE(reported_address.local_address.IsInitialized());
+    EXPECT_FALSE(reported_address.interface_address.IsInitialized());
+    EXPECT_THAT(addresses, Contains(reported_address.local_address.ToString()));
+    addresses.erase(reported_address.local_address.ToString());
+
+    EXPECT_EQ(24, reported_address.prefix_length);
+  }
+
+  EXPECT_TRUE(addresses.empty());
+}
+
+TEST_F(NetlinkTest, ChangeLocalAddressAdd) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicIpAddress ip = QuicIpAddress::Any6();
+  ExpectNetlinkPacket(
+      RTM_NEWADDR, NLM_F_ACK | NLM_F_REQUEST,
+      [](void* buf, size_t len, int seq) {
+        struct nlmsghdr* netlink_message =
+            CreateNetlinkMessage(buf, nullptr, NLMSG_ERROR, seq);
+        auto* err =
+            reinterpret_cast<struct nlmsgerr*>(NLMSG_DATA(netlink_message));
+        // Ack the request
+        err->error = 0;
+        netlink_message->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
+        return netlink_message->nlmsg_len;
+      },
+      [ip](const void* buf, size_t len) {
+        auto* netlink_message = reinterpret_cast<const struct nlmsghdr*>(buf);
+        auto* ifa = reinterpret_cast<const struct ifaddrmsg*>(
+            NLMSG_DATA(netlink_message));
+        EXPECT_EQ(19, ifa->ifa_prefixlen);
+        EXPECT_EQ(RT_SCOPE_UNIVERSE, ifa->ifa_scope);
+        EXPECT_EQ(IFA_F_PERMANENT, ifa->ifa_flags);
+        EXPECT_EQ(7, ifa->ifa_index);
+        EXPECT_EQ(AF_INET6, ifa->ifa_family);
+
+        const struct rtattr* rta;
+        int payload_length = IFA_PAYLOAD(netlink_message);
+        int num_rta = 0;
+        for (rta = IFA_RTA(ifa); RTA_OK(rta, payload_length);
+             rta = RTA_NEXT(rta, payload_length)) {
+          switch (rta->rta_type) {
+            case IFA_LOCAL: {
+              EXPECT_EQ(ip.ToPackedString().size(), RTA_PAYLOAD(rta));
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(ip, address);
+              break;
+            }
+            case IFA_CACHEINFO: {
+              EXPECT_EQ(sizeof(struct ifa_cacheinfo), RTA_PAYLOAD(rta));
+              const auto* cache_info =
+                  reinterpret_cast<const struct ifa_cacheinfo*>(RTA_DATA(rta));
+              EXPECT_EQ(8, cache_info->ifa_prefered);  // common_typos_disable
+              EXPECT_EQ(6, cache_info->ifa_valid);
+              EXPECT_EQ(4, cache_info->cstamp);
+              EXPECT_EQ(2, cache_info->tstamp);
+              break;
+            }
+            default:
+              EXPECT_TRUE(false) << "Seeing rtattr that should not exist";
+          }
+          ++num_rta;
+        }
+        EXPECT_EQ(2, num_rta);
+      });
+
+  struct {
+    struct rtattr rta;
+    struct ifa_cacheinfo cache_info;
+  } additional_rta;
+
+  additional_rta.rta.rta_type = IFA_CACHEINFO;
+  additional_rta.rta.rta_len = RTA_LENGTH(sizeof(struct ifa_cacheinfo));
+  additional_rta.cache_info.ifa_prefered = 8;
+  additional_rta.cache_info.ifa_valid = 6;
+  additional_rta.cache_info.cstamp = 4;
+  additional_rta.cache_info.tstamp = 2;
+
+  EXPECT_TRUE(netlink->ChangeLocalAddress(7, Netlink::Verb::kAdd, ip, 19,
+                                          IFA_F_PERMANENT, RT_SCOPE_UNIVERSE,
+                                          {&additional_rta.rta}));
+}
+
+TEST_F(NetlinkTest, ChangeLocalAddressRemove) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicIpAddress ip = QuicIpAddress::Any4();
+  ExpectNetlinkPacket(
+      RTM_DELADDR, NLM_F_ACK | NLM_F_REQUEST,
+      [](void* buf, size_t len, int seq) {
+        struct nlmsghdr* netlink_message =
+            CreateNetlinkMessage(buf, nullptr, NLMSG_ERROR, seq);
+        auto* err =
+            reinterpret_cast<struct nlmsgerr*>(NLMSG_DATA(netlink_message));
+        // Ack the request
+        err->error = 0;
+        netlink_message->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
+        return netlink_message->nlmsg_len;
+      },
+      [ip](const void* buf, size_t len) {
+        auto* netlink_message = reinterpret_cast<const struct nlmsghdr*>(buf);
+        auto* ifa = reinterpret_cast<const struct ifaddrmsg*>(
+            NLMSG_DATA(netlink_message));
+        EXPECT_EQ(32, ifa->ifa_prefixlen);
+        EXPECT_EQ(RT_SCOPE_UNIVERSE, ifa->ifa_scope);
+        EXPECT_EQ(0, ifa->ifa_flags);
+        EXPECT_EQ(7, ifa->ifa_index);
+        EXPECT_EQ(AF_INET, ifa->ifa_family);
+
+        const struct rtattr* rta;
+        int payload_length = IFA_PAYLOAD(netlink_message);
+        int num_rta = 0;
+        for (rta = IFA_RTA(ifa); RTA_OK(rta, payload_length);
+             rta = RTA_NEXT(rta, payload_length)) {
+          switch (rta->rta_type) {
+            case IFA_LOCAL: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(in_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(ip, address);
+              break;
+            }
+            default:
+              EXPECT_TRUE(false) << "Seeing rtattr that should not exist";
+          }
+          ++num_rta;
+        }
+        EXPECT_EQ(1, num_rta);
+      });
+
+  EXPECT_TRUE(netlink->ChangeLocalAddress(7, Netlink::Verb::kRemove, ip, 32, 0,
+                                          RT_SCOPE_UNIVERSE, {}));
+}
+
+TEST_F(NetlinkTest, GetRouteInfoWorks) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicIpAddress destination;
+  ASSERT_TRUE(destination.FromString("f800::2"));
+  ExpectNetlinkPacket(RTM_GETROUTE, NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST,
+                      [destination](void* buf, size_t len, int seq) {
+                        int ret = 0;
+                        struct nlmsghdr* netlink_message = CreateNetlinkMessage(
+                            buf, nullptr, RTM_NEWROUTE, seq);
+                        CreateRtmsg(netlink_message, AF_INET6, 48, 0, 0,
+                                    RT_TABLE_MAIN, RTPROT_STATIC, RT_SCOPE_LINK,
+                                    RTN_UNICAST, 0, destination, 7);
+                        ret += NLMSG_ALIGN(netlink_message->nlmsg_len);
+
+                        netlink_message = CreateNetlinkMessage(
+                            buf, netlink_message, NLMSG_DONE, seq);
+                        ret += NLMSG_ALIGN(netlink_message->nlmsg_len);
+
+                        QUIC_LOG(INFO) << "ret: " << ret;
+                        return ret;
+                      });
+
+  std::vector<Netlink::RoutingRule> routing_rules;
+  EXPECT_TRUE(netlink->GetRouteInfo(&routing_rules));
+
+  ASSERT_EQ(1, routing_rules.size());
+  EXPECT_EQ(RT_SCOPE_LINK, routing_rules[0].scope);
+  EXPECT_EQ(IpRange(destination, 48).ToString(),
+            routing_rules[0].destination_subnet.ToString());
+  EXPECT_FALSE(routing_rules[0].preferred_source.IsInitialized());
+  EXPECT_EQ(7, routing_rules[0].out_interface);
+}
+
+TEST_F(NetlinkTest, ChangeRouteAdd) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicIpAddress preferred_ip;
+  preferred_ip.FromString("ff80:dead:beef::1");
+  IpRange subnet;
+  subnet.FromString("ff80:dead:beef::/48");
+  int egress_interface_index = 7;
+  ExpectNetlinkPacket(
+      RTM_NEWROUTE, NLM_F_ACK | NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL,
+      [](void* buf, size_t len, int seq) {
+        struct nlmsghdr* netlink_message =
+            CreateNetlinkMessage(buf, nullptr, NLMSG_ERROR, seq);
+        auto* err =
+            reinterpret_cast<struct nlmsgerr*>(NLMSG_DATA(netlink_message));
+        // Ack the request
+        err->error = 0;
+        netlink_message->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
+        return netlink_message->nlmsg_len;
+      },
+      [preferred_ip, subnet, egress_interface_index](const void* buf,
+                                                     size_t len) {
+        auto* netlink_message = reinterpret_cast<const struct nlmsghdr*>(buf);
+        auto* rtm =
+            reinterpret_cast<const struct rtmsg*>(NLMSG_DATA(netlink_message));
+        EXPECT_EQ(AF_INET6, rtm->rtm_family);
+        EXPECT_EQ(48, rtm->rtm_dst_len);
+        EXPECT_EQ(0, rtm->rtm_src_len);
+        EXPECT_EQ(RT_TABLE_MAIN, rtm->rtm_table);
+        EXPECT_EQ(RTPROT_STATIC, rtm->rtm_protocol);
+        EXPECT_EQ(RT_SCOPE_LINK, rtm->rtm_scope);
+        EXPECT_EQ(RTN_UNICAST, rtm->rtm_type);
+
+        const struct rtattr* rta;
+        int payload_length = RTM_PAYLOAD(netlink_message);
+        int num_rta = 0;
+        for (rta = RTM_RTA(rtm); RTA_OK(rta, payload_length);
+             rta = RTA_NEXT(rta, payload_length)) {
+          switch (rta->rta_type) {
+            case RTA_PREFSRC: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(struct in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(preferred_ip, address);
+              break;
+            }
+            case RTA_OIF: {
+              ASSERT_EQ(sizeof(int), RTA_PAYLOAD(rta));
+              const auto* interface_index =
+                  reinterpret_cast<const int*>(RTA_DATA(rta));
+              EXPECT_EQ(egress_interface_index, *interface_index);
+              break;
+            }
+            case RTA_DST: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(struct in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(subnet.ToString(),
+                        IpRange(address, rtm->rtm_dst_len).ToString());
+              break;
+            }
+            case RTA_TABLE: {
+              ASSERT_EQ(*reinterpret_cast<uint32_t*>(RTA_DATA(rta)),
+                        QboneConstants::kQboneRouteTableId);
+              break;
+            }
+            default:
+              EXPECT_TRUE(false) << "Seeing rtattr that should not be sent";
+          }
+          ++num_rta;
+        }
+        EXPECT_EQ(4, num_rta);
+      });
+  EXPECT_TRUE(netlink->ChangeRoute(
+      Netlink::Verb::kAdd, QboneConstants::kQboneRouteTableId, subnet,
+      RT_SCOPE_LINK, preferred_ip, egress_interface_index));
+}
+
+TEST_F(NetlinkTest, ChangeRouteRemove) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicIpAddress preferred_ip;
+  preferred_ip.FromString("ff80:dead:beef::1");
+  IpRange subnet;
+  subnet.FromString("ff80:dead:beef::/48");
+  int egress_interface_index = 7;
+  ExpectNetlinkPacket(
+      RTM_DELROUTE, NLM_F_ACK | NLM_F_REQUEST,
+      [](void* buf, size_t len, int seq) {
+        struct nlmsghdr* netlink_message =
+            CreateNetlinkMessage(buf, nullptr, NLMSG_ERROR, seq);
+        auto* err =
+            reinterpret_cast<struct nlmsgerr*>(NLMSG_DATA(netlink_message));
+        // Ack the request
+        err->error = 0;
+        netlink_message->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
+        return netlink_message->nlmsg_len;
+      },
+      [preferred_ip, subnet, egress_interface_index](const void* buf,
+                                                     size_t len) {
+        auto* netlink_message = reinterpret_cast<const struct nlmsghdr*>(buf);
+        auto* rtm =
+            reinterpret_cast<const struct rtmsg*>(NLMSG_DATA(netlink_message));
+        EXPECT_EQ(AF_INET6, rtm->rtm_family);
+        EXPECT_EQ(48, rtm->rtm_dst_len);
+        EXPECT_EQ(0, rtm->rtm_src_len);
+        EXPECT_EQ(RT_TABLE_MAIN, rtm->rtm_table);
+        EXPECT_EQ(RTPROT_UNSPEC, rtm->rtm_protocol);
+        EXPECT_EQ(RT_SCOPE_LINK, rtm->rtm_scope);
+        EXPECT_EQ(RTN_UNICAST, rtm->rtm_type);
+
+        const struct rtattr* rta;
+        int payload_length = RTM_PAYLOAD(netlink_message);
+        int num_rta = 0;
+        for (rta = RTM_RTA(rtm); RTA_OK(rta, payload_length);
+             rta = RTA_NEXT(rta, payload_length)) {
+          switch (rta->rta_type) {
+            case RTA_PREFSRC: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(struct in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(preferred_ip, address);
+              break;
+            }
+            case RTA_OIF: {
+              ASSERT_EQ(sizeof(int), RTA_PAYLOAD(rta));
+              const auto* interface_index =
+                  reinterpret_cast<const int*>(RTA_DATA(rta));
+              EXPECT_EQ(egress_interface_index, *interface_index);
+              break;
+            }
+            case RTA_DST: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(struct in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(subnet.ToString(),
+                        IpRange(address, rtm->rtm_dst_len).ToString());
+              break;
+            }
+            case RTA_TABLE: {
+              ASSERT_EQ(*reinterpret_cast<uint32_t*>(RTA_DATA(rta)),
+                        QboneConstants::kQboneRouteTableId);
+              break;
+            }
+            default:
+              EXPECT_TRUE(false) << "Seeing rtattr that should not be sent";
+          }
+          ++num_rta;
+        }
+        EXPECT_EQ(4, num_rta);
+      });
+  EXPECT_TRUE(netlink->ChangeRoute(
+      Netlink::Verb::kRemove, QboneConstants::kQboneRouteTableId, subnet,
+      RT_SCOPE_LINK, preferred_ip, egress_interface_index));
+}
+
+TEST_F(NetlinkTest, ChangeRouteReplace) {
+  auto netlink = QuicMakeUnique<Netlink>(&mock_kernel_);
+
+  QuicIpAddress preferred_ip;
+  preferred_ip.FromString("ff80:dead:beef::1");
+  IpRange subnet;
+  subnet.FromString("ff80:dead:beef::/48");
+  int egress_interface_index = 7;
+  ExpectNetlinkPacket(
+      RTM_NEWROUTE, NLM_F_ACK | NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE,
+      [](void* buf, size_t len, int seq) {
+        struct nlmsghdr* netlink_message =
+            CreateNetlinkMessage(buf, nullptr, NLMSG_ERROR, seq);
+        auto* err =
+            reinterpret_cast<struct nlmsgerr*>(NLMSG_DATA(netlink_message));
+        // Ack the request
+        err->error = 0;
+        netlink_message->nlmsg_len = NLMSG_LENGTH(sizeof(struct nlmsgerr));
+        return netlink_message->nlmsg_len;
+      },
+      [preferred_ip, subnet, egress_interface_index](const void* buf,
+                                                     size_t len) {
+        auto* netlink_message = reinterpret_cast<const struct nlmsghdr*>(buf);
+        auto* rtm =
+            reinterpret_cast<const struct rtmsg*>(NLMSG_DATA(netlink_message));
+        EXPECT_EQ(AF_INET6, rtm->rtm_family);
+        EXPECT_EQ(48, rtm->rtm_dst_len);
+        EXPECT_EQ(0, rtm->rtm_src_len);
+        EXPECT_EQ(RT_TABLE_MAIN, rtm->rtm_table);
+        EXPECT_EQ(RTPROT_STATIC, rtm->rtm_protocol);
+        EXPECT_EQ(RT_SCOPE_LINK, rtm->rtm_scope);
+        EXPECT_EQ(RTN_UNICAST, rtm->rtm_type);
+
+        const struct rtattr* rta;
+        int payload_length = RTM_PAYLOAD(netlink_message);
+        int num_rta = 0;
+        for (rta = RTM_RTA(rtm); RTA_OK(rta, payload_length);
+             rta = RTA_NEXT(rta, payload_length)) {
+          switch (rta->rta_type) {
+            case RTA_PREFSRC: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(struct in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(preferred_ip, address);
+              break;
+            }
+            case RTA_OIF: {
+              ASSERT_EQ(sizeof(int), RTA_PAYLOAD(rta));
+              const auto* interface_index =
+                  reinterpret_cast<const int*>(RTA_DATA(rta));
+              EXPECT_EQ(egress_interface_index, *interface_index);
+              break;
+            }
+            case RTA_DST: {
+              const auto* raw_address =
+                  reinterpret_cast<const char*>(RTA_DATA(rta));
+              ASSERT_EQ(sizeof(struct in6_addr), RTA_PAYLOAD(rta));
+              QuicIpAddress address;
+              address.FromPackedString(raw_address, RTA_PAYLOAD(rta));
+              EXPECT_EQ(subnet.ToString(),
+                        IpRange(address, rtm->rtm_dst_len).ToString());
+              break;
+            }
+            case RTA_TABLE: {
+              ASSERT_EQ(*reinterpret_cast<uint32_t*>(RTA_DATA(rta)),
+                        QboneConstants::kQboneRouteTableId);
+              break;
+            }
+            default:
+              EXPECT_TRUE(false) << "Seeing rtattr that should not be sent";
+          }
+          ++num_rta;
+        }
+        EXPECT_EQ(4, num_rta);
+      });
+  EXPECT_TRUE(netlink->ChangeRoute(
+      Netlink::Verb::kReplace, QboneConstants::kQboneRouteTableId, subnet,
+      RT_SCOPE_LINK, preferred_ip, egress_interface_index));
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/platform/rtnetlink_message.cc b/quic/qbone/platform/rtnetlink_message.cc
new file mode 100644
index 0000000..f35628a
--- /dev/null
+++ b/quic/qbone/platform/rtnetlink_message.cc
@@ -0,0 +1,177 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/rtnetlink_message.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+
+namespace quic {
+
+RtnetlinkMessage::RtnetlinkMessage(uint16_t type,
+                                   uint16_t flags,
+                                   uint32_t seq,
+                                   uint32_t pid,
+                                   const void* payload_header,
+                                   size_t payload_header_length) {
+  auto* buf = new uint8_t[NLMSG_SPACE(payload_header_length)];
+  memset(buf, 0, NLMSG_SPACE(payload_header_length));
+
+  auto* message_header = reinterpret_cast<struct nlmsghdr*>(buf);
+  message_header->nlmsg_len = NLMSG_LENGTH(payload_header_length);
+  message_header->nlmsg_type = type;
+  message_header->nlmsg_flags = flags;
+  message_header->nlmsg_seq = seq;
+  message_header->nlmsg_pid = pid;
+
+  if (payload_header != nullptr) {
+    memcpy(NLMSG_DATA(message_header), payload_header, payload_header_length);
+  }
+  message_.push_back({buf, NLMSG_SPACE(payload_header_length)});
+}
+
+RtnetlinkMessage::~RtnetlinkMessage() {
+  for (const auto& iov : message_) {
+    delete[] reinterpret_cast<uint8_t*>(iov.iov_base);
+  }
+}
+
+void RtnetlinkMessage::AppendAttribute(uint16_t type,
+                                       const void* data,
+                                       uint16_t data_length) {
+  auto* buf = new uint8_t[RTA_SPACE(data_length)];
+  memset(buf, 0, RTA_SPACE(data_length));
+
+  auto* rta = reinterpret_cast<struct rtattr*>(buf);
+  static_assert(sizeof(uint16_t) == sizeof(rta->rta_len),
+                "struct rtattr uses unsigned short, it's no longer 16bits");
+  static_assert(sizeof(uint16_t) == sizeof(rta->rta_type),
+                "struct rtattr uses unsigned short, it's no longer 16bits");
+
+  rta->rta_len = RTA_LENGTH(data_length);
+  rta->rta_type = type;
+  memcpy(RTA_DATA(rta), data, data_length);
+
+  message_.push_back({buf, RTA_SPACE(data_length)});
+  AdjustMessageLength(rta->rta_len);
+}
+
+std::unique_ptr<struct iovec[]> RtnetlinkMessage::BuildIoVec() const {
+  auto message = QuicMakeUnique<struct iovec[]>(message_.size());
+  int idx = 0;
+  for (const auto& vec : message_) {
+    message[idx++] = vec;
+  }
+  return message;
+}
+
+size_t RtnetlinkMessage::IoVecSize() const {
+  return message_.size();
+}
+
+void RtnetlinkMessage::AdjustMessageLength(size_t additional_data_length) {
+  MessageHeader()->nlmsg_len =
+      NLMSG_ALIGN(MessageHeader()->nlmsg_len) + additional_data_length;
+}
+
+struct nlmsghdr* RtnetlinkMessage::MessageHeader() {
+  return reinterpret_cast<struct nlmsghdr*>(message_[0].iov_base);
+}
+
+LinkMessage LinkMessage::New(RtnetlinkMessage::Operation request_operation,
+                             uint16_t flags,
+                             uint32_t seq,
+                             uint32_t pid,
+                             const struct ifinfomsg* interface_info_header) {
+  uint16_t request_type;
+  switch (request_operation) {
+    case RtnetlinkMessage::Operation::NEW:
+      request_type = RTM_NEWLINK;
+      break;
+    case RtnetlinkMessage::Operation::DEL:
+      request_type = RTM_DELLINK;
+      break;
+    case RtnetlinkMessage::Operation::GET:
+      request_type = RTM_GETLINK;
+      break;
+  }
+  bool is_get = request_type == RTM_GETLINK;
+
+  if (is_get) {
+    struct rtgenmsg g = {AF_UNSPEC};
+    return LinkMessage(request_type, flags, seq, pid, &g, sizeof(g));
+  }
+  return LinkMessage(request_type, flags, seq, pid, interface_info_header,
+                     sizeof(struct ifinfomsg));
+}
+
+AddressMessage AddressMessage::New(
+    RtnetlinkMessage::Operation request_operation,
+    uint16_t flags,
+    uint32_t seq,
+    uint32_t pid,
+    const struct ifaddrmsg* interface_address_header) {
+  uint16_t request_type;
+  switch (request_operation) {
+    case RtnetlinkMessage::Operation::NEW:
+      request_type = RTM_NEWADDR;
+      break;
+    case RtnetlinkMessage::Operation::DEL:
+      request_type = RTM_DELADDR;
+      break;
+    case RtnetlinkMessage::Operation::GET:
+      request_type = RTM_GETADDR;
+      break;
+  }
+  bool is_get = request_type == RTM_GETADDR;
+
+  if (is_get) {
+    struct rtgenmsg g = {AF_UNSPEC};
+    return AddressMessage(request_type, flags, seq, pid, &g, sizeof(g));
+  }
+  return AddressMessage(request_type, flags, seq, pid, interface_address_header,
+                        sizeof(struct ifaddrmsg));
+}
+
+RouteMessage RouteMessage::New(RtnetlinkMessage::Operation request_operation,
+                               uint16_t flags,
+                               uint32_t seq,
+                               uint32_t pid,
+                               const struct rtmsg* route_message_header) {
+  uint16_t request_type;
+  switch (request_operation) {
+    case RtnetlinkMessage::Operation::NEW:
+      request_type = RTM_NEWROUTE;
+      break;
+    case RtnetlinkMessage::Operation::DEL:
+      request_type = RTM_DELROUTE;
+      break;
+    case RtnetlinkMessage::Operation::GET:
+      request_type = RTM_GETROUTE;
+      break;
+  }
+  return RouteMessage(request_type, flags, seq, pid, route_message_header,
+                      sizeof(struct rtmsg));
+}
+
+RuleMessage RuleMessage::New(RtnetlinkMessage::Operation request_operation,
+                             uint16_t flags,
+                             uint32_t seq,
+                             uint32_t pid,
+                             const struct rtmsg* rule_message_header) {
+  uint16_t request_type;
+  switch (request_operation) {
+    case RtnetlinkMessage::Operation::NEW:
+      request_type = RTM_NEWRULE;
+      break;
+    case RtnetlinkMessage::Operation::DEL:
+      request_type = RTM_DELRULE;
+      break;
+    case RtnetlinkMessage::Operation::GET:
+      request_type = RTM_GETRULE;
+      break;
+  }
+  return RuleMessage(request_type, flags, seq, pid, rule_message_header,
+                     sizeof(rtmsg));
+}
+}  // namespace quic
diff --git a/quic/qbone/platform/rtnetlink_message.h b/quic/qbone/platform/rtnetlink_message.h
new file mode 100644
index 0000000..0412d54
--- /dev/null
+++ b/quic/qbone/platform/rtnetlink_message.h
@@ -0,0 +1,126 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_RTNETLINK_MESSAGE_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_RTNETLINK_MESSAGE_H_
+
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+#include <stdint.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+
+#include <memory>
+#include <vector>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_str_cat.h"
+
+namespace quic {
+
+// This base class is used to construct an array struct iovec that represents a
+// rtnetlink message as defined in man 7 rtnet. Padding for message header
+// alignment to conform NLMSG_* and RTA_* macros is added at the end of each
+// iovec::iov_base.
+class RtnetlinkMessage {
+ public:
+  virtual ~RtnetlinkMessage();
+
+  enum class Operation {
+    NEW,
+    DEL,
+    GET,
+  };
+
+  // Appends a struct rtattr to the message. nlmsg_len and rta_len is handled
+  // properly.
+  // Override this to perform check on type.
+  virtual void AppendAttribute(uint16_t type,
+                               const void* data,
+                               uint16_t data_length);
+
+  // Builds the array of iovec that can be fed into sendmsg directly.
+  std::unique_ptr<struct iovec[]> BuildIoVec() const;
+
+  // The size of the array of iovec if BuildIovec is called.
+  size_t IoVecSize() const;
+
+ protected:
+  // Subclass should add their own message header immediately after the
+  // nlmsghdr. Make this private to force the creation of such header.
+  RtnetlinkMessage(uint16_t type,
+                   uint16_t flags,
+                   uint32_t seq,
+                   uint32_t pid,
+                   const void* payload_header,
+                   size_t payload_header_length);
+
+  // Adjusts nlmsg_len in the header assuming additional_data_length is appended
+  // at the end.
+  void AdjustMessageLength(size_t additional_data_length);
+
+ private:
+  // Convenient function for accessing the nlmsghdr.
+  struct nlmsghdr* MessageHeader();
+
+  std::vector<struct iovec> message_;
+};
+
+// Message for manipulating link level configuration as defined in man 7
+// rtnetlink. RTM_NEWLINK, RTM_DELLINK and RTM_GETLINK are supported.
+class LinkMessage : public RtnetlinkMessage {
+ public:
+  static LinkMessage New(RtnetlinkMessage::Operation request_operation,
+                         uint16_t flags,
+                         uint32_t seq,
+                         uint32_t pid,
+                         const struct ifinfomsg* interface_info_header);
+
+ private:
+  using RtnetlinkMessage::RtnetlinkMessage;
+};
+
+// Message for manipulating address level configuration as defined in man 7
+// rtnetlink. RTM_NEWADDR, RTM_NEWADDR and RTM_GETADDR are supported.
+class AddressMessage : public RtnetlinkMessage {
+ public:
+  static AddressMessage New(RtnetlinkMessage::Operation request_operation,
+                            uint16_t flags,
+                            uint32_t seq,
+                            uint32_t pid,
+                            const struct ifaddrmsg* interface_address_header);
+
+ private:
+  using RtnetlinkMessage::RtnetlinkMessage;
+};
+
+// Message for manipulating routing table as defined in man 7 rtnetlink.
+// RTM_NEWROUTE, RTM_DELROUTE and RTM_GETROUTE are supported.
+class RouteMessage : public RtnetlinkMessage {
+ public:
+  static RouteMessage New(RtnetlinkMessage::Operation request_operation,
+                          uint16_t flags,
+                          uint32_t seq,
+                          uint32_t pid,
+                          const struct rtmsg* route_message_header);
+
+ private:
+  using RtnetlinkMessage::RtnetlinkMessage;
+};
+
+class RuleMessage : public RtnetlinkMessage {
+ public:
+  static RuleMessage New(RtnetlinkMessage::Operation request_operation,
+                         uint16_t flags,
+                         uint32_t seq,
+                         uint32_t pid,
+                         const struct rtmsg* rule_message_header);
+
+ private:
+  using RtnetlinkMessage::RtnetlinkMessage;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_RTNETLINK_MESSAGE_H_
diff --git a/quic/qbone/platform/rtnetlink_message_test.cc b/quic/qbone/platform/rtnetlink_message_test.cc
new file mode 100644
index 0000000..b129237
--- /dev/null
+++ b/quic/qbone/platform/rtnetlink_message_test.cc
@@ -0,0 +1,228 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/rtnetlink_message.h"
+
+#include <net/if_arp.h>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+
+namespace quic {
+namespace {
+
+using ::testing::StrEq;
+
+TEST(RtnetlinkMessageTest, LinkMessageCanBeCreatedForGetOperation) {
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ROOT | NLM_F_MATCH;
+  uint32_t seq = 42;
+  uint32_t pid = 7;
+  auto message = LinkMessage::New(RtnetlinkMessage::Operation::GET, flags, seq,
+                                  pid, nullptr);
+
+  // No rtattr appended.
+  EXPECT_EQ(1, message.IoVecSize());
+
+  // nlmsghdr is built properly.
+  auto iov = message.BuildIoVec();
+  EXPECT_EQ(NLMSG_SPACE(sizeof(struct rtgenmsg)), iov[0].iov_len);
+  auto* netlink_message = reinterpret_cast<struct nlmsghdr*>(iov[0].iov_base);
+  EXPECT_EQ(NLMSG_LENGTH(sizeof(struct rtgenmsg)), netlink_message->nlmsg_len);
+  EXPECT_EQ(RTM_GETLINK, netlink_message->nlmsg_type);
+  EXPECT_EQ(flags, netlink_message->nlmsg_flags);
+  EXPECT_EQ(seq, netlink_message->nlmsg_seq);
+  EXPECT_EQ(pid, netlink_message->nlmsg_pid);
+
+  // We actually included rtgenmsg instead of the passed in ifinfomsg since this
+  // is a GET operation.
+  EXPECT_EQ(NLMSG_LENGTH(sizeof(struct rtgenmsg)), netlink_message->nlmsg_len);
+}
+
+TEST(RtnetlinkMessageTest, LinkMessageCanBeCreatedForNewOperation) {
+  struct ifinfomsg interface_info_header = {AF_INET, /* pad */ 0, ARPHRD_TUNNEL,
+                                            3,       0,           0xffffffff};
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ROOT | NLM_F_MATCH;
+  uint32_t seq = 42;
+  uint32_t pid = 7;
+  auto message = LinkMessage::New(RtnetlinkMessage::Operation::NEW, flags, seq,
+                                  pid, &interface_info_header);
+
+  string device_name = "device0";
+  message.AppendAttribute(IFLA_IFNAME, device_name.c_str(), device_name.size());
+
+  // One rtattr appended.
+  EXPECT_EQ(2, message.IoVecSize());
+
+  // nlmsghdr is built properly.
+  auto iov = message.BuildIoVec();
+  EXPECT_EQ(NLMSG_ALIGN(NLMSG_LENGTH(sizeof(struct ifinfomsg))),
+            iov[0].iov_len);
+  auto* netlink_message = reinterpret_cast<struct nlmsghdr*>(iov[0].iov_base);
+  EXPECT_EQ(NLMSG_ALIGN(NLMSG_LENGTH(sizeof(struct ifinfomsg))) +
+                RTA_LENGTH(device_name.size()),
+            netlink_message->nlmsg_len);
+  EXPECT_EQ(RTM_NEWLINK, netlink_message->nlmsg_type);
+  EXPECT_EQ(flags, netlink_message->nlmsg_flags);
+  EXPECT_EQ(seq, netlink_message->nlmsg_seq);
+  EXPECT_EQ(pid, netlink_message->nlmsg_pid);
+
+  // ifinfomsg is included properly.
+  auto* parsed_header =
+      reinterpret_cast<struct ifinfomsg*>(NLMSG_DATA(netlink_message));
+  EXPECT_EQ(interface_info_header.ifi_family, parsed_header->ifi_family);
+  EXPECT_EQ(interface_info_header.ifi_type, parsed_header->ifi_type);
+  EXPECT_EQ(interface_info_header.ifi_index, parsed_header->ifi_index);
+  EXPECT_EQ(interface_info_header.ifi_flags, parsed_header->ifi_flags);
+  EXPECT_EQ(interface_info_header.ifi_change, parsed_header->ifi_change);
+
+  // rtattr is handled properly.
+  EXPECT_EQ(RTA_SPACE(device_name.size()), iov[1].iov_len);
+  auto* rta = reinterpret_cast<struct rtattr*>(iov[1].iov_base);
+  EXPECT_EQ(IFLA_IFNAME, rta->rta_type);
+  EXPECT_EQ(RTA_LENGTH(device_name.size()), rta->rta_len);
+  EXPECT_THAT(device_name, StrEq(string(reinterpret_cast<char*>(RTA_DATA(rta)),
+                                        RTA_PAYLOAD(rta))));
+}
+
+TEST(RtnetlinkMessageTest, AddressMessageCanBeCreatedForGetOperation) {
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ROOT | NLM_F_MATCH;
+  uint32_t seq = 42;
+  uint32_t pid = 7;
+  auto message = AddressMessage::New(RtnetlinkMessage::Operation::GET, flags,
+                                     seq, pid, nullptr);
+
+  // No rtattr appended.
+  EXPECT_EQ(1, message.IoVecSize());
+
+  // nlmsghdr is built properly.
+  auto iov = message.BuildIoVec();
+  EXPECT_EQ(NLMSG_SPACE(sizeof(struct rtgenmsg)), iov[0].iov_len);
+  auto* netlink_message = reinterpret_cast<struct nlmsghdr*>(iov[0].iov_base);
+  EXPECT_EQ(NLMSG_LENGTH(sizeof(struct rtgenmsg)), netlink_message->nlmsg_len);
+  EXPECT_EQ(RTM_GETADDR, netlink_message->nlmsg_type);
+  EXPECT_EQ(flags, netlink_message->nlmsg_flags);
+  EXPECT_EQ(seq, netlink_message->nlmsg_seq);
+  EXPECT_EQ(pid, netlink_message->nlmsg_pid);
+
+  // We actually included rtgenmsg instead of the passed in ifinfomsg since this
+  // is a GET operation.
+  EXPECT_EQ(NLMSG_LENGTH(sizeof(struct rtgenmsg)), netlink_message->nlmsg_len);
+}
+
+TEST(RtnetlinkMessageTest, AddressMessageCanBeCreatedForNewOperation) {
+  struct ifaddrmsg interface_address_header = {AF_INET,
+                                               /* prefixlen */ 24,
+                                               /* flags */ 0,
+                                               /* scope */ RT_SCOPE_LINK,
+                                               /* index */ 4};
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ROOT | NLM_F_MATCH;
+  uint32_t seq = 42;
+  uint32_t pid = 7;
+  auto message = AddressMessage::New(RtnetlinkMessage::Operation::NEW, flags,
+                                     seq, pid, &interface_address_header);
+
+  QuicIpAddress ip;
+  CHECK(ip.FromString("10.0.100.3"));
+  message.AppendAttribute(IFA_ADDRESS, ip.ToPackedString().c_str(),
+                          ip.ToPackedString().size());
+
+  // One rtattr is appended.
+  EXPECT_EQ(2, message.IoVecSize());
+
+  // nlmsghdr is built properly.
+  auto iov = message.BuildIoVec();
+  EXPECT_EQ(NLMSG_ALIGN(NLMSG_LENGTH(sizeof(struct ifaddrmsg))),
+            iov[0].iov_len);
+  auto* netlink_message = reinterpret_cast<struct nlmsghdr*>(iov[0].iov_base);
+  EXPECT_EQ(NLMSG_ALIGN(NLMSG_LENGTH(sizeof(struct ifaddrmsg))) +
+                RTA_LENGTH(ip.ToPackedString().size()),
+            netlink_message->nlmsg_len);
+  EXPECT_EQ(RTM_NEWADDR, netlink_message->nlmsg_type);
+  EXPECT_EQ(flags, netlink_message->nlmsg_flags);
+  EXPECT_EQ(seq, netlink_message->nlmsg_seq);
+  EXPECT_EQ(pid, netlink_message->nlmsg_pid);
+
+  // ifaddrmsg is included properly.
+  auto* parsed_header =
+      reinterpret_cast<struct ifaddrmsg*>(NLMSG_DATA(netlink_message));
+  EXPECT_EQ(interface_address_header.ifa_family, parsed_header->ifa_family);
+  EXPECT_EQ(interface_address_header.ifa_prefixlen,
+            parsed_header->ifa_prefixlen);
+  EXPECT_EQ(interface_address_header.ifa_flags, parsed_header->ifa_flags);
+  EXPECT_EQ(interface_address_header.ifa_scope, parsed_header->ifa_scope);
+  EXPECT_EQ(interface_address_header.ifa_index, parsed_header->ifa_index);
+
+  // rtattr is handled properly.
+  EXPECT_EQ(RTA_SPACE(ip.ToPackedString().size()), iov[1].iov_len);
+  auto* rta = reinterpret_cast<struct rtattr*>(iov[1].iov_base);
+  EXPECT_EQ(IFA_ADDRESS, rta->rta_type);
+  EXPECT_EQ(RTA_LENGTH(ip.ToPackedString().size()), rta->rta_len);
+  EXPECT_THAT(
+      ip.ToPackedString(),
+      StrEq(string(reinterpret_cast<char*>(RTA_DATA(rta)), RTA_PAYLOAD(rta))));
+}
+
+TEST(RtnetlinkMessageTest, RouteMessageCanBeCreatedFromNewOperation) {
+  struct rtmsg route_message_header = {AF_INET6,
+                                       /* rtm_dst_len */ 48,
+                                       /* rtm_src_len */ 0,
+                                       /* rtm_tos */ 0,
+                                       /* rtm_table */ RT_TABLE_MAIN,
+                                       /* rtm_protocol */ RTPROT_STATIC,
+                                       /* rtm_scope */ RT_SCOPE_LINK,
+                                       /* rtm_type */ RTN_LOCAL,
+                                       /* rtm_flags */ 0};
+  uint16_t flags = NLM_F_REQUEST | NLM_F_ROOT | NLM_F_MATCH;
+  uint32_t seq = 42;
+  uint32_t pid = 7;
+  auto message = RouteMessage::New(RtnetlinkMessage::Operation::NEW, flags, seq,
+                                   pid, &route_message_header);
+
+  QuicIpAddress preferred_source;
+  CHECK(preferred_source.FromString("ff80::1"));
+  message.AppendAttribute(RTA_PREFSRC,
+                          preferred_source.ToPackedString().c_str(),
+                          preferred_source.ToPackedString().size());
+
+  // One rtattr is appended.
+  EXPECT_EQ(2, message.IoVecSize());
+
+  // nlmsghdr is built properly
+  auto iov = message.BuildIoVec();
+  EXPECT_EQ(NLMSG_ALIGN(NLMSG_LENGTH(sizeof(struct rtmsg))), iov[0].iov_len);
+  auto* netlink_message = reinterpret_cast<struct nlmsghdr*>(iov[0].iov_base);
+  EXPECT_EQ(NLMSG_ALIGN(NLMSG_LENGTH(sizeof(struct rtmsg))) +
+                RTA_LENGTH(preferred_source.ToPackedString().size()),
+            netlink_message->nlmsg_len);
+  EXPECT_EQ(RTM_NEWROUTE, netlink_message->nlmsg_type);
+  EXPECT_EQ(flags, netlink_message->nlmsg_flags);
+  EXPECT_EQ(seq, netlink_message->nlmsg_seq);
+  EXPECT_EQ(pid, netlink_message->nlmsg_pid);
+
+  // rtmsg is included properly.
+  auto* parsed_header =
+      reinterpret_cast<struct rtmsg*>(NLMSG_DATA(netlink_message));
+  EXPECT_EQ(route_message_header.rtm_family, parsed_header->rtm_family);
+  EXPECT_EQ(route_message_header.rtm_dst_len, parsed_header->rtm_dst_len);
+  EXPECT_EQ(route_message_header.rtm_src_len, parsed_header->rtm_src_len);
+  EXPECT_EQ(route_message_header.rtm_tos, parsed_header->rtm_tos);
+  EXPECT_EQ(route_message_header.rtm_table, parsed_header->rtm_table);
+  EXPECT_EQ(route_message_header.rtm_protocol, parsed_header->rtm_protocol);
+  EXPECT_EQ(route_message_header.rtm_scope, parsed_header->rtm_scope);
+  EXPECT_EQ(route_message_header.rtm_type, parsed_header->rtm_type);
+  EXPECT_EQ(route_message_header.rtm_flags, parsed_header->rtm_flags);
+
+  // rtattr is handled properly.
+  EXPECT_EQ(RTA_SPACE(preferred_source.ToPackedString().size()),
+            iov[1].iov_len);
+  auto* rta = reinterpret_cast<struct rtattr*>(iov[1].iov_base);
+  EXPECT_EQ(RTA_PREFSRC, rta->rta_type);
+  EXPECT_EQ(RTA_LENGTH(preferred_source.ToPackedString().size()), rta->rta_len);
+  EXPECT_THAT(
+      preferred_source.ToPackedString(),
+      StrEq(string(reinterpret_cast<char*>(RTA_DATA(rta)), RTA_PAYLOAD(rta))));
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/platform/tcp_packet.cc b/quic/qbone/platform/tcp_packet.cc
new file mode 100644
index 0000000..56fa88a
--- /dev/null
+++ b/quic/qbone/platform/tcp_packet.cc
@@ -0,0 +1,125 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/tcp_packet.h"
+
+#include <netinet/ip6.h>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_endian.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/internet_checksum.h"
+
+namespace quic {
+namespace {
+
+constexpr size_t kIPv6AddressSize = sizeof(in6_addr);
+constexpr size_t kTcpTtl = 64;
+
+struct TCPv6Packet {
+  ip6_hdr ip_header;
+  tcphdr tcp_header;
+};
+
+struct TCPv6PseudoHeader {
+  uint32_t payload_size{};
+  uint8_t zeros[3] = {0, 0, 0};
+  uint8_t next_header = IPPROTO_TCP;
+};
+
+}  // namespace
+
+void CreateTcpResetPacket(
+    quic::QuicStringPiece original_packet,
+    const std::function<void(quic::QuicStringPiece)>& cb) {
+  // By the time this method is called, original_packet should be fairly
+  // strongly validated. However, it's better to be more paranoid than not, so
+  // here are a bunch of very obvious checks.
+  if (QUIC_PREDICT_FALSE(original_packet.size() < sizeof(ip6_hdr))) {
+    return;
+  }
+  auto* ip6_header = reinterpret_cast<const ip6_hdr*>(original_packet.data());
+  if (QUIC_PREDICT_FALSE(ip6_header->ip6_vfc >> 4 != 6)) {
+    return;
+  }
+  if (QUIC_PREDICT_FALSE(ip6_header->ip6_nxt != IPPROTO_TCP)) {
+    return;
+  }
+  if (QUIC_PREDICT_FALSE(QuicEndian::NetToHost16(ip6_header->ip6_plen) <
+                         sizeof(tcphdr))) {
+    return;
+  }
+  auto* tcp_header = reinterpret_cast<const tcphdr*>(ip6_header + 1);
+
+  // Now that the original packet has been confirmed to be well-formed, it's
+  // time to make the TCP RST packet.
+  TCPv6Packet tcp_packet{};
+
+  const size_t payload_size = sizeof(tcphdr);
+
+  // Set version to 6.
+  tcp_packet.ip_header.ip6_vfc = 0x6 << 4;
+  // Set the payload size, protocol and TTL.
+  tcp_packet.ip_header.ip6_plen = QuicEndian::HostToNet16(payload_size);
+  tcp_packet.ip_header.ip6_nxt = IPPROTO_TCP;
+  tcp_packet.ip_header.ip6_hops = kTcpTtl;
+  // Since the TCP RST is impersonating the endpoint, flip the source and
+  // destination addresses from the original packet.
+  tcp_packet.ip_header.ip6_src = ip6_header->ip6_dst;
+  tcp_packet.ip_header.ip6_dst = ip6_header->ip6_src;
+
+  // The same is true about the TCP ports
+  tcp_packet.tcp_header.dest = tcp_header->source;
+  tcp_packet.tcp_header.source = tcp_header->dest;
+
+  // There are no extensions in this header, so size is trivial
+  tcp_packet.tcp_header.doff = sizeof(tcphdr) >> 2;
+  // Checksum is 0 before it is computed
+  tcp_packet.tcp_header.check = 0;
+
+  // Per RFC 793, TCP RST comes in one of 3 flavors:
+  //
+  // * connection CLOSED
+  // * connection in non-synchronized state (LISTEN, SYN-SENT, SYN-RECEIVED)
+  // * connection in synchronized state (ESTABLISHED, FIN-WAIT-1, etc.)
+  //
+  // QBONE is acting like a firewall, so the RFC text of interest is the CLOSED
+  // state. Note, however, that it is possible for a connection to actually be
+  // in the FIN-WAIT-1 state on the remote end, but the processing logic does
+  // not change.
+  tcp_packet.tcp_header.rst = 1;
+
+  // If the incoming segment has an ACK field, the reset takes its sequence
+  // number from the ACK field of the segment,
+  if (tcp_header->ack) {
+    tcp_packet.tcp_header.seq = tcp_header->ack_seq;
+  } else {
+    // Otherwise the reset has sequence number zero and the ACK field is set to
+    // the sum of the sequence number and segment length of the incoming segment
+    tcp_packet.tcp_header.ack = 1;
+    tcp_packet.tcp_header.seq = 0;
+    tcp_packet.tcp_header.ack_seq =
+        QuicEndian::HostToNet32(QuicEndian::NetToHost32(tcp_header->seq) + 1);
+  }
+
+  TCPv6PseudoHeader pseudo_header{};
+  pseudo_header.payload_size = QuicEndian::HostToNet32(payload_size);
+
+  InternetChecksum checksum;
+  // Pseudoheader.
+  checksum.Update(tcp_packet.ip_header.ip6_src.s6_addr, kIPv6AddressSize);
+  checksum.Update(tcp_packet.ip_header.ip6_dst.s6_addr, kIPv6AddressSize);
+  checksum.Update(reinterpret_cast<char*>(&pseudo_header),
+                  sizeof(pseudo_header));
+  // TCP header.
+  checksum.Update(reinterpret_cast<const char*>(&tcp_packet.tcp_header),
+                  sizeof(tcp_packet.tcp_header));
+  // There is no body.
+  tcp_packet.tcp_header.check = checksum.Value();
+
+  const char* packet = reinterpret_cast<char*>(&tcp_packet);
+
+  cb(QuicStringPiece(packet, sizeof(tcp_packet)));
+}
+
+}  // namespace quic
diff --git a/quic/qbone/platform/tcp_packet.h b/quic/qbone/platform/tcp_packet.h
new file mode 100644
index 0000000..cf33f03
--- /dev/null
+++ b/quic/qbone/platform/tcp_packet.h
@@ -0,0 +1,25 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_PLATFORM_TCP_PACKET_H_
+#define QUICHE_QUIC_QBONE_PLATFORM_TCP_PACKET_H_
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+
+#include <functional>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_string_piece.h"
+
+namespace quic {
+
+// Creates an TCPv6 RST packet, returning a packed string representation of the
+// packet to |cb|.
+void CreateTcpResetPacket(quic::QuicStringPiece original_packet,
+                          const std::function<void(quic::QuicStringPiece)>& cb);
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_PLATFORM_TCP_PACKET_H_
diff --git a/quic/qbone/platform/tcp_packet_test.cc b/quic/qbone/platform/tcp_packet_test.cc
new file mode 100644
index 0000000..53a2c3f
--- /dev/null
+++ b/quic/qbone/platform/tcp_packet_test.cc
@@ -0,0 +1,116 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/platform/tcp_packet.h"
+
+#include <netinet/ip6.h>
+
+#include <cstdint>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h"
+
+namespace quic {
+namespace {
+
+// clang-format off
+constexpr uint8_t kReferenceTCPSYNPacket[] = {
+  // START IPv6 Header
+  // IPv6 with zero ToS and flow label
+  0x60, 0x00, 0x00, 0x00,
+  // Payload is 40 bytes
+  0x00, 0x28,
+  // Next header is TCP (6)
+  0x06,
+  // Hop limit is 64
+  0x40,
+  // Source address of ::1
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+  // Destination address of ::1
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+  // END IPv6 Header
+  // START TCPv6 Header
+  // Source port
+  0xac, 0x1e,
+  // Destination port
+  0x27, 0x0f,
+  // Sequence number
+  0x4b, 0x01, 0xe8, 0x99,
+  // Acknowledgement Sequence number,
+  0x00, 0x00, 0x00, 0x00,
+  // Offset
+  0xa0,
+  // Flags
+  0x02,
+  // Window
+  0xaa, 0xaa,
+  // Checksum
+  0x2e, 0x21,
+  // Urgent
+  0x00, 0x00,
+  // END TCPv6 Header
+  // Options
+  0x02, 0x04, 0xff, 0xc4, 0x04, 0x02, 0x08, 0x0a,
+  0x1b, 0xb8, 0x52, 0xa1, 0x00, 0x00, 0x00, 0x00,
+  0x01, 0x03, 0x03, 0x07,
+};
+
+constexpr uint8_t kReferenceTCPRSTPacket[] = {
+  // START IPv6 Header
+  // IPv6 with zero ToS and flow label
+  0x60, 0x00, 0x00, 0x00,
+  // Payload is 20 bytes
+  0x00, 0x14,
+  // Next header is TCP (6)
+  0x06,
+  // Hop limit is 64
+  0x40,
+  // Source address of ::1
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+  // Destination address of ::1
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+  // END IPv6 Header
+  // START TCPv6 Header
+  // Source port
+  0x27, 0x0f,
+  // Destination port
+  0xac, 0x1e,
+  // Sequence number
+  0x00, 0x00, 0x00, 0x00,
+  // Acknowledgement Sequence number,
+  0x4b, 0x01, 0xe8, 0x9a,
+  // Offset
+  0x50,
+  // Flags
+  0x14,
+  // Window
+  0x00, 0x00,
+  // Checksum
+  0xa9, 0x05,
+  // Urgent
+  0x00, 0x00,
+  // END TCPv6 Header
+};
+// clang-format on
+
+}  // namespace
+
+TEST(TcpPacketTest, CreatedPacketMatchesReference) {
+  QuicStringPiece syn =
+      QuicStringPiece(reinterpret_cast<const char*>(kReferenceTCPSYNPacket),
+                      sizeof(kReferenceTCPSYNPacket));
+  QuicStringPiece expected_packet =
+      QuicStringPiece(reinterpret_cast<const char*>(kReferenceTCPRSTPacket),
+                      sizeof(kReferenceTCPRSTPacket));
+  CreateTcpResetPacket(syn, [&expected_packet](QuicStringPiece packet) {
+    QUIC_LOG(INFO) << QuicTextUtils::HexDump(packet);
+    ASSERT_EQ(packet, expected_packet);
+  });
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_client.cc b/quic/qbone/qbone_client.cc
new file mode 100644
index 0000000..f062d3f
--- /dev/null
+++ b/quic/qbone/qbone_client.cc
@@ -0,0 +1,98 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_client.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_epoll_alarm_factory.h"
+#include "net/third_party/quiche/src/quic/core/quic_epoll_connection_helper.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_epoll.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_exported_stats.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_stream.h"
+
+namespace quic {
+namespace {
+std::unique_ptr<QuicClientBase::NetworkHelper> CreateNetworkHelper(
+    QuicEpollServer* epoll_server,
+    QboneClient* client) {
+  std::unique_ptr<QuicClientBase::NetworkHelper> helper =
+      QuicMakeUnique<QuicClientEpollNetworkHelper>(epoll_server, client);
+  testing::testvalue::Adjust("QboneClient/network_helper", &helper);
+  return helper;
+}
+}  // namespace
+
+QboneClient::QboneClient(QuicSocketAddress server_address,
+                         const QuicServerId& server_id,
+                         const ParsedQuicVersionVector& supported_versions,
+                         QuicSession::Visitor* session_owner,
+                         const QuicConfig& config,
+                         QuicEpollServer* epoll_server,
+                         std::unique_ptr<ProofVerifier> proof_verifier,
+                         QbonePacketWriter* qbone_writer,
+                         QboneClientControlStream::Handler* qbone_handler)
+    : QuicClientBase(
+          server_id,
+          supported_versions,
+          config,
+          new QuicEpollConnectionHelper(epoll_server, QuicAllocator::SIMPLE),
+          new QuicEpollAlarmFactory(epoll_server),
+          CreateNetworkHelper(epoll_server, this),
+          std::move(proof_verifier)),
+      qbone_writer_(qbone_writer),
+      qbone_handler_(qbone_handler),
+      session_owner_(session_owner) {
+  set_server_address(server_address);
+  crypto_config()->set_alpn("qbone");
+}
+
+QboneClient::~QboneClient() {
+  ResetSession();
+}
+
+QboneClientSession* QboneClient::qbone_session() {
+  return static_cast<QboneClientSession*>(QuicClientBase::session());
+}
+
+void QboneClient::ProcessPacketFromNetwork(QuicStringPiece packet) {
+  qbone_session()->ProcessPacketFromNetwork(packet);
+}
+
+int QboneClient::GetNumSentClientHellosFromSession() {
+  return qbone_session()->GetNumSentClientHellos();
+}
+
+int QboneClient::GetNumReceivedServerConfigUpdatesFromSession() {
+  return qbone_session()->GetNumReceivedServerConfigUpdates();
+}
+
+void QboneClient::ResendSavedData() {
+  // no op.
+}
+
+void QboneClient::ClearDataToResend() {
+  // no op.
+}
+
+bool QboneClient::HasActiveRequests() {
+  return qbone_session()->HasActiveRequests();
+}
+
+class QboneClientSessionWithConnection : public QboneClientSession {
+ public:
+  using QboneClientSession::QboneClientSession;
+
+  ~QboneClientSessionWithConnection() override { delete connection(); }
+};
+
+// Takes ownership of |connection|.
+std::unique_ptr<QuicSession> QboneClient::CreateQuicClientSession(
+    const ParsedQuicVersionVector& supported_versions,
+    QuicConnection* connection) {
+  return QuicMakeUnique<QboneClientSessionWithConnection>(
+      connection, crypto_config(), session_owner(), *config(),
+      supported_versions, server_id(), qbone_writer_, qbone_handler_);
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_client.h b/quic/qbone/qbone_client.h
new file mode 100644
index 0000000..a0fe4fc
--- /dev/null
+++ b/quic/qbone/qbone_client.h
@@ -0,0 +1,74 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_CLIENT_H_
+#define QUICHE_QUIC_QBONE_QBONE_CLIENT_H_
+
+#include "net/third_party/quiche/src/quic/qbone/qbone_client_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_client_session.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_writer.h"
+#include "net/third_party/quiche/src/quic/tools/quic_client_base.h"
+#include "net/third_party/quiche/src/quic/tools/quic_client_epoll_network_helper.h"
+
+namespace quic {
+// A QboneClient encapsulates connecting to a server via an epoll server
+// and setting up a Qbone tunnel. See the QboneTestClient in qbone_client_test
+// for usage.
+class QboneClient : public QuicClientBase, public QboneClientInterface {
+ public:
+  // Note that the epoll server, qbone writer, and handler are owned
+  // by the caller.
+  QboneClient(QuicSocketAddress server_address,
+              const QuicServerId& server_id,
+              const ParsedQuicVersionVector& supported_versions,
+              QuicSession::Visitor* session_owner,
+              const QuicConfig& config,
+              QuicEpollServer* epoll_server,
+              std::unique_ptr<ProofVerifier> proof_verifier,
+              QbonePacketWriter* qbone_writer,
+              QboneClientControlStream::Handler* qbone_handler);
+  ~QboneClient() override;
+  QboneClientSession* qbone_session();
+
+  // From QboneClientInterface. Accepts a given packet from the network and
+  // sends the packet down to the QBONE connection.
+  void ProcessPacketFromNetwork(QuicStringPiece packet) override;
+
+ protected:
+  int GetNumSentClientHellosFromSession() override;
+  int GetNumReceivedServerConfigUpdatesFromSession() override;
+
+  // This client does not resend saved data. This will be a no-op.
+  void ResendSavedData() override;
+
+  // This client does not resend saved data. This will be a no-op.
+  void ClearDataToResend() override;
+
+  // Takes ownership of |connection|.
+  std::unique_ptr<QuicSession> CreateQuicClientSession(
+      const ParsedQuicVersionVector& supported_versions,
+      QuicConnection* connection) override;
+
+  QbonePacketWriter* qbone_writer() { return qbone_writer_; }
+
+  QboneClientControlStream::Handler* qbone_control_handler() {
+    return qbone_handler_;
+  }
+
+  QuicSession::Visitor* session_owner() {
+    return session_owner_;
+  }
+
+  bool HasActiveRequests() override;
+
+ private:
+  QbonePacketWriter* qbone_writer_;
+  QboneClientControlStream::Handler* qbone_handler_;
+
+  QuicSession::Visitor* session_owner_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_CLIENT_H_
diff --git a/quic/qbone/qbone_client_interface.h b/quic/qbone/qbone_client_interface.h
new file mode 100644
index 0000000..28d88ac
--- /dev/null
+++ b/quic/qbone/qbone_client_interface.h
@@ -0,0 +1,25 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_CLIENT_INTERFACE_H_
+#define QUICHE_QUIC_QBONE_QBONE_CLIENT_INTERFACE_H_
+
+#include <cstdint>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_string_piece.h"
+
+namespace quic {
+
+// An interface that includes methods to interact with a QBONE client.
+class QboneClientInterface {
+ public:
+  virtual ~QboneClientInterface() {}
+  // Accepts a given packet from the network and sends the packet down to the
+  // QBONE connection.
+  virtual void ProcessPacketFromNetwork(QuicStringPiece packet) = 0;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_CLIENT_INTERFACE_H_
diff --git a/quic/qbone/qbone_client_session.cc b/quic/qbone/qbone_client_session.cc
new file mode 100644
index 0000000..7bd401d
--- /dev/null
+++ b/quic/qbone/qbone_client_session.cc
@@ -0,0 +1,88 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_client_session.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_types.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+
+namespace quic {
+
+QboneClientSession::QboneClientSession(
+    QuicConnection* connection,
+    QuicCryptoClientConfig* quic_crypto_client_config,
+    QuicSession::Visitor* owner,
+    const QuicConfig& config,
+    const ParsedQuicVersionVector& supported_versions,
+    const QuicServerId& server_id,
+    QbonePacketWriter* writer,
+    QboneClientControlStream::Handler* handler)
+    : QboneSessionBase(connection, owner, config, supported_versions, writer),
+      server_id_(server_id),
+      quic_crypto_client_config_(quic_crypto_client_config),
+      handler_(handler) {}
+
+QboneClientSession::~QboneClientSession() {}
+
+std::unique_ptr<QuicCryptoStream> QboneClientSession::CreateCryptoStream() {
+  return QuicMakeUnique<QuicCryptoClientStream>(
+      server_id_, this, nullptr, quic_crypto_client_config_, this);
+}
+
+void QboneClientSession::Initialize() {
+  // Initialize must be called first, as that's what generates the crypto
+  // stream.
+  QboneSessionBase::Initialize();
+  static_cast<QuicCryptoClientStreamBase*>(GetMutableCryptoStream())
+      ->CryptoConnect();
+  // Register the reserved control stream.
+  QuicStreamId next_id = GetNextOutgoingBidirectionalStreamId();
+  DCHECK_EQ(next_id, QboneConstants::GetControlStreamId(
+                         connection()->transport_version()));
+  auto control_stream =
+      QuicMakeUnique<QboneClientControlStream>(this, handler_);
+  control_stream_ = control_stream.get();
+  RegisterStaticStream(std::move(control_stream),
+                       /*stream_already_counted = */ false);
+}
+
+int QboneClientSession::GetNumSentClientHellos() const {
+  return static_cast<const QuicCryptoClientStreamBase*>(GetCryptoStream())
+      ->num_sent_client_hellos();
+}
+
+int QboneClientSession::GetNumReceivedServerConfigUpdates() const {
+  return static_cast<const QuicCryptoClientStreamBase*>(GetCryptoStream())
+      ->num_scup_messages_received();
+}
+
+bool QboneClientSession::SendServerRequest(const QboneServerRequest& request) {
+  if (!control_stream_) {
+    QUIC_BUG << "Cannot send server request before control stream is created.";
+    return false;
+  }
+  return control_stream_->SendRequest(request);
+}
+
+void QboneClientSession::ProcessPacketFromNetwork(QuicStringPiece packet) {
+  SendPacketToPeer(packet);
+}
+
+void QboneClientSession::ProcessPacketFromPeer(QuicStringPiece packet) {
+  writer_->WritePacketToNetwork(packet.data(), packet.size());
+}
+
+void QboneClientSession::OnProofValid(
+    const QuicCryptoClientConfig::CachedState& cached) {}
+
+void QboneClientSession::OnProofVerifyDetailsAvailable(
+    const ProofVerifyDetails& verify_details) {}
+
+bool QboneClientSession::HasActiveRequests() const {
+  return (stream_map().size() - num_incoming_static_streams() -
+          num_outgoing_static_streams()) > 0;
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_client_session.h b/quic/qbone/qbone_client_session.h
new file mode 100644
index 0000000..5dcf2ac
--- /dev/null
+++ b/quic/qbone/qbone_client_session.h
@@ -0,0 +1,76 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_CLIENT_SESSION_H_
+#define QUICHE_QUIC_QBONE_QBONE_CLIENT_SESSION_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_crypto_client_stream.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_export.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_control.pb.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_control_stream.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_writer.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_session_base.h"
+
+namespace quic {
+
+class QUIC_EXPORT_PRIVATE QboneClientSession
+    : public QboneSessionBase,
+      public QuicCryptoClientStream::ProofHandler {
+ public:
+  QboneClientSession(QuicConnection* connection,
+                     QuicCryptoClientConfig* quic_crypto_client_config,
+                     QuicSession::Visitor* owner,
+                     const QuicConfig& config,
+                     const ParsedQuicVersionVector& supported_versions,
+                     const QuicServerId& server_id,
+                     QbonePacketWriter* writer,
+                     QboneClientControlStream::Handler* handler);
+  QboneClientSession(const QboneClientSession&) = delete;
+  QboneClientSession& operator=(const QboneClientSession&) = delete;
+  ~QboneClientSession() override;
+
+  // QuicSession overrides. This will initiate the crypto stream.
+  void Initialize() override;
+
+  // Returns the number of client hello messages that have been sent on the
+  // crypto stream. If the handshake has completed then this is one greater
+  // than the number of round-trips needed for the handshake.
+  int GetNumSentClientHellos() const;
+  int GetNumReceivedServerConfigUpdates() const;
+
+  bool SendServerRequest(const QboneServerRequest& request);
+
+  void ProcessPacketFromNetwork(QuicStringPiece packet) override;
+  void ProcessPacketFromPeer(QuicStringPiece packet) override;
+
+  // Returns true if there are active requests on this session.
+  bool HasActiveRequests() const;
+
+ protected:
+  // QboneSessionBase interface implementation.
+  std::unique_ptr<QuicCryptoStream> CreateCryptoStream() override;
+
+  // ProofHandler interface implementation.
+  void OnProofValid(const QuicCryptoClientConfig::CachedState& cached) override;
+  void OnProofVerifyDetailsAvailable(
+      const ProofVerifyDetails& verify_details) override;
+
+  QuicServerId server_id() { return server_id_; }
+  QuicCryptoClientConfig* crypto_client_config() {
+    return quic_crypto_client_config_;
+  }
+
+ private:
+  QuicServerId server_id_;
+  // Config for QUIC crypto client stream, used by the client.
+  QuicCryptoClientConfig* quic_crypto_client_config_;
+  // Passed to the control stream.
+  QboneClientControlStream::Handler* handler_;
+  // The unowned control stream.
+  QboneClientControlStream* control_stream_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_CLIENT_SESSION_H_
diff --git a/quic/qbone/qbone_client_test.cc b/quic/qbone/qbone_client_test.cc
new file mode 100644
index 0000000..78e533c
--- /dev/null
+++ b/quic/qbone/qbone_client_test.cc
@@ -0,0 +1,258 @@
+// Copyright (c) 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Sets up a dispatcher and sends requests via the QboneClient.
+
+#include "net/third_party/quiche/src/quic/qbone/qbone_client.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_alarm_factory.h"
+#include "net/third_party/quiche/src/quic/core/quic_default_packet_writer.h"
+#include "net/third_party/quiche/src/quic/core/quic_dispatcher.h"
+#include "net/third_party/quiche/src/quic/core/quic_epoll_alarm_factory.h"
+#include "net/third_party/quiche/src/quic/core/quic_epoll_connection_helper.h"
+#include "net/third_party/quiche/src/quic/core/quic_packet_reader.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_mutex.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_port_utils.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_socket_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test_loopback.h"
+#include "net/quic/platform/impl/quic_socket_utils.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_processor_test_tools.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_server_session.h"
+#include "net/third_party/quiche/src/quic/test_tools/crypto_test_utils.h"
+#include "net/third_party/quiche/src/quic/test_tools/quic_server_peer.h"
+#include "net/third_party/quiche/src/quic/test_tools/server_thread.h"
+#include "net/third_party/quiche/src/quic/tools/quic_memory_cache_backend.h"
+#include "net/third_party/quiche/src/quic/tools/quic_server.h"
+
+namespace quic {
+namespace test {
+namespace {
+
+string TestPacketIn(const string& body) {
+  return PrependIPv6HeaderForTest(body, 5);
+}
+
+string TestPacketOut(const string& body) {
+  return PrependIPv6HeaderForTest(body, 4);
+}
+
+class DataSavingQbonePacketWriter : public QbonePacketWriter {
+ public:
+  void WritePacketToNetwork(const char* packet, size_t size) override {
+    QuicWriterMutexLock lock(&mu_);
+    data_.push_back(string(packet, size));
+  }
+
+  std::vector<string> data() {
+    QuicWriterMutexLock lock(&mu_);
+    return data_;
+  }
+
+ private:
+  QuicMutex mu_;
+  std::vector<string> data_;
+};
+
+// A subclass of a qbone session that will own the connection passed in.
+class ConnectionOwningQboneServerSession : public QboneServerSession {
+ public:
+  ConnectionOwningQboneServerSession(
+      const ParsedQuicVersionVector& supported_versions,
+      QuicConnection* connection,
+      Visitor* owner,
+      const QuicConfig& config,
+      const QuicCryptoServerConfig* quic_crypto_server_config,
+      QuicCompressedCertsCache* compressed_certs_cache,
+      QbonePacketWriter* writer)
+      : QboneServerSession(supported_versions,
+                           connection,
+                           owner,
+                           config,
+                           quic_crypto_server_config,
+                           compressed_certs_cache,
+                           writer,
+                           TestLoopback6(),
+                           TestLoopback6(),
+                           64,
+                           nullptr),
+        connection_(connection) {}
+
+ private:
+  // Note that we don't expect the QboneServerSession or any of its parent
+  // classes to do anything with the connection_ in their destructors.
+  std::unique_ptr<QuicConnection> connection_;
+};
+
+class QuicQboneDispatcher : public QuicDispatcher {
+ public:
+  QuicQboneDispatcher(
+      const QuicConfig* config,
+      const QuicCryptoServerConfig* crypto_config,
+      QuicVersionManager* version_manager,
+      std::unique_ptr<QuicConnectionHelperInterface> helper,
+      std::unique_ptr<QuicCryptoServerStream::Helper> session_helper,
+      std::unique_ptr<QuicAlarmFactory> alarm_factory,
+      QbonePacketWriter* writer)
+      : QuicDispatcher(config,
+                       crypto_config,
+                       version_manager,
+                       std::move(helper),
+                       std::move(session_helper),
+                       std::move(alarm_factory),
+                       kQuicDefaultConnectionIdLength),
+        writer_(writer) {}
+
+  QuicSession* CreateQuicSession(
+      QuicConnectionId id,
+      const QuicSocketAddress& client,
+      QuicStringPiece alpn,
+      const quic::ParsedQuicVersion& version) override {
+    CHECK_EQ(alpn, "qbone");
+    QuicConnection* connection =
+        new QuicConnection(id, client, helper(), alarm_factory(), writer(),
+                           /* owns_writer= */ false, Perspective::IS_SERVER,
+                           ParsedQuicVersionVector{version});
+    // The connection owning wrapper owns the connection created.
+    QboneServerSession* session = new ConnectionOwningQboneServerSession(
+        GetSupportedVersions(), connection, this, config(), crypto_config(),
+        compressed_certs_cache(), writer_);
+    session->Initialize();
+    return session;
+  }
+
+  QuicConnectionId GenerateNewServerConnectionId(
+      ParsedQuicVersion version,
+      QuicConnectionId connection_id) const override {
+    char connection_id_bytes[kQuicDefaultConnectionIdLength] = {};
+    return QuicConnectionId(connection_id_bytes, sizeof(connection_id_bytes));
+  }
+
+ private:
+  QbonePacketWriter* writer_;
+};
+
+class QboneTestServer : public QuicServer {
+ public:
+  explicit QboneTestServer(std::unique_ptr<ProofSource> proof_source)
+      : QuicServer(std::move(proof_source), &response_cache_) {}
+  QuicDispatcher* CreateQuicDispatcher() override {
+    QuicEpollAlarmFactory alarm_factory(epoll_server());
+    return new QuicQboneDispatcher(
+        &config(), &crypto_config(), version_manager(),
+        std::unique_ptr<QuicEpollConnectionHelper>(
+            new QuicEpollConnectionHelper(epoll_server(),
+                                          QuicAllocator::BUFFER_POOL)),
+        std::unique_ptr<QuicCryptoServerStream::Helper>(
+            new QboneCryptoServerStreamHelper()),
+        std::unique_ptr<QuicEpollAlarmFactory>(
+            new QuicEpollAlarmFactory(epoll_server())),
+        &writer_);
+  }
+
+  std::vector<string> data() { return writer_.data(); }
+
+  void WaitForDataSize(int n) {
+    while (data().size() != n) {
+    }
+  }
+
+ private:
+  quic::QuicMemoryCacheBackend response_cache_;
+  DataSavingQbonePacketWriter writer_;
+};
+
+class QboneTestClient : public QboneClient {
+ public:
+  QboneTestClient(QuicSocketAddress server_address,
+                  const QuicServerId& server_id,
+                  const ParsedQuicVersionVector& supported_versions,
+                  QuicEpollServer* epoll_server,
+                  std::unique_ptr<ProofVerifier> proof_verifier)
+      : QboneClient(server_address,
+                    server_id,
+                    supported_versions,
+                    /*session_owner=*/nullptr,
+                    QuicConfig(),
+                    epoll_server,
+                    std::move(proof_verifier),
+                    &qbone_writer_,
+                    nullptr) {}
+
+  ~QboneTestClient() override {}
+
+  void SendData(const string& data) {
+    qbone_session()->ProcessPacketFromNetwork(data);
+  }
+
+  void WaitForWriteToFlush() {
+    while (connected() && session()->HasDataToWrite()) {
+      WaitForEvents();
+    }
+  }
+
+  void WaitForDataSize(int n) {
+    while (data().size() != n) {
+      WaitForEvents();
+    }
+  }
+
+  std::vector<string> data() { return qbone_writer_.data(); }
+
+ private:
+  DataSavingQbonePacketWriter qbone_writer_;
+};
+
+TEST(QboneClientTest, SendDataFromClient) {
+  SetQuicReloadableFlag(quic_use_parse_public_header, true);
+  auto server = new QboneTestServer(crypto_test_utils::ProofSourceForTesting());
+  QuicSocketAddress server_address(TestLoopback(), QuicPickUnusedPortOrDie());
+  ServerThread server_thread(server, server_address);
+  server_thread.Initialize();
+  server_thread.Start();
+
+  QuicEpollServer epoll_server;
+  QboneTestClient client(
+      server_address,
+      QuicServerId("test.example.com", server_address.port(), false),
+      AllSupportedVersions(), &epoll_server,
+      crypto_test_utils::ProofVerifierForTesting());
+  ASSERT_TRUE(client.Initialize());
+  ASSERT_TRUE(client.Connect());
+  ASSERT_TRUE(client.WaitForCryptoHandshakeConfirmed());
+  client.SendData(TestPacketIn("hello"));
+  client.SendData(TestPacketIn("world"));
+  client.WaitForWriteToFlush();
+  server->WaitForDataSize(2);
+  EXPECT_THAT(server->data()[0], testing::Eq(TestPacketOut("hello")));
+  EXPECT_THAT(server->data()[1], testing::Eq(TestPacketOut("world")));
+  auto server_session =
+      static_cast<QboneServerSession*>(QuicServerPeer::GetDispatcher(server)
+                                           ->session_map()
+                                           .begin()
+                                           ->second.get());
+  string long_data(QboneConstants::kMaxQbonePacketBytes - sizeof(ip6_hdr) - 1,
+                   'A');
+  // Pretend the server gets data.
+  server_thread.Schedule([&server_session, &long_data]() {
+    server_session->ProcessPacketFromNetwork(
+        TestPacketIn("Somethingsomething"));
+    server_session->ProcessPacketFromNetwork(TestPacketIn(long_data));
+    server_session->ProcessPacketFromNetwork(TestPacketIn(long_data));
+  });
+  client.WaitForDataSize(3);
+  EXPECT_THAT(client.data()[0],
+              testing::Eq(TestPacketOut("Somethingsomething")));
+  EXPECT_THAT(client.data()[1], testing::Eq(TestPacketOut(long_data)));
+  EXPECT_THAT(client.data()[2], testing::Eq(TestPacketOut(long_data)));
+
+  client.Disconnect();
+  server_thread.Quit();
+  server_thread.Join();
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace quic
diff --git a/quic/qbone/qbone_constants.cc b/quic/qbone/qbone_constants.cc
new file mode 100644
index 0000000..a83f74e
--- /dev/null
+++ b/quic/qbone/qbone_constants.cc
@@ -0,0 +1,36 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_constants.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_utils.h"
+
+namespace quic {
+
+constexpr char QboneConstants::kQboneAlpn[];
+const QuicByteCount QboneConstants::kMaxQbonePacketBytes;
+const uint32_t QboneConstants::kQboneRouteTableId;
+
+QuicStreamId QboneConstants::GetControlStreamId(QuicTransportVersion version) {
+  return QuicUtils::GetFirstBidirectionalStreamId(version,
+                                                  Perspective::IS_CLIENT);
+}
+
+const QuicIpAddress* QboneConstants::TerminatorLocalAddress() {
+  static auto* terminator_address = []() {
+    QuicIpAddress* address = new QuicIpAddress;
+    // 0x71 0x62 0x6f 0x6e 0x65 is 'qbone' in ascii.
+    address->FromString("fe80::71:626f:6e65");
+    return address;
+  }();
+  return terminator_address;
+}
+
+const IpRange* QboneConstants::TerminatorLocalAddressRange() {
+  static auto* range =
+      new quic::IpRange(*quic::QboneConstants::TerminatorLocalAddress(), 128);
+  return range;
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_constants.h b/quic/qbone/qbone_constants.h
new file mode 100644
index 0000000..1fa2688
--- /dev/null
+++ b/quic/qbone/qbone_constants.h
@@ -0,0 +1,32 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_CONSTANTS_H_
+#define QUICHE_QUIC_QBONE_QBONE_CONSTANTS_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_types.h"
+#include "net/third_party/quiche/src/quic/core/quic_versions.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/ip_range.h"
+
+namespace quic {
+
+struct QboneConstants {
+  // Qbone's ALPN
+  static constexpr char kQboneAlpn[] = "qbone";
+  // The maximum number of bytes allowed in a qbone packet.
+  static const QuicByteCount kMaxQbonePacketBytes = 2000;
+  // The table id for Qbone's routing table. 'bone' in ascii.
+  static const uint32_t kQboneRouteTableId = 0x626F6E65;
+  // The stream ID of the control channel.
+  static QuicStreamId GetControlStreamId(QuicTransportVersion version);
+  // The link-local address of the Terminator
+  static const QuicIpAddress* TerminatorLocalAddress();
+  // The IPRange containing the TerminatorLocalAddress
+  static const IpRange* TerminatorLocalAddressRange();
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_CONSTANTS_H_
diff --git a/quic/qbone/qbone_control.proto b/quic/qbone/qbone_control.proto
new file mode 100644
index 0000000..f0090d6
--- /dev/null
+++ b/quic/qbone/qbone_control.proto
@@ -0,0 +1,13 @@
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package quic;
+
+message QboneServerRequest {
+  extensions 1000 to max;
+};
+
+message QboneClientRequest {
+  extensions 1000 to max;
+};
diff --git a/quic/qbone/qbone_control_placeholder.proto b/quic/qbone/qbone_control_placeholder.proto
new file mode 100644
index 0000000..375b015
--- /dev/null
+++ b/quic/qbone/qbone_control_placeholder.proto
@@ -0,0 +1,20 @@
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package quic;
+
+import "net/third_party/quiche/src/quic/qbone/qbone_control.proto";
+
+// These provide fields for QboneServerRequest and QboneClientRequest that are
+// used to test the control channel.  Once the control channel actually has real
+// data to pass they can be removed.
+// TODO(b/62139999): Remove this file in favor of testing actual configuration.
+
+extend QboneServerRequest {
+  optional string server_placeholder = 179838467;
+}
+
+extend QboneClientRequest {
+  optional string client_placeholder = 179838467;
+}
diff --git a/quic/qbone/qbone_control_stream.cc b/quic/qbone/qbone_control_stream.cc
new file mode 100644
index 0000000..6272f3f
--- /dev/null
+++ b/quic/qbone/qbone_control_stream.cc
@@ -0,0 +1,67 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_control_stream.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_session.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+
+namespace quic {
+
+namespace {
+static constexpr size_t kRequestSizeBytes = sizeof(uint16_t);
+}  // namespace
+
+QboneControlStreamBase::QboneControlStreamBase(QuicSession* session)
+    : QuicStream(QboneConstants::GetControlStreamId(
+                     session->connection()->transport_version()),
+                 session,
+                 /*is_static=*/true,
+                 BIDIRECTIONAL),
+      pending_message_size_(0) {}
+
+void QboneControlStreamBase::OnDataAvailable() {
+  sequencer()->Read(&buffer_);
+  while (true) {
+    if (pending_message_size_ == 0) {
+      // Start of a message.
+      if (buffer_.size() < kRequestSizeBytes) {
+        return;
+      }
+      memcpy(&pending_message_size_, buffer_.data(), kRequestSizeBytes);
+      buffer_.erase(0, kRequestSizeBytes);
+    }
+    // Continuation of a message.
+    if (buffer_.size() < pending_message_size_) {
+      return;
+    }
+    string tmp = buffer_.substr(0, pending_message_size_);
+    buffer_.erase(0, pending_message_size_);
+    pending_message_size_ = 0;
+    OnMessage(tmp);
+  }
+}
+
+bool QboneControlStreamBase::SendMessage(const proto2::Message& proto) {
+  string tmp;
+  if (!proto.SerializeToString(&tmp)) {
+    QUIC_BUG << "Failed to serialize QboneControlRequest";
+    return false;
+  }
+  if (tmp.size() > kuint16max) {
+    QUIC_BUG << "QboneControlRequest too large: " << tmp.size() << " > "
+             << kuint16max;
+    return false;
+  }
+  uint16_t size = tmp.size();
+  char size_str[kRequestSizeBytes];
+  memcpy(size_str, &size, kRequestSizeBytes);
+  WriteOrBufferData(QuicStringPiece(size_str, kRequestSizeBytes), false,
+                    nullptr);
+  WriteOrBufferData(tmp, false, nullptr);
+  return true;
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_control_stream.h b/quic/qbone/qbone_control_stream.h
new file mode 100644
index 0000000..6e3dead
--- /dev/null
+++ b/quic/qbone/qbone_control_stream.h
@@ -0,0 +1,75 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_CONTROL_STREAM_H_
+#define QUICHE_QUIC_QBONE_QBONE_CONTROL_STREAM_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_stream.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_control.pb.h"
+
+namespace quic {
+
+class QboneSessionBase;
+
+class QUIC_EXPORT_PRIVATE QboneControlStreamBase : public QuicStream {
+ public:
+  explicit QboneControlStreamBase(QuicSession* session);
+
+  void OnDataAvailable() override;
+
+ protected:
+  virtual void OnMessage(const string& data) = 0;
+  bool SendMessage(const proto2::Message& proto);
+
+ private:
+  uint16_t pending_message_size_;
+  string buffer_;
+};
+
+template <class T>
+class QUIC_EXPORT_PRIVATE QboneControlHandler {
+ public:
+  virtual ~QboneControlHandler() { }
+
+  virtual void OnControlRequest(const T& request) = 0;
+  virtual void OnControlError() = 0;
+};
+
+template <class Incoming, class Outgoing>
+class QUIC_EXPORT_PRIVATE QboneControlStream : public QboneControlStreamBase {
+ public:
+  using Handler = QboneControlHandler<Incoming>;
+
+  QboneControlStream(QuicSession* session, Handler* handler)
+      : QboneControlStreamBase(session), handler_(handler) {}
+
+  bool SendRequest(const Outgoing& request) { return SendMessage(request); }
+
+ protected:
+  void OnMessage(const string& data) override {
+    Incoming request;
+    if (!request.ParseFromString(data)) {
+      QUIC_LOG(ERROR) << "Failed to parse incoming request";
+      if (handler_ != nullptr) {
+        handler_->OnControlError();
+      }
+      return;
+    }
+    if (handler_ != nullptr) {
+      handler_->OnControlRequest(request);
+    }
+  }
+
+ private:
+  Handler* handler_;
+};
+
+using QboneServerControlStream =
+    QboneControlStream<QboneServerRequest, QboneClientRequest>;
+using QboneClientControlStream =
+    QboneControlStream<QboneClientRequest, QboneServerRequest>;
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_CONTROL_STREAM_H_
diff --git a/quic/qbone/qbone_packet_exchanger.cc b/quic/qbone/qbone_packet_exchanger.cc
new file mode 100644
index 0000000..3f3a5f9
--- /dev/null
+++ b/quic/qbone/qbone_packet_exchanger.cc
@@ -0,0 +1,70 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_packet_exchanger.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+
+namespace quic {
+
+bool QbonePacketExchanger::ReadAndDeliverPacket(
+    QboneClientInterface* qbone_client) {
+  bool blocked = false;
+  string error;
+  std::unique_ptr<QuicData> packet = ReadPacket(&blocked, &error);
+  if (packet == nullptr) {
+    if (!blocked) {
+      visitor_->OnReadError(error);
+    }
+    return false;
+  }
+  qbone_client->ProcessPacketFromNetwork(packet->AsStringPiece());
+  return true;
+}
+
+void QbonePacketExchanger::WritePacketToNetwork(const char* packet,
+                                                size_t size) {
+  bool blocked = false;
+  string error;
+  if (packet_queue_.empty() && !write_blocked_) {
+    if (WritePacket(packet, size, &blocked, &error)) {
+      return;
+    }
+    if (!blocked) {
+      visitor_->OnWriteError(error);
+      return;
+    }
+    write_blocked_ = true;
+  }
+
+  // Drop the packet on the floor if the queue if full.
+  if (packet_queue_.size() >= max_pending_packets_) {
+    return;
+  }
+
+  auto data_copy = new char[size];
+  memcpy(data_copy, packet, size);
+  packet_queue_.push_back(
+      QuicMakeUnique<QuicData>(data_copy, size, /* owns_buffer = */ true));
+}
+
+void QbonePacketExchanger::SetWritable() {
+  write_blocked_ = false;
+  while (!packet_queue_.empty()) {
+    bool blocked = false;
+    string error;
+    if (WritePacket(packet_queue_.front()->data(),
+                    packet_queue_.front()->length(), &blocked, &error)) {
+      packet_queue_.pop_front();
+    } else {
+      if (!blocked) {
+        visitor_->OnWriteError(error);
+      }
+      write_blocked_ = blocked;
+      return;
+    }
+  }
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_packet_exchanger.h b/quic/qbone/qbone_packet_exchanger.h
new file mode 100644
index 0000000..8620a49
--- /dev/null
+++ b/quic/qbone/qbone_packet_exchanger.h
@@ -0,0 +1,80 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
+#define QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_packets.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_client_interface.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_writer.h"
+
+namespace quic {
+
+// Handles reading and writing on the local network and exchange packets between
+// the local network with a Qbone connection.
+class QbonePacketExchanger : public QbonePacketWriter {
+ public:
+  // The owner might want to receive notifications when read or write fails.
+  class Visitor {
+   public:
+    virtual ~Visitor() {}
+    virtual void OnReadError(const string& error) {}
+    virtual void OnWriteError(const string& error) {}
+  };
+  // Does not take ownership of visitor.
+  QbonePacketExchanger(Visitor* visitor, size_t max_pending_packets)
+      : visitor_(visitor), max_pending_packets_(max_pending_packets) {}
+
+  QbonePacketExchanger(const QbonePacketExchanger&) = delete;
+  QbonePacketExchanger& operator=(const QbonePacketExchanger&) = delete;
+
+  QbonePacketExchanger(QbonePacketExchanger&&) = delete;
+  QbonePacketExchanger& operator=(QbonePacketExchanger&&) = delete;
+
+  ~QbonePacketExchanger() = default;
+
+  // Returns true if there may be more packets to read.
+  // Implementations handles the actual raw read and delivers the packet to
+  // qbone_client.
+  bool ReadAndDeliverPacket(QboneClientInterface* qbone_client);
+
+  // From QbonePacketWriter.
+  // Writes a packet to the local network. If the write would be blocked, the
+  // packet will be queued if the queue is smaller than max_pending_packets_.
+  void WritePacketToNetwork(const char* packet, size_t size) override;
+
+  // The caller signifies that the local network is no longer blocked.
+  void SetWritable();
+
+ private:
+  // The actual implementation that reads a packet from the local network.
+  // Returns the packet if one is successfully read. This might nullptr when a)
+  // there is no packet to read, b) the read failed. In the former case, blocked
+  // is set to true. error contains the error message.
+  virtual std::unique_ptr<QuicData> ReadPacket(bool* blocked,
+                                               string* error) = 0;
+
+  // The actual implementation that writes a packet to the local network.
+  // Returns true if the write succeeds. blocked will be set to true if the
+  // write failure is caused by the local network being blocked. error contains
+  // the error message.
+  virtual bool WritePacket(const char* packet,
+                           size_t size,
+                           bool* blocked,
+                           string* error) = 0;
+
+  std::list<std::unique_ptr<QuicData>> packet_queue_;
+
+  Visitor* visitor_;
+
+  // The maximum number of packets that could be queued up when writing to local
+  // network is blocked.
+  size_t max_pending_packets_;
+
+  bool write_blocked_ = false;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_PACKET_EXCHANGER_H_
diff --git a/quic/qbone/qbone_packet_exchanger_test.cc b/quic/qbone/qbone_packet_exchanger_test.cc
new file mode 100644
index 0000000..4e63b99
--- /dev/null
+++ b/quic/qbone/qbone_packet_exchanger_test.cc
@@ -0,0 +1,253 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_packet_exchanger.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/mock_qbone_client.h"
+
+namespace quic {
+namespace {
+
+using ::testing::StrEq;
+using ::testing::StrictMock;
+
+const size_t kMaxPendingPackets = 2;
+
+class MockVisitor : public QbonePacketExchanger::Visitor {
+ public:
+  MOCK_METHOD1(OnReadError, void(const string&));
+  MOCK_METHOD1(OnWriteError, void(const string&));
+};
+
+class FakeQbonePacketExchanger : public QbonePacketExchanger {
+ public:
+  using QbonePacketExchanger::QbonePacketExchanger;
+
+  // Adds a packet to the end of list of packets to be returned by ReadPacket.
+  // When the list is empty, ReadPacket returns nullptr to signify error as
+  // defined by QbonePacketExchanger. If SetReadError is not called or called
+  // with empty error string, ReadPacket sets blocked to true.
+  void AddPacketToBeRead(std::unique_ptr<QuicData> packet) {
+    packets_to_be_read_.push_back(std::move(packet));
+  }
+
+  // Sets the error to be returned by ReadPacket when the list of packets is
+  // empty. If error is empty string, blocked is set by ReadPacket.
+  void SetReadError(const string& error) { read_error_ = error; }
+
+  // Force WritePacket to fail with the given status. WritePacket returns true
+  // when blocked == true and error is empty.
+  void ForceWriteFailure(bool blocked, const string& error) {
+    write_blocked_ = blocked;
+    write_error_ = error;
+  }
+
+  // Packets that have been successfully written by WritePacket.
+  const std::vector<string>& packets_written() const {
+    return packets_written_;
+  }
+
+ private:
+  // Implements QbonePacketExchanger::ReadPacket.
+  std::unique_ptr<QuicData> ReadPacket(bool* blocked, string* error) override {
+    *blocked = false;
+
+    if (packets_to_be_read_.empty()) {
+      *blocked = read_error_.empty();
+      *error = read_error_;
+      return nullptr;
+    }
+
+    std::unique_ptr<QuicData> packet = std::move(packets_to_be_read_.front());
+    packets_to_be_read_.pop_front();
+    return packet;
+  }
+
+  // Implements QbonePacketExchanger::WritePacket.
+  bool WritePacket(const char* packet,
+                   size_t size,
+                   bool* blocked,
+                   string* error) override {
+    *blocked = false;
+
+    if (write_blocked_ || !write_error_.empty()) {
+      *blocked = write_blocked_;
+      *error = write_error_;
+      return false;
+    }
+
+    packets_written_.push_back(string(packet, size));
+    return true;
+  }
+
+  string read_error_;
+  std::list<std::unique_ptr<QuicData>> packets_to_be_read_;
+
+  string write_error_;
+  bool write_blocked_ = false;
+  std::vector<string> packets_written_;
+};
+
+TEST(QbonePacketExchangerTest,
+     ReadAndDeliverPacketDeliversPacketToQboneClient) {
+  StrictMock<MockVisitor> visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  StrictMock<MockQboneClient> client;
+
+  string packet = "data";
+  exchanger.AddPacketToBeRead(
+      QuicMakeUnique<QuicData>(packet.data(), packet.length()));
+  EXPECT_CALL(client, ProcessPacketFromNetwork(StrEq("data")));
+
+  EXPECT_TRUE(exchanger.ReadAndDeliverPacket(&client));
+}
+
+TEST(QbonePacketExchangerTest,
+     ReadAndDeliverPacketNotifiesVisitorOnReadFailure) {
+  MockVisitor visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  MockQboneClient client;
+
+  // Force read error.
+  string io_error = "I/O error";
+  exchanger.SetReadError(io_error);
+  EXPECT_CALL(visitor, OnReadError(StrEq(io_error))).Times(1);
+
+  EXPECT_FALSE(exchanger.ReadAndDeliverPacket(&client));
+}
+
+TEST(QbonePacketExchangerTest,
+     ReadAndDeliverPacketDoesNotNotifyVisitorOnBlockedIO) {
+  MockVisitor visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  MockQboneClient client;
+
+  // No more packets to read.
+  EXPECT_FALSE(exchanger.ReadAndDeliverPacket(&client));
+}
+
+TEST(QbonePacketExchangerTest,
+     WritePacketToNetworkWritesDirectlyToNetworkWhenNotBlocked) {
+  MockVisitor visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  MockQboneClient client;
+
+  string packet = "data";
+  exchanger.WritePacketToNetwork(packet.data(), packet.length());
+
+  ASSERT_EQ(exchanger.packets_written().size(), 1);
+  EXPECT_THAT(exchanger.packets_written()[0], StrEq(packet));
+}
+
+TEST(QbonePacketExchangerTest,
+     WritePacketToNetworkQueuesPacketsAndProcessThemLater) {
+  MockVisitor visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  MockQboneClient client;
+
+  // Force write to be blocked so that packets are queued.
+  exchanger.ForceWriteFailure(true, "");
+  std::vector<string> packets = {"packet0", "packet1"};
+  for (int i = 0; i < packets.size(); i++) {
+    exchanger.WritePacketToNetwork(packets[i].data(), packets[i].length());
+  }
+
+  // Nothing should have been written because of blockage.
+  ASSERT_TRUE(exchanger.packets_written().empty());
+
+  // Remove blockage and start proccessing queued packets.
+  exchanger.ForceWriteFailure(false, "");
+  exchanger.SetWritable();
+
+  // Queued packets are processed.
+  ASSERT_EQ(exchanger.packets_written().size(), 2);
+  for (int i = 0; i < packets.size(); i++) {
+    EXPECT_THAT(exchanger.packets_written()[i], StrEq(packets[i]));
+  }
+}
+
+TEST(QbonePacketExchangerTest,
+     SetWritableContinuesProcessingPacketIfPreviousCallBlocked) {
+  MockVisitor visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  MockQboneClient client;
+
+  // Force write to be blocked so that packets are queued.
+  exchanger.ForceWriteFailure(true, "");
+  std::vector<string> packets = {"packet0", "packet1"};
+  for (int i = 0; i < packets.size(); i++) {
+    exchanger.WritePacketToNetwork(packets[i].data(), packets[i].length());
+  }
+
+  // Nothing should have been written because of blockage.
+  ASSERT_TRUE(exchanger.packets_written().empty());
+
+  // Start processing packets, but since writes are still blocked, nothing
+  // should have been written.
+  exchanger.SetWritable();
+  ASSERT_TRUE(exchanger.packets_written().empty());
+
+  // Remove blockage and start processing packets again.
+  exchanger.ForceWriteFailure(false, "");
+  exchanger.SetWritable();
+
+  ASSERT_EQ(exchanger.packets_written().size(), 2);
+  for (int i = 0; i < packets.size(); i++) {
+    EXPECT_THAT(exchanger.packets_written()[i], StrEq(packets[i]));
+  }
+}
+
+TEST(QbonePacketExchangerTest, WritePacketToNetworkDropsPacketIfQueueIfFull) {
+  std::vector<string> packets = {"packet0", "packet1", "packet2"};
+  size_t queue_size = packets.size() - 1;
+  MockVisitor visitor;
+  // exchanger has smaller queue than number of packets.
+  FakeQbonePacketExchanger exchanger(&visitor, queue_size);
+  MockQboneClient client;
+
+  exchanger.ForceWriteFailure(true, "");
+  for (int i = 0; i < packets.size(); i++) {
+    exchanger.WritePacketToNetwork(packets[i].data(), packets[i].length());
+  }
+
+  // Blocked writes cause packets to be queued or dropped.
+  ASSERT_TRUE(exchanger.packets_written().empty());
+
+  exchanger.ForceWriteFailure(false, "");
+  exchanger.SetWritable();
+
+  ASSERT_EQ(exchanger.packets_written().size(), queue_size);
+  for (int i = 0; i < queue_size; i++) {
+    EXPECT_THAT(exchanger.packets_written()[i], StrEq(packets[i]));
+  }
+}
+
+TEST(QbonePacketExchangerTest, WriteErrorsGetNotified) {
+  MockVisitor visitor;
+  FakeQbonePacketExchanger exchanger(&visitor, kMaxPendingPackets);
+  MockQboneClient client;
+  string packet = "data";
+
+  // Write error is delivered to visitor during WritePacketToNetwork.
+  string io_error = "I/O error";
+  exchanger.ForceWriteFailure(false, io_error);
+  EXPECT_CALL(visitor, OnWriteError(StrEq(io_error))).Times(1);
+  exchanger.WritePacketToNetwork(packet.data(), packet.length());
+  ASSERT_TRUE(exchanger.packets_written().empty());
+
+  // Write error is delivered to visitor during SetWritable.
+  exchanger.ForceWriteFailure(true, "");
+  exchanger.WritePacketToNetwork(packet.data(), packet.length());
+
+  string sys_error = "sys error";
+  exchanger.ForceWriteFailure(false, sys_error);
+  EXPECT_CALL(visitor, OnWriteError(StrEq(sys_error))).Times(1);
+  exchanger.SetWritable();
+  ASSERT_TRUE(exchanger.packets_written().empty());
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/qbone_packet_processor.cc b/quic/qbone/qbone_packet_processor.cc
new file mode 100644
index 0000000..db7a138
--- /dev/null
+++ b/quic/qbone/qbone_packet_processor.cc
@@ -0,0 +1,269 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_packet_processor.h"
+
+#include <cstring>
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_bug_tracker.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_endian.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_logging.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/icmp_packet.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/internet_checksum.h"
+#include "net/third_party/quiche/src/quic/qbone/platform/tcp_packet.h"
+
+namespace {
+
+constexpr size_t kIPv6AddressSize = 16;
+constexpr size_t kIPv6MinPacketSize = 1280;
+constexpr size_t kIcmpTtl = 64;
+constexpr size_t kICMPv6DestinationUnreachableDueToSourcePolicy = 5;
+
+}  // namespace
+
+namespace quic {
+
+const QuicIpAddress QbonePacketProcessor::kInvalidIpAddress =
+    QuicIpAddress::Any6();
+
+QbonePacketProcessor::QbonePacketProcessor(QuicIpAddress self_ip,
+                                           QuicIpAddress client_ip,
+                                           size_t client_ip_subnet_length,
+                                           OutputInterface* output,
+                                           StatsInterface* stats)
+    : client_ip_(client_ip),
+      output_(output),
+      stats_(stats),
+      filter_(new Filter) {
+  memcpy(self_ip_.s6_addr, self_ip.ToPackedString().data(), kIPv6AddressSize);
+  DCHECK_LE(client_ip_subnet_length, kIPv6AddressSize * 8);
+  client_ip_subnet_length_ = client_ip_subnet_length;
+
+  DCHECK(IpAddressFamily::IP_V6 == self_ip.address_family());
+  DCHECK(IpAddressFamily::IP_V6 == client_ip.address_family());
+  DCHECK(self_ip != kInvalidIpAddress);
+}
+
+QbonePacketProcessor::OutputInterface::~OutputInterface() {}
+QbonePacketProcessor::StatsInterface::~StatsInterface() {}
+QbonePacketProcessor::Filter::~Filter() {}
+
+QbonePacketProcessor::ProcessingResult
+QbonePacketProcessor::Filter::FilterPacket(Direction direction,
+                                           QuicStringPiece full_packet,
+                                           QuicStringPiece payload,
+                                           icmp6_hdr* icmp_header,
+                                           OutputInterface* output) {
+  return ProcessingResult::OK;
+}
+
+void QbonePacketProcessor::ProcessPacket(string* packet, Direction direction) {
+  if (QUIC_PREDICT_FALSE(!IsValid())) {
+    QUIC_BUG << "QuicPacketProcessor is invoked in an invalid state.";
+    stats_->OnPacketDroppedSilently(direction);
+    return;
+  }
+
+  uint8_t transport_protocol;
+  char* transport_data;
+  icmp6_hdr icmp_header;
+  memset(&icmp_header, 0, sizeof(icmp_header));
+  ProcessingResult result = ProcessIPv6HeaderAndFilter(
+      packet, direction, &transport_protocol, &transport_data, &icmp_header);
+
+  switch (result) {
+    case ProcessingResult::OK:
+      switch (direction) {
+        case Direction::FROM_CLIENT:
+          output_->SendPacketToNetwork(*packet);
+          break;
+        case Direction::FROM_NETWORK:
+          output_->SendPacketToClient(*packet);
+          break;
+      }
+      stats_->OnPacketForwarded(direction);
+      break;
+    case ProcessingResult::SILENT_DROP:
+      stats_->OnPacketDroppedSilently(direction);
+      break;
+    case ProcessingResult::DEFER:
+      stats_->OnPacketDeferred(direction);
+      break;
+    case ProcessingResult::ICMP:
+      SendIcmpResponse(&icmp_header, *packet, direction);
+      stats_->OnPacketDroppedWithIcmp(direction);
+      break;
+    case ProcessingResult::ICMP_AND_TCP_RESET:
+      SendIcmpResponse(&icmp_header, *packet, direction);
+      stats_->OnPacketDroppedWithIcmp(direction);
+      SendTcpReset(*packet, direction);
+      stats_->OnPacketDroppedWithTcpReset(direction);
+      break;
+  }
+}
+
+QbonePacketProcessor::ProcessingResult
+QbonePacketProcessor::ProcessIPv6HeaderAndFilter(string* packet,
+                                                 Direction direction,
+                                                 uint8_t* transport_protocol,
+                                                 char** transport_data,
+                                                 icmp6_hdr* icmp_header) {
+  ProcessingResult result = ProcessIPv6Header(
+      packet, direction, transport_protocol, transport_data, icmp_header);
+
+  if (result == ProcessingResult::OK) {
+    char* packet_data = &*packet->begin();
+    size_t header_size = *transport_data - packet_data;
+    // Sanity-check the bounds.
+    if (packet_data >= *transport_data || header_size > packet->size() ||
+        header_size < kIPv6HeaderSize) {
+      QUIC_BUG << "Invalid pointers encountered in "
+                  "QbonePacketProcessor::ProcessPacket.  Dropping the packet";
+      return ProcessingResult::SILENT_DROP;
+    }
+
+    result = filter_->FilterPacket(
+        direction, *packet,
+        QuicStringPiece(*transport_data, packet->size() - header_size),
+        icmp_header, output_);
+  }
+
+  // Do not send ICMP error messages in response to ICMP errors.
+  if (result == ProcessingResult::ICMP) {
+    const uint8_t* header = reinterpret_cast<const uint8_t*>(packet->data());
+
+    constexpr size_t kIPv6NextHeaderOffset = 6;
+    constexpr size_t kIcmpMessageTypeOffset = kIPv6HeaderSize + 0;
+    constexpr size_t kIcmpMessageTypeMaxError = 127;
+    if (
+        // Check size.
+        packet->size() >= (kIPv6HeaderSize + kICMPv6HeaderSize) &&
+        // Check that the packet is in fact ICMP.
+        header[kIPv6NextHeaderOffset] == IPPROTO_ICMPV6 &&
+        // Check that ICMP message type is an error.
+        header[kIcmpMessageTypeOffset] < kIcmpMessageTypeMaxError) {
+      result = ProcessingResult::SILENT_DROP;
+    }
+  }
+
+  return result;
+}
+
+QbonePacketProcessor::ProcessingResult QbonePacketProcessor::ProcessIPv6Header(
+    string* packet,
+    Direction direction,
+    uint8_t* transport_protocol,
+    char** transport_data,
+    icmp6_hdr* icmp_header) {
+  // Check if the packet is big enough to have IPv6 header.
+  if (packet->size() < kIPv6HeaderSize) {
+    QUIC_DVLOG(1) << "Dropped malformed packet: IPv6 header too short";
+    return ProcessingResult::SILENT_DROP;
+  }
+
+  // Check version field.
+  ip6_hdr* header = reinterpret_cast<ip6_hdr*>(&*packet->begin());
+  if (header->ip6_vfc >> 4 != 6) {
+    QUIC_DVLOG(1) << "Dropped malformed packet: IP version is not IPv6";
+    return ProcessingResult::SILENT_DROP;
+  }
+
+  // Check payload size.
+  const size_t declared_payload_size =
+      QuicEndian::NetToHost16(header->ip6_plen);
+  const size_t actual_payload_size = packet->size() - kIPv6HeaderSize;
+  if (declared_payload_size != actual_payload_size) {
+    QUIC_DVLOG(1)
+        << "Dropped malformed packet: incorrect packet length specified";
+    return ProcessingResult::SILENT_DROP;
+  }
+
+  // Check that the address of the client is in the packet.
+  QuicIpAddress address_to_check;
+  uint8_t address_reject_code;
+  bool ip_parse_result;
+  switch (direction) {
+    case Direction::FROM_CLIENT:
+      // Expect the source IP to match the client.
+      ip_parse_result = address_to_check.FromPackedString(
+          reinterpret_cast<const char*>(&header->ip6_src),
+          sizeof(header->ip6_src));
+      address_reject_code = kICMPv6DestinationUnreachableDueToSourcePolicy;
+      break;
+    case Direction::FROM_NETWORK:
+      // Expect the destination IP to match the client.
+      ip_parse_result = address_to_check.FromPackedString(
+          reinterpret_cast<const char*>(&header->ip6_dst),
+          sizeof(header->ip6_src));
+      address_reject_code = ICMP6_DST_UNREACH_NOROUTE;
+      break;
+  }
+  DCHECK(ip_parse_result);
+  if (!client_ip_.InSameSubnet(address_to_check, client_ip_subnet_length_)) {
+    QUIC_DVLOG(1)
+        << "Dropped packet: source/destination address is not client's";
+    icmp_header->icmp6_type = ICMP6_DST_UNREACH;
+    icmp_header->icmp6_code = address_reject_code;
+    return ProcessingResult::ICMP;
+  }
+
+  // Check and decrement TTL.
+  if (header->ip6_hops <= 1) {
+    icmp_header->icmp6_type = ICMP6_TIME_EXCEEDED;
+    icmp_header->icmp6_code = ICMP6_TIME_EXCEED_TRANSIT;
+    return ProcessingResult::ICMP;
+  }
+  header->ip6_hops--;
+
+  // Check and extract IP headers.
+  switch (header->ip6_nxt) {
+    case IPPROTO_TCP:
+    case IPPROTO_UDP:
+    case IPPROTO_ICMPV6:
+      *transport_protocol = header->ip6_nxt;
+      *transport_data = (&*packet->begin()) + kIPv6HeaderSize;
+      break;
+    default:
+      icmp_header->icmp6_type = ICMP6_PARAM_PROB;
+      icmp_header->icmp6_code = ICMP6_PARAMPROB_NEXTHEADER;
+      return ProcessingResult::ICMP;
+  }
+
+  return ProcessingResult::OK;
+}
+
+void QbonePacketProcessor::SendIcmpResponse(icmp6_hdr* icmp_header,
+                                            QuicStringPiece original_packet,
+                                            Direction original_direction) {
+  in6_addr dst;
+  // TODO(b/70339814): ensure this is actually a unicast address.
+  memcpy(dst.s6_addr, &original_packet[8], kIPv6AddressSize);
+
+  CreateIcmpPacket(self_ip_, dst, *icmp_header, original_packet,
+                   [this, original_direction](QuicStringPiece packet) {
+                     SendResponse(original_direction, packet);
+                   });
+}
+
+void QbonePacketProcessor::SendTcpReset(QuicStringPiece original_packet,
+                                        Direction original_direction) {
+  CreateTcpResetPacket(original_packet,
+                       [this, original_direction](QuicStringPiece packet) {
+                         SendResponse(original_direction, packet);
+                       });
+}
+
+void QbonePacketProcessor::SendResponse(Direction original_direction,
+                                        QuicStringPiece packet) {
+  switch (original_direction) {
+    case Direction::FROM_CLIENT:
+      output_->SendPacketToClient(packet);
+      break;
+    case Direction::FROM_NETWORK:
+      output_->SendPacketToNetwork(packet);
+      break;
+  }
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_packet_processor.h b/quic/qbone/qbone_packet_processor.h
new file mode 100644
index 0000000..4476771
--- /dev/null
+++ b/quic/qbone/qbone_packet_processor.h
@@ -0,0 +1,198 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_PACKET_PROCESSOR_H_
+#define QUICHE_QUIC_QBONE_QBONE_PACKET_PROCESSOR_H_
+
+#include <netinet/icmp6.h>
+#include <netinet/ip6.h>
+
+#include "net/third_party/quiche/src/quic/core/quic_types.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ip_address.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_string_piece.h"
+
+namespace quic {
+
+enum : size_t {
+  kIPv6HeaderSize = 40,
+  kICMPv6HeaderSize = sizeof(icmp6_hdr),
+  kTotalICMPv6HeaderSize = kIPv6HeaderSize + kICMPv6HeaderSize,
+};
+
+// QBONE packet processor accepts packets destined in either direction
+// (client-to-network or network-to-client).  It inspects them and makes
+// decisions on whether they should be forwarded or dropped, replying with ICMP
+// messages as appropriate.
+class QbonePacketProcessor {
+ public:
+  enum class Direction {
+    // Packet is going from the QBONE client into the network behind the QBONE.
+    FROM_CLIENT = 0,
+    // Packet is going from the network begin QBONE to the client.
+    FROM_NETWORK = 1
+  };
+
+  enum class ProcessingResult {
+    OK = 0,
+    SILENT_DROP = 1,
+    ICMP = 2,
+    // Equivalent to |SILENT_DROP| at the moment, but indicates that the
+    // downstream filter has buffered the packet and deferred its processing.
+    // The packet may be emitted at a later time.
+    DEFER = 3,
+    // In addition to sending an ICMP message, also send a TCP RST. This option
+    // requires the incoming packet to have been a valid TCP packet, as a TCP
+    // RST requires information from the current connection state to be
+    // well-formed.
+    ICMP_AND_TCP_RESET = 4,
+  };
+
+  class OutputInterface {
+   public:
+    virtual ~OutputInterface();
+
+    virtual void SendPacketToClient(QuicStringPiece packet) = 0;
+    virtual void SendPacketToNetwork(QuicStringPiece packet) = 0;
+  };
+
+  class StatsInterface {
+   public:
+    virtual ~StatsInterface();
+
+    virtual void OnPacketForwarded(Direction direction) = 0;
+    virtual void OnPacketDroppedSilently(Direction direction) = 0;
+    virtual void OnPacketDroppedWithIcmp(Direction direction) = 0;
+    virtual void OnPacketDroppedWithTcpReset(Direction direction) = 0;
+    virtual void OnPacketDeferred(Direction direction) = 0;
+  };
+
+  // Allows to implement a custom packet filter on top of the filtering done by
+  // the packet processor itself.
+  class Filter {
+   public:
+    virtual ~Filter();
+    // The main interface function.  The following arguments are supplied:
+    // - |direction|, to indicate direction of the packet.
+    // - |full_packet|, which includes the IPv6 header and possibly the IPv6
+    //   options that were understood by the processor.
+    // - |payload|, the contents of the IPv6 packet, i.e. a TCP, a UDP or an
+    //   ICMP packet.
+    // - |icmp_header|, an output argument which allows the filter to specify
+    //   the ICMP message with which the packet is to be rejected.
+    // The method is called only on packets which were already verified as valid
+    // IPv6 packets.
+    //
+    // The implementer of this method has four options to return:
+    // - OK will cause the filter to pass the packet through
+    // - SILENT_DROP will cause the filter to drop the packet silently
+    // - ICMP will cause the filter to drop the packet and send an ICMP
+    //   response.
+    // - DEFER will cause the packet to be not forwarded; the filter is
+    //   responsible for sending (or not sending) it later using |output|.
+    //
+    // Note that |output| should not be used except in the DEFER case, as the
+    // processor will perform the necessary writes itself.
+    virtual ProcessingResult FilterPacket(Direction direction,
+                                          QuicStringPiece full_packet,
+                                          QuicStringPiece payload,
+                                          icmp6_hdr* icmp_header,
+                                          OutputInterface* output);
+
+   protected:
+    // Helper methods that allow to easily extract information that is required
+    // for filtering from the |ipv6_header| argument.  All of those assume that
+    // the header is of valid size, which is true for everything passed into
+    // FilterPacket().
+    inline uint8_t TransportProtocolFromHeader(QuicStringPiece ipv6_header) {
+      return ipv6_header[6];
+    }
+    inline QuicIpAddress SourceIpFromHeader(QuicStringPiece ipv6_header) {
+      QuicIpAddress address;
+      address.FromPackedString(&ipv6_header[8],
+                               QuicIpAddress::kIPv6AddressSize);
+      return address;
+    }
+    inline QuicIpAddress DestinationIpFromHeader(QuicStringPiece ipv6_header) {
+      QuicIpAddress address;
+      address.FromPackedString(&ipv6_header[24],
+                               QuicIpAddress::kIPv6AddressSize);
+      return address;
+    }
+  };
+
+  // |self_ip| is the IP address from which the processor will originate ICMP
+  // messages.  |client_ip| is the expected IP address of the client, used for
+  // packet validation.
+  //
+  // |output| and |stats| are the visitor interfaces used by the processor.
+  // |output| gets notified whenever the processor decides to send a packet, and
+  // |stats| gets notified about any decisions that processor makes, without a
+  // reference to which packet that decision was made about.
+  QbonePacketProcessor(QuicIpAddress self_ip,
+                       QuicIpAddress client_ip,
+                       size_t client_ip_subnet_length,
+                       OutputInterface* output,
+                       StatsInterface* stats);
+  QbonePacketProcessor(const QbonePacketProcessor&) = delete;
+  QbonePacketProcessor& operator=(const QbonePacketProcessor&) = delete;
+
+  // Accepts an IPv6 packet and handles it accordingly by either forwarding it,
+  // replying with an ICMP packet or silently dropping it.  |packet| will be
+  // modified in the process, by having the TTL field decreased.
+  void ProcessPacket(string* packet, Direction direction);
+
+  void set_filter(std::unique_ptr<Filter> filter) {
+    filter_ = std::move(filter);
+  }
+
+  void set_client_ip(QuicIpAddress client_ip) { client_ip_ = client_ip; }
+  void set_client_ip_subnet_length(size_t client_ip_subnet_length) {
+    client_ip_subnet_length_ = client_ip_subnet_length;
+  }
+
+  static const QuicIpAddress kInvalidIpAddress;
+
+ protected:
+  // Processes the header and returns what should be done with the packet.
+  // After that, calls an external packet filter if registered.  TTL of the
+  // packet may be decreased in the process.
+  ProcessingResult ProcessIPv6HeaderAndFilter(string* packet,
+                                              Direction direction,
+                                              uint8_t* transport_protocol,
+                                              char** transport_data,
+                                              icmp6_hdr* icmp_header);
+
+  void SendIcmpResponse(icmp6_hdr* icmp_header,
+                        QuicStringPiece original_packet,
+                        Direction original_direction);
+
+  void SendTcpReset(QuicStringPiece original_packet,
+                    Direction original_direction);
+
+  inline bool IsValid() const { return client_ip_ != kInvalidIpAddress; }
+
+  // IP address of the server.  Used to send ICMP messages.
+  in6_addr self_ip_;
+  // IP address range of the VPN client.
+  QuicIpAddress client_ip_;
+  size_t client_ip_subnet_length_;
+
+  OutputInterface* output_;
+  StatsInterface* stats_;
+  std::unique_ptr<Filter> filter_;
+
+ private:
+  // Performs basic sanity and permission checks on the packet, and decreases
+  // the TTL.
+  ProcessingResult ProcessIPv6Header(string* packet,
+                                     Direction direction,
+                                     uint8_t* transport_protocol,
+                                     char** transport_data,
+                                     icmp6_hdr* icmp_header);
+
+  void SendResponse(Direction original_direction, QuicStringPiece packet);
+};
+
+}  // namespace quic
+#endif  // QUICHE_QUIC_QBONE_QBONE_PACKET_PROCESSOR_H_
diff --git a/quic/qbone/qbone_packet_processor_test.cc b/quic/qbone/qbone_packet_processor_test.cc
new file mode 100644
index 0000000..256f5b0
--- /dev/null
+++ b/quic/qbone/qbone_packet_processor_test.cc
@@ -0,0 +1,283 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_packet_processor.h"
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_processor_test_tools.h"
+
+namespace quic {
+namespace {
+
+using Direction = QbonePacketProcessor::Direction;
+using ProcessingResult = QbonePacketProcessor::ProcessingResult;
+using OutputInterface = QbonePacketProcessor::OutputInterface;
+using ::testing::_;
+using ::testing::Return;
+
+// clang-format off
+static const char kReferenceClientPacketData[] = {
+    // IPv6 with zero TOS and flow label.
+    0x60, 0x00, 0x00, 0x00,
+    // Payload size is 8 bytes.
+    0x00, 0x08,
+    // Next header is UDP
+    17,
+    // TTL is 50.
+    50,
+    // IP address of the sender is fd00:0:0:1::1
+    0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // IP address of the receiver is fd00:0:0:5::1
+    0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // Source port 12345
+    0x30, 0x39,
+    // Destination port 443
+    0x01, 0xbb,
+    // UDP content length is zero
+    0x00, 0x00,
+    // Checksum is not actually checked in any of the tests, so we leave it as
+    // zero
+    0x00, 0x00,
+};
+
+static const char kReferenceNetworkPacketData[] = {
+    // IPv6 with zero TOS and flow label.
+    0x60, 0x00, 0x00, 0x00,
+    // Payload size is 8 bytes.
+    0x00, 0x08,
+    // Next header is UDP
+    17,
+    // TTL is 50.
+    50,
+    // IP address of the sender is fd00:0:0:5::1
+    0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // IP address of the receiver is fd00:0:0:1::1
+    0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // Source port 443
+    0x01, 0xbb,
+    // Destination port 12345
+    0x30, 0x39,
+    // UDP content length is zero
+    0x00, 0x00,
+    // Checksum is not actually checked in any of the tests, so we leave it as
+    // zero
+    0x00, 0x00,
+};
+
+static const char kReferenceClientSubnetPacketData[] = {
+    // IPv6 with zero TOS and flow label.
+    0x60, 0x00, 0x00, 0x00,
+    // Payload size is 8 bytes.
+    0x00, 0x08,
+    // Next header is UDP
+    17,
+    // TTL is 50.
+    50,
+    // IP address of the sender is fd00:0:0:2::1, which is within the /62 of the
+    // client.
+    0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // IP address of the receiver is fd00:0:0:5::1
+    0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05,
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+    // Source port 12345
+    0x30, 0x39,
+    // Destination port 443
+    0x01, 0xbb,
+    // UDP content length is zero
+    0x00, 0x00,
+    // Checksum is not actually checked in any of the tests, so we leave it as
+    // zero
+    0x00, 0x00,
+};
+
+// clang-format on
+
+static const QuicStringPiece kReferenceClientPacket(
+    kReferenceClientPacketData,
+    arraysize(kReferenceClientPacketData));
+
+static const QuicStringPiece kReferenceNetworkPacket(
+    kReferenceNetworkPacketData,
+    arraysize(kReferenceNetworkPacketData));
+
+static const QuicStringPiece kReferenceClientSubnetPacket(
+    kReferenceClientSubnetPacketData,
+    arraysize(kReferenceClientSubnetPacketData));
+
+MATCHER_P(IsIcmpMessage,
+          icmp_type,
+          "Checks whether the argument is an ICMP message of supplied type") {
+  if (arg.size() < kTotalICMPv6HeaderSize) {
+    return false;
+  }
+
+  return arg[40] == icmp_type;
+}
+
+class MockPacketFilter : public QbonePacketProcessor::Filter {
+ public:
+  MOCK_METHOD5(FilterPacket,
+               ProcessingResult(Direction,
+                                QuicStringPiece,
+                                QuicStringPiece,
+                                icmp6_hdr*,
+                                OutputInterface*));
+};
+
+class QbonePacketProcessorTest : public QuicTest {
+ protected:
+  QbonePacketProcessorTest() {
+    CHECK(client_ip_.FromString("fd00:0:0:1::1"));
+    CHECK(self_ip_.FromString("fd00:0:0:4::1"));
+    CHECK(network_ip_.FromString("fd00:0:0:5::1"));
+
+    processor_ = QuicMakeUnique<QbonePacketProcessor>(
+        self_ip_, client_ip_, /*client_ip_subnet_length=*/62, &output_,
+        &stats_);
+  }
+
+  void SendPacketFromClient(QuicStringPiece packet) {
+    string packet_buffer(packet.data(), packet.size());
+    processor_->ProcessPacket(&packet_buffer, Direction::FROM_CLIENT);
+  }
+
+  void SendPacketFromNetwork(QuicStringPiece packet) {
+    string packet_buffer(packet.data(), packet.size());
+    processor_->ProcessPacket(&packet_buffer, Direction::FROM_NETWORK);
+  }
+
+  QuicIpAddress client_ip_;
+  QuicIpAddress self_ip_;
+  QuicIpAddress network_ip_;
+
+  std::unique_ptr<QbonePacketProcessor> processor_;
+  testing::StrictMock<MockPacketProcessorOutput> output_;
+  testing::StrictMock<MockPacketProcessorStats> stats_;
+};
+
+TEST_F(QbonePacketProcessorTest, EmptyPacket) {
+  EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_CLIENT));
+  SendPacketFromClient("");
+
+  EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_NETWORK));
+  SendPacketFromNetwork("");
+}
+
+TEST_F(QbonePacketProcessorTest, RandomGarbage) {
+  EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_CLIENT));
+  SendPacketFromClient(string(1280, 'a'));
+
+  EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_NETWORK));
+  SendPacketFromNetwork(string(1280, 'a'));
+}
+
+TEST_F(QbonePacketProcessorTest, RandomGarbageWithCorrectLengthFields) {
+  string packet(40, 'a');
+  packet[4] = 0;
+  packet[5] = 0;
+
+  EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_CLIENT));
+  EXPECT_CALL(output_, SendPacketToClient(IsIcmpMessage(ICMP6_DST_UNREACH)));
+  SendPacketFromClient(packet);
+}
+
+TEST_F(QbonePacketProcessorTest, GoodPacketFromClient) {
+  EXPECT_CALL(stats_, OnPacketForwarded(Direction::FROM_CLIENT));
+  EXPECT_CALL(output_, SendPacketToNetwork(_));
+  SendPacketFromClient(kReferenceClientPacket);
+}
+
+TEST_F(QbonePacketProcessorTest, GoodPacketFromClientSubnet) {
+  EXPECT_CALL(stats_, OnPacketForwarded(Direction::FROM_CLIENT));
+  EXPECT_CALL(output_, SendPacketToNetwork(_));
+  SendPacketFromClient(kReferenceClientSubnetPacket);
+}
+
+TEST_F(QbonePacketProcessorTest, GoodPacketFromNetwork) {
+  EXPECT_CALL(stats_, OnPacketForwarded(Direction::FROM_NETWORK));
+  EXPECT_CALL(output_, SendPacketToClient(_));
+  SendPacketFromNetwork(kReferenceNetworkPacket);
+}
+
+TEST_F(QbonePacketProcessorTest, GoodPacketFromNetworkWrongDirection) {
+  EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_CLIENT));
+  EXPECT_CALL(output_, SendPacketToClient(IsIcmpMessage(ICMP6_DST_UNREACH)));
+  SendPacketFromClient(kReferenceNetworkPacket);
+}
+
+TEST_F(QbonePacketProcessorTest, TtlExpired) {
+  string packet(kReferenceNetworkPacket);
+  packet[7] = 1;
+
+  EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_NETWORK));
+  EXPECT_CALL(output_, SendPacketToNetwork(IsIcmpMessage(ICMP6_TIME_EXCEEDED)));
+  SendPacketFromNetwork(packet);
+}
+
+TEST_F(QbonePacketProcessorTest, UnknownProtocol) {
+  string packet(kReferenceNetworkPacket);
+  packet[6] = IPPROTO_SCTP;
+
+  EXPECT_CALL(stats_, OnPacketDroppedWithIcmp(Direction::FROM_NETWORK));
+  EXPECT_CALL(output_, SendPacketToNetwork(IsIcmpMessage(ICMP6_PARAM_PROB)));
+  SendPacketFromNetwork(packet);
+}
+
+TEST_F(QbonePacketProcessorTest, FilterFromClient) {
+  auto filter = QuicMakeUnique<MockPacketFilter>();
+  EXPECT_CALL(*filter, FilterPacket(_, _, _, _, _))
+      .WillRepeatedly(Return(ProcessingResult::SILENT_DROP));
+  processor_->set_filter(std::move(filter));
+
+  EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_CLIENT));
+  SendPacketFromClient(kReferenceClientPacket);
+}
+
+class TestFilter : public QbonePacketProcessor::Filter {
+ public:
+  TestFilter(QuicIpAddress client_ip, QuicIpAddress network_ip)
+      : client_ip_(client_ip), network_ip_(network_ip) {}
+  ProcessingResult FilterPacket(Direction direction,
+                                QuicStringPiece full_packet,
+                                QuicStringPiece payload,
+                                icmp6_hdr* icmp_header,
+                                OutputInterface* output) override {
+    EXPECT_EQ(kIPv6HeaderSize, full_packet.size() - payload.size());
+    EXPECT_EQ(IPPROTO_UDP, TransportProtocolFromHeader(full_packet));
+    EXPECT_EQ(client_ip_, SourceIpFromHeader(full_packet));
+    EXPECT_EQ(network_ip_, DestinationIpFromHeader(full_packet));
+
+    called_++;
+    return ProcessingResult::SILENT_DROP;
+  }
+
+  int called() const { return called_; }
+
+ private:
+  int called_ = 0;
+
+  QuicIpAddress client_ip_;
+  QuicIpAddress network_ip_;
+};
+
+// Verify that the parameters are passed correctly into the filter, and that the
+// helper functions of the filter class work.
+TEST_F(QbonePacketProcessorTest, FilterHelperFunctions) {
+  auto filter_owned = QuicMakeUnique<TestFilter>(client_ip_, network_ip_);
+  TestFilter* filter = filter_owned.get();
+  processor_->set_filter(std::move(filter_owned));
+
+  EXPECT_CALL(stats_, OnPacketDroppedSilently(Direction::FROM_CLIENT));
+  SendPacketFromClient(kReferenceClientPacket);
+  ASSERT_EQ(1, filter->called());
+}
+
+}  // namespace
+}  // namespace quic
diff --git a/quic/qbone/qbone_packet_processor_test_tools.cc b/quic/qbone/qbone_packet_processor_test_tools.cc
new file mode 100644
index 0000000..9f9b623
--- /dev/null
+++ b/quic/qbone/qbone_packet_processor_test_tools.cc
@@ -0,0 +1,28 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_packet_processor_test_tools.h"
+
+#include <netinet/ip6.h>
+
+namespace quic {
+
+string PrependIPv6HeaderForTest(const string& body, int hops) {
+  ip6_hdr header;
+  memset(&header, 0, sizeof(header));
+
+  header.ip6_vfc = 6 << 4;
+  header.ip6_plen = htons(body.size());
+  header.ip6_nxt = IPPROTO_UDP;
+  header.ip6_hops = hops;
+  header.ip6_src = in6addr_loopback;
+  header.ip6_dst = in6addr_loopback;
+
+  string packet(sizeof(header) + body.size(), '\0');
+  memcpy(&packet[0], &header, sizeof(header));
+  memcpy(&packet[sizeof(header)], body.data(), body.size());
+  return packet;
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_packet_processor_test_tools.h b/quic/qbone/qbone_packet_processor_test_tools.h
new file mode 100644
index 0000000..646dc42
--- /dev/null
+++ b/quic/qbone/qbone_packet_processor_test_tools.h
@@ -0,0 +1,37 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_PACKET_PROCESSOR_TEST_TOOLS_H_
+#define QUICHE_QUIC_QBONE_QBONE_PACKET_PROCESSOR_TEST_TOOLS_H_
+
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_processor.h"
+
+namespace quic {
+
+class MockPacketProcessorOutput : public QbonePacketProcessor::OutputInterface {
+ public:
+  MockPacketProcessorOutput() {}
+
+  MOCK_METHOD1(SendPacketToClient, void(QuicStringPiece));
+  MOCK_METHOD1(SendPacketToNetwork, void(QuicStringPiece));
+};
+
+class MockPacketProcessorStats : public QbonePacketProcessor::StatsInterface {
+ public:
+  MockPacketProcessorStats() {}
+
+  MOCK_METHOD1(OnPacketForwarded, void(QbonePacketProcessor::Direction));
+  MOCK_METHOD1(OnPacketDroppedSilently, void(QbonePacketProcessor::Direction));
+  MOCK_METHOD1(OnPacketDroppedWithIcmp, void(QbonePacketProcessor::Direction));
+  MOCK_METHOD1(OnPacketDroppedWithTcpReset,
+               void(QbonePacketProcessor::Direction));
+  MOCK_METHOD1(OnPacketDeferred, void(QbonePacketProcessor::Direction));
+};
+
+string PrependIPv6HeaderForTest(const string& body, int hops);
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_PACKET_PROCESSOR_TEST_TOOLS_H_
diff --git a/quic/qbone/qbone_packet_writer.h b/quic/qbone/qbone_packet_writer.h
new file mode 100644
index 0000000..1ed8a46
--- /dev/null
+++ b/quic/qbone/qbone_packet_writer.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_PACKET_WRITER_H_
+#define QUICHE_QUIC_QBONE_QBONE_PACKET_WRITER_H_
+
+#include <cstring>
+
+namespace quic {
+
+// QbonePacketWriter expects only one function to be defined,
+// WritePacketToNetwork, which is called when a packet is received via QUIC
+// and should be sent out on the network.  This is the complete packet,
+// and not just a fragment.
+class QbonePacketWriter {
+ public:
+  virtual ~QbonePacketWriter() {}
+  virtual void WritePacketToNetwork(const char* packet, size_t size) = 0;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_PACKET_WRITER_H_
diff --git a/quic/qbone/qbone_server_session.cc b/quic/qbone/qbone_server_session.cc
new file mode 100644
index 0000000..9b2ebdd
--- /dev/null
+++ b/quic/qbone/qbone_server_session.cc
@@ -0,0 +1,95 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_server_session.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_connection_id.h"
+#include "net/third_party/quiche/src/quic/core/quic_types.h"
+#include "net/third_party/quiche/src/quic/core/quic_utils.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+
+namespace quic {
+
+bool QboneCryptoServerStreamHelper::CanAcceptClientHello(
+    const CryptoHandshakeMessage& chlo,
+    const QuicSocketAddress& client_address,
+    const QuicSocketAddress& peer_address,
+    const QuicSocketAddress& self_address,
+    string* error_details) const {
+  absl::string_view alpn;
+  chlo.GetStringPiece(quic::kALPN, &alpn);
+  if (alpn != QboneConstants::kQboneAlpn) {
+    *error_details = "ALPN-indicated protocol is not qbone";
+    return false;
+  }
+  return true;
+}
+
+QboneServerSession::QboneServerSession(
+    const quic::ParsedQuicVersionVector& supported_versions,
+    QuicConnection* connection,
+    Visitor* owner,
+    const QuicConfig& config,
+    const QuicCryptoServerConfig* quic_crypto_server_config,
+    QuicCompressedCertsCache* compressed_certs_cache,
+    QbonePacketWriter* writer,
+    QuicIpAddress self_ip,
+    QuicIpAddress client_ip,
+    size_t client_ip_subnet_length,
+    QboneServerControlStream::Handler* handler)
+    : QboneSessionBase(connection, owner, config, supported_versions, writer),
+      processor_(self_ip, client_ip, client_ip_subnet_length, this, this),
+      quic_crypto_server_config_(quic_crypto_server_config),
+      compressed_certs_cache_(compressed_certs_cache),
+      handler_(handler) {}
+
+QboneServerSession::~QboneServerSession() {}
+
+std::unique_ptr<QuicCryptoStream> QboneServerSession::CreateCryptoStream() {
+  return QuicMakeUnique<QuicCryptoServerStream>(quic_crypto_server_config_,
+                                                compressed_certs_cache_, this,
+                                                &stream_helper_);
+}
+
+void QboneServerSession::Initialize() {
+  QboneSessionBase::Initialize();
+  // Register the reserved control stream.
+  auto control_stream =
+      QuicMakeUnique<QboneServerControlStream>(this, handler_);
+  control_stream_ = control_stream.get();
+  RegisterStaticStream(std::move(control_stream),
+                       /*stream_already_counted = */ false);
+}
+
+bool QboneServerSession::SendClientRequest(const QboneClientRequest& request) {
+  if (!control_stream_) {
+    QUIC_BUG << "Cannot send client request before control stream is created.";
+    return false;
+  }
+  return control_stream_->SendRequest(request);
+}
+
+void QboneServerSession::ProcessPacketFromNetwork(QuicStringPiece packet) {
+  string buffer = string(packet);
+  processor_.ProcessPacket(&buffer,
+                           QbonePacketProcessor::Direction::FROM_NETWORK);
+}
+
+void QboneServerSession::ProcessPacketFromPeer(QuicStringPiece packet) {
+  string buffer = string(packet);
+  processor_.ProcessPacket(&buffer,
+                           QbonePacketProcessor::Direction::FROM_CLIENT);
+}
+
+void QboneServerSession::SendPacketToClient(QuicStringPiece packet) {
+  SendPacketToPeer(packet);
+}
+
+void QboneServerSession::SendPacketToNetwork(QuicStringPiece packet) {
+  DCHECK(writer_ != nullptr);
+  writer_->WritePacketToNetwork(packet.data(), packet.size());
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_server_session.h b/quic/qbone/qbone_server_session.h
new file mode 100644
index 0000000..9536f87
--- /dev/null
+++ b/quic/qbone/qbone_server_session.h
@@ -0,0 +1,92 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_SERVER_SESSION_H_
+#define QUICHE_QUIC_QBONE_QBONE_SERVER_SESSION_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_crypto_server_stream.h"
+#include "net/third_party/quiche/src/quic/core/quic_crypto_stream.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_export.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_control.pb.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_control_stream.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_processor.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_writer.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_session_base.h"
+
+namespace quic {
+
+// A helper class is used by the QuicCryptoServerStream.
+class QboneCryptoServerStreamHelper : public QuicCryptoServerStream::Helper {
+ public:
+  // This will look for the qbone alpn.
+  bool CanAcceptClientHello(const CryptoHandshakeMessage& chlo,
+                            const QuicSocketAddress& client_address,
+                            const QuicSocketAddress& peer_address,
+                            const QuicSocketAddress& self_address,
+                            string* error_details) const override;
+};
+
+class QUIC_EXPORT_PRIVATE QboneServerSession
+    : public QboneSessionBase,
+      public QbonePacketProcessor::OutputInterface,
+      public QbonePacketProcessor::StatsInterface {
+ public:
+  QboneServerSession(const quic::ParsedQuicVersionVector& supported_versions,
+                     QuicConnection* connection,
+                     Visitor* owner,
+                     const QuicConfig& config,
+                     const QuicCryptoServerConfig* quic_crypto_server_config,
+                     QuicCompressedCertsCache* compressed_certs_cache,
+                     QbonePacketWriter* writer,
+                     QuicIpAddress self_ip,
+                     QuicIpAddress client_ip,
+                     size_t client_ip_subnet_length,
+                     QboneServerControlStream::Handler* handler);
+  QboneServerSession(const QboneServerSession&) = delete;
+  QboneServerSession& operator=(const QboneServerSession&) = delete;
+  ~QboneServerSession() override;
+
+  void Initialize() override;
+
+  virtual bool SendClientRequest(const QboneClientRequest& request);
+
+  void ProcessPacketFromNetwork(QuicStringPiece packet) override;
+  void ProcessPacketFromPeer(QuicStringPiece packet) override;
+
+  // QbonePacketProcessor::OutputInterface implementation.
+  void SendPacketToClient(QuicStringPiece packet) override;
+  void SendPacketToNetwork(QuicStringPiece packet) override;
+
+  // QbonePacketProcessor::StatsInterface implementation.
+  void OnPacketForwarded(QbonePacketProcessor::Direction direction) override {}
+  void OnPacketDroppedSilently(
+      QbonePacketProcessor::Direction direction) override {}
+  void OnPacketDroppedWithIcmp(
+      QbonePacketProcessor::Direction direction) override {}
+  void OnPacketDroppedWithTcpReset(
+      QbonePacketProcessor::Direction direction) override {}
+  void OnPacketDeferred(QbonePacketProcessor::Direction direction) override {}
+
+ protected:
+  // QboneSessionBase interface implementation.
+  std::unique_ptr<QuicCryptoStream> CreateCryptoStream() override;
+  // The packet processor.
+  QbonePacketProcessor processor_;
+
+ private:
+  // Config for QUIC crypto server stream, used by the server.
+  const QuicCryptoServerConfig* quic_crypto_server_config_;
+  // Used by QUIC crypto server stream to track most recently compressed certs.
+  QuicCompressedCertsCache* compressed_certs_cache_;
+  // This helper is needed when create QuicCryptoServerStream.
+  QboneCryptoServerStreamHelper stream_helper_;
+  // Passed to the control stream.
+  QboneServerControlStream::Handler* handler_;
+  // The unowned control stream.
+  QboneServerControlStream* control_stream_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_SERVER_SESSION_H_
diff --git a/quic/qbone/qbone_session_base.cc b/quic/qbone/qbone_session_base.cc
new file mode 100644
index 0000000..da2f4ec
--- /dev/null
+++ b/quic/qbone/qbone_session_base.cc
@@ -0,0 +1,148 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_session_base.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_data_reader.h"
+#include "net/third_party/quiche/src/quic/core/quic_types.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_exported_stats.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+
+namespace quic {
+
+#define ENDPOINT \
+  (perspective() == Perspective::IS_SERVER ? "Server: " : "Client: ")
+
+QboneSessionBase::QboneSessionBase(
+    QuicConnection* connection,
+    Visitor* owner,
+    const QuicConfig& config,
+    const ParsedQuicVersionVector& supported_versions,
+    QbonePacketWriter* writer)
+    : QuicSession(connection, owner, config, supported_versions) {
+  set_writer(writer);
+  const uint32_t max_streams =
+      (std::numeric_limits<uint32_t>::max() / kMaxAvailableStreamsMultiplier) -
+      1;
+  this->config()->SetMaxIncomingBidirectionalStreamsToSend(max_streams);
+  if (VersionHasIetfQuicFrames(transport_version())) {
+    this->config()->SetMaxIncomingUnidirectionalStreamsToSend(max_streams);
+  }
+  write_blocked_streams()->SwitchWriteScheduler(
+      spdy::WriteSchedulerType::LIFO, connection->transport_version());
+}
+
+QboneSessionBase::~QboneSessionBase() {
+  // Clear out the streams before leaving this destructor to avoid calling
+  // QuicSession::UnregisterStreamPriority
+  stream_map().clear();
+  closed_streams()->clear();
+}
+
+void QboneSessionBase::Initialize() {
+  crypto_stream_ = CreateCryptoStream();
+  QuicSession::Initialize();
+}
+
+const QuicCryptoStream* QboneSessionBase::GetCryptoStream() const {
+  return crypto_stream_.get();
+}
+
+QuicCryptoStream* QboneSessionBase::GetMutableCryptoStream() {
+  return crypto_stream_.get();
+}
+
+QuicStream* QboneSessionBase::CreateOutgoingStream() {
+  return ActivateDataStream(
+      CreateDataStream(GetNextOutgoingUnidirectionalStreamId()));
+}
+
+void QboneSessionBase::CloseStream(QuicStreamId stream_id) {
+  if (IsClosedStream(stream_id)) {
+    // When CloseStream has been called recursively (via
+    // QuicStream::OnClose), the stream is already closed so return.
+    return;
+  }
+  QuicSession::CloseStream(stream_id);
+}
+
+void QboneSessionBase::OnStreamFrame(const QuicStreamFrame& frame) {
+  if (frame.offset == 0 && frame.fin && frame.data_length > 0) {
+    ++num_ephemeral_packets_;
+    ProcessPacketFromPeer(
+        QuicStringPiece(frame.data_buffer, frame.data_length));
+    flow_controller()->AddBytesConsumed(frame.data_length);
+    return;
+  }
+  QuicSession::OnStreamFrame(frame);
+}
+
+QuicStream* QboneSessionBase::CreateIncomingStream(QuicStreamId id) {
+  return ActivateDataStream(CreateDataStream(id));
+}
+
+QuicStream* QboneSessionBase::CreateIncomingStream(PendingStream* /*pending*/) {
+  QUIC_NOTREACHED();
+  return nullptr;
+}
+
+bool QboneSessionBase::ShouldKeepConnectionAlive() const {
+  // Qbone connections stay alive until they're explicitly closed.
+  return true;
+}
+
+std::unique_ptr<QuicStream> QboneSessionBase::CreateDataStream(
+    QuicStreamId id) {
+  if (crypto_stream_ == nullptr || !crypto_stream_->encryption_established()) {
+    // Encryption not active so no stream created
+    return nullptr;
+  }
+
+  if (IsIncomingStream(id)) {
+    ++num_streamed_packets_;
+    return QuicMakeUnique<QboneReadOnlyStream>(id, this);
+  }
+
+  return QuicMakeUnique<QboneWriteOnlyStream>(id, this);
+}
+
+QuicStream* QboneSessionBase::ActivateDataStream(
+    std::unique_ptr<QuicStream> stream) {
+  // Transfer ownership of the data stream to the session via ActivateStream().
+  QuicStream* raw = stream.get();
+  if (stream) {
+    // Make QuicSession take ownership of the stream.
+    ActivateStream(std::move(stream));
+  }
+  return raw;
+}
+
+void QboneSessionBase::SendPacketToPeer(QuicStringPiece packet) {
+  // Qbone streams are ephemeral.
+  QuicStream* stream = CreateOutgoingStream();
+  if (!stream) {
+    QUIC_BUG << "Failed to create an outgoing QBONE stream.";
+    return;
+  }
+
+  QboneWriteOnlyStream* qbone_stream =
+      static_cast<QboneWriteOnlyStream*>(stream);
+  qbone_stream->WritePacketToQuicStream(packet);
+}
+
+uint64_t QboneSessionBase::GetNumEphemeralPackets() const {
+  return num_ephemeral_packets_;
+}
+
+uint64_t QboneSessionBase::GetNumStreamedPackets() const {
+  return num_streamed_packets_;
+}
+
+void QboneSessionBase::set_writer(QbonePacketWriter* writer) {
+  writer_ = writer;
+  testing::testvalue::Adjust("quic_QbonePacketWriter", &writer_);
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_session_base.h b/quic/qbone/qbone_session_base.h
new file mode 100644
index 0000000..5bfc9be
--- /dev/null
+++ b/quic/qbone/qbone_session_base.h
@@ -0,0 +1,93 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_SESSION_BASE_H_
+#define QUICHE_QUIC_QBONE_QBONE_SESSION_BASE_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_crypto_server_stream.h"
+#include "net/third_party/quiche/src/quic/core/quic_crypto_stream.h"
+#include "net/third_party/quiche/src/quic/core/quic_error_codes.h"
+#include "net/third_party/quiche/src/quic/core/quic_session.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_containers.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_export.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_writer.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_stream.h"
+
+namespace quic {
+
+class QUIC_EXPORT_PRIVATE QboneSessionBase : public QuicSession {
+ public:
+  QboneSessionBase(QuicConnection* connection,
+                   Visitor* owner,
+                   const QuicConfig& config,
+                   const ParsedQuicVersionVector& supported_versions,
+                   QbonePacketWriter* writer);
+  QboneSessionBase(const QboneSessionBase&) = delete;
+  QboneSessionBase& operator=(const QboneSessionBase&) = delete;
+  ~QboneSessionBase() override;
+
+  // Overrides from QuicSession.
+  // This will ensure that the crypto session is created.
+  void Initialize() override;
+  // This will ensure that we keep track of stream ids that can be
+  // write blocked.
+  void CloseStream(QuicStreamId stream_id) override;
+  // This will check if the packet is wholly contained.
+  void OnStreamFrame(const QuicStreamFrame& frame) override;
+
+  virtual void ProcessPacketFromNetwork(QuicStringPiece packet) = 0;
+  virtual void ProcessPacketFromPeer(QuicStringPiece packet) = 0;
+
+  // Returns the number of qbone network packets that were received
+  // that fit into a single QuicStreamFrame and elided the creation of
+  // a QboneReadOnlyStream.
+  uint64_t GetNumEphemeralPackets() const;
+
+  // Returns the number of qbone network packets that were via
+  // multiple packets, requiring the creation of a QboneReadOnlyStream.
+  uint64_t GetNumStreamedPackets() const;
+
+  void set_writer(QbonePacketWriter* writer);
+
+ protected:
+  virtual std::unique_ptr<QuicCryptoStream> CreateCryptoStream() = 0;
+
+  // QuicSession interface implementation.
+  QuicCryptoStream* GetMutableCryptoStream() override;
+  const QuicCryptoStream* GetCryptoStream() const override;
+  QuicStream* CreateIncomingStream(QuicStreamId id) override;
+  QuicStream* CreateIncomingStream(PendingStream* pending) override;
+  bool ShouldKeepConnectionAlive() const override;
+
+  bool MaybeIncreaseLargestPeerStreamId(const QuicStreamId stream_id) override {
+    return true;
+  }
+
+  QuicStream* CreateOutgoingStream();
+  std::unique_ptr<QuicStream> CreateDataStream(QuicStreamId id);
+  // Activates a QuicStream.  The session takes ownership of the stream, but
+  // returns an unowned pointer to the stream for convenience.
+  QuicStream* ActivateDataStream(std::unique_ptr<QuicStream> stream);
+
+  // Accepts a given packet from the network and writes it out
+  // to the QUIC stream. This will create an ephemeral stream per
+  // packet. This function will return true if a stream was created
+  // and the packet sent. It will return false if the stream could not
+  // be created.
+  void SendPacketToPeer(QuicStringPiece packet);
+
+  QbonePacketWriter* writer_;
+
+ private:
+  // Used for the crypto handshake.
+  std::unique_ptr<QuicCryptoStream> crypto_stream_;
+
+  uint64_t num_ephemeral_packets_ = 0;
+  uint64_t num_streamed_packets_ = 0;
+  QuicUnorderedSet<QuicStreamId> reliable_streams_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_SESSION_BASE_H_
diff --git a/quic/qbone/qbone_session_test.cc b/quic/qbone/qbone_session_test.cc
new file mode 100644
index 0000000..80bf08f
--- /dev/null
+++ b/quic/qbone/qbone_session_test.cc
@@ -0,0 +1,519 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/core/proto/crypto_server_config_proto.h"
+#include "net/third_party/quiche/src/quic/core/quic_alarm_factory.h"
+#include "net/third_party/quiche/src/quic/core/quic_epoll_alarm_factory.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_expect_bug.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_port_utils.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test_loopback.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_client_session.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_control_placeholder.pb.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_packet_processor_test_tools.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_server_session.h"
+#include "net/third_party/quiche/src/quic/test_tools/mock_clock.h"
+#include "net/third_party/quiche/src/quic/test_tools/quic_connection_peer.h"
+#include "net/third_party/quiche/src/quic/test_tools/quic_session_peer.h"
+#include "net/third_party/quiche/src/quic/test_tools/quic_test_utils.h"
+
+namespace quic {
+namespace test {
+namespace {
+
+using ::testing::_;
+using ::testing::Contains;
+using ::testing::ElementsAre;
+using ::testing::Eq;
+using ::testing::Invoke;
+using ::testing::NiceMock;
+using ::testing::Not;
+
+string TestPacketIn(const string& body) {
+  return PrependIPv6HeaderForTest(body, 5);
+}
+
+string TestPacketOut(const string& body) {
+  return PrependIPv6HeaderForTest(body, 4);
+}
+
+// Used by QuicCryptoServerConfig to provide server credentials, returning a
+// canned response equal to |success|.
+class FakeProofSource : public ProofSource {
+ public:
+  explicit FakeProofSource(bool success) : success_(success) {}
+
+  // ProofSource override.
+  void GetProof(const QuicSocketAddress& server_address,
+                const string& hostname,
+                const string& server_config,
+                QuicTransportVersion transport_version,
+                QuicStringPiece chlo_hash,
+                std::unique_ptr<Callback> callback) override {
+    QuicReferenceCountedPointer<ProofSource::Chain> chain =
+        GetCertChain(server_address, hostname);
+    QuicCryptoProof proof;
+    if (success_) {
+      proof.signature = "Signature";
+      proof.leaf_cert_scts = "Time";
+    }
+    callback->Run(success_, chain, proof, nullptr /* details */);
+  }
+
+  QuicReferenceCountedPointer<Chain> GetCertChain(
+      const QuicSocketAddress& server_address,
+      const string& hostname) override {
+    if (!success_) {
+      return QuicReferenceCountedPointer<Chain>();
+    }
+    std::vector<string> certs;
+    certs.push_back("Required to establish handshake");
+    return QuicReferenceCountedPointer<ProofSource::Chain>(
+        new ProofSource::Chain(certs));
+  }
+
+  void ComputeTlsSignature(
+      const QuicSocketAddress& server_address,
+      const string& hostname,
+      uint16_t signature_algorithm,
+      QuicStringPiece in,
+      std::unique_ptr<SignatureCallback> callback) override {
+    callback->Run(true, "Signature");
+  }
+
+ private:
+  // Whether or not obtaining proof source succeeds.
+  bool success_;
+};
+
+// Used by QuicCryptoClientConfig to verify server credentials, returning a
+// canned response of QUIC_SUCCESS if |success| is true.
+class FakeProofVerifier : public ProofVerifier {
+ public:
+  explicit FakeProofVerifier(bool success) : success_(success) {}
+
+  // ProofVerifier override
+  QuicAsyncStatus VerifyProof(
+      const string& hostname,
+      const uint16_t port,
+      const string& server_config,
+      QuicTransportVersion transport_version,
+      QuicStringPiece chlo_hash,
+      const std::vector<string>& certs,
+      const string& cert_sct,
+      const string& signature,
+      const ProofVerifyContext* context,
+      string* error_details,
+      std::unique_ptr<ProofVerifyDetails>* verify_details,
+      std::unique_ptr<ProofVerifierCallback> callback) override {
+    return success_ ? QUIC_SUCCESS : QUIC_FAILURE;
+  }
+
+  QuicAsyncStatus VerifyCertChain(
+      const string& hostname,
+      const std::vector<string>& certs,
+      const std::string& ocsp_response,
+      const std::string& cert_sct,
+      const ProofVerifyContext* context,
+      string* error_details,
+      std::unique_ptr<ProofVerifyDetails>* details,
+      std::unique_ptr<ProofVerifierCallback> callback) override {
+    return success_ ? QUIC_SUCCESS : QUIC_FAILURE;
+  }
+
+  std::unique_ptr<ProofVerifyContext> CreateDefaultContext() override {
+    return nullptr;
+  }
+
+ private:
+  // Whether or not proof verification succeeds.
+  bool success_;
+};
+
+class DataSavingQbonePacketWriter : public QbonePacketWriter {
+ public:
+  void WritePacketToNetwork(const char* packet, size_t size) override {
+    data_.push_back(string(packet, size));
+  }
+
+  const std::vector<string>& data() { return data_; }
+
+ private:
+  std::vector<string> data_;
+};
+
+template <class T>
+class DataSavingQboneControlHandler : public QboneControlHandler<T> {
+ public:
+  void OnControlRequest(const T& request) override { data_.push_back(request); }
+
+  void OnControlError() override { error_ = true; }
+
+  const std::vector<T>& data() { return data_; }
+  bool error() { return error_; }
+
+ private:
+  std::vector<T> data_;
+  bool error_ = false;
+};
+
+// Single-threaded scheduled task runner based on a MockClock.
+//
+// Simulates asynchronous execution on a single thread by holding scheduled
+// tasks until Run() is called. Performs no synchronization, assumes that
+// Schedule() and Run() are called on the same thread.
+class FakeTaskRunner {
+ public:
+  explicit FakeTaskRunner(MockQuicConnectionHelper* helper)
+      : tasks_([this](const TaskType& l, const TaskType& r) {
+          // Items at a later time should run after items at an earlier time.
+          // Priority queue comparisons should return true if l appears after r.
+          return l->time() > r->time();
+        }),
+        helper_(helper) {}
+
+  // Runs all tasks in time order.  Executes tasks scheduled at
+  // the same in an arbitrary order.
+  void Run() {
+    while (!tasks_.empty()) {
+      tasks_.top()->Run();
+      tasks_.pop();
+    }
+  }
+
+ private:
+  class InnerTask {
+   public:
+    InnerTask(std::function<void()> task, QuicTime time)
+        : task_(std::move(task)), time_(time) {}
+
+    void Cancel() { cancelled_ = true; }
+
+    void Run() {
+      if (!cancelled_) {
+        task_();
+      }
+    }
+
+    QuicTime time() const { return time_; }
+
+   private:
+    bool cancelled_ = false;
+    std::function<void()> task_;
+    QuicTime time_;
+  };
+
+ public:
+  // Schedules a function to run immediately and advances the time.
+  void Schedule(std::function<void()> task) {
+    tasks_.push(std::shared_ptr<InnerTask>(
+        new InnerTask(std::move(task), helper_->GetClock()->Now())));
+    helper_->AdvanceTime(QuicTime::Delta::FromMilliseconds(1));
+  }
+
+ private:
+  using TaskType = std::shared_ptr<InnerTask>;
+  std::priority_queue<TaskType,
+                      std::vector<TaskType>,
+                      std::function<bool(const TaskType&, const TaskType&)>>
+      tasks_;
+  MockQuicConnectionHelper* helper_;
+};
+
+class QboneSessionTest : public QuicTest {
+ public:
+  QboneSessionTest() : runner_(&helper_), compressed_certs_cache_(100) {}
+
+  ~QboneSessionTest() override {
+    delete client_connection_;
+    delete server_connection_;
+  }
+
+  const MockClock* GetClock() const {
+    return static_cast<const MockClock*>(helper_.GetClock());
+  }
+
+  // The parameters are used to control whether the handshake will success or
+  // not.
+  void CreateClientAndServerSessions(bool client_handshake_success = true,
+                                     bool server_handshake_success = true,
+                                     bool send_qbone_alpn = true) {
+    // Quic crashes if packets are sent at time 0, and the clock defaults to 0.
+    helper_.AdvanceTime(QuicTime::Delta::FromMilliseconds(1000));
+    alarm_factory_ = QuicMakeUnique<QuicEpollAlarmFactory>(&epoll_server_);
+    client_writer_ = QuicMakeUnique<DataSavingQbonePacketWriter>();
+    server_writer_ = QuicMakeUnique<DataSavingQbonePacketWriter>();
+    client_handler_ =
+        QuicMakeUnique<DataSavingQboneControlHandler<QboneClientRequest>>();
+    server_handler_ =
+        QuicMakeUnique<DataSavingQboneControlHandler<QboneServerRequest>>();
+    QuicSocketAddress server_address(TestLoopback(), QuicPickUnusedPortOrDie());
+    QuicSocketAddress client_address;
+    if (server_address.host().address_family() == IpAddressFamily::IP_V4) {
+      client_address = QuicSocketAddress(QuicIpAddress::Any4(), 0);
+    } else {
+      client_address = QuicSocketAddress(QuicIpAddress::Any6(), 0);
+    }
+
+    {
+      client_connection_ = new QuicConnection(
+          TestConnectionId(), server_address, &helper_, alarm_factory_.get(),
+          new NiceMock<MockPacketWriter>(), true, Perspective::IS_CLIENT,
+          ParsedVersionOfIndex(AllSupportedVersions(), 0));
+      client_connection_->SetSelfAddress(client_address);
+      QuicConfig config;
+      client_crypto_config_ = QuicMakeUnique<QuicCryptoClientConfig>(
+          QuicMakeUnique<FakeProofVerifier>(client_handshake_success));
+      if (send_qbone_alpn) {
+        client_crypto_config_->set_alpn("qbone");
+      }
+      client_peer_ = QuicMakeUnique<QboneClientSession>(
+          client_connection_, client_crypto_config_.get(),
+          /*owner=*/nullptr, config,
+          ParsedVersionOfIndex(AllSupportedVersions(), 0),
+          QuicServerId("test.example.com", 1234, false), client_writer_.get(),
+          client_handler_.get());
+    }
+
+    {
+      server_connection_ = new QuicConnection(
+          TestConnectionId(), client_address, &helper_, alarm_factory_.get(),
+          new NiceMock<MockPacketWriter>(), true, Perspective::IS_SERVER,
+          ParsedVersionOfIndex(AllSupportedVersions(), 0));
+      server_connection_->SetSelfAddress(server_address);
+      QuicConfig config;
+      server_crypto_config_ = QuicMakeUnique<QuicCryptoServerConfig>(
+          "TESTING", QuicRandom::GetInstance(),
+          std::unique_ptr<FakeProofSource>(
+              new FakeProofSource(server_handshake_success)),
+          KeyExchangeSource::Default());
+      QuicCryptoServerConfig::ConfigOptions options;
+      QuicServerConfigProtobuf primary_config =
+          server_crypto_config_->GenerateConfig(QuicRandom::GetInstance(),
+                                                GetClock(), options);
+      std::unique_ptr<CryptoHandshakeMessage> message(
+          server_crypto_config_->AddConfig(std::move(primary_config),
+                                           GetClock()->WallNow()));
+
+      server_peer_ = QuicMakeUnique<QboneServerSession>(
+          AllSupportedVersions(), server_connection_, nullptr, config,
+          server_crypto_config_.get(), &compressed_certs_cache_,
+          server_writer_.get(), TestLoopback6(), TestLoopback6(), 64,
+          server_handler_.get());
+    }
+
+    // Hook everything up!
+    MockPacketWriter* client_writer = static_cast<MockPacketWriter*>(
+        QuicConnectionPeer::GetWriter(client_peer_->connection()));
+    ON_CALL(*client_writer, WritePacket(_, _, _, _, _))
+        .WillByDefault(Invoke([this](const char* buffer, size_t buf_len,
+                                     const QuicIpAddress& self_address,
+                                     const QuicSocketAddress& peer_address,
+                                     PerPacketOptions* options) {
+          char* copy = new char[1024 * 1024];
+          memcpy(copy, buffer, buf_len);
+          runner_.Schedule([this, copy, buf_len] {
+            QuicReceivedPacket packet(copy, buf_len, GetClock()->Now());
+            server_peer_->ProcessUdpPacket(server_connection_->self_address(),
+                                           client_connection_->self_address(),
+                                           packet);
+            delete[] copy;
+          });
+          return WriteResult(WRITE_STATUS_OK, buf_len);
+        }));
+    MockPacketWriter* server_writer = static_cast<MockPacketWriter*>(
+        QuicConnectionPeer::GetWriter(server_peer_->connection()));
+    ON_CALL(*server_writer, WritePacket(_, _, _, _, _))
+        .WillByDefault(Invoke([this](const char* buffer, size_t buf_len,
+                                     const QuicIpAddress& self_address,
+                                     const QuicSocketAddress& peer_address,
+                                     PerPacketOptions* options) {
+          char* copy = new char[1024 * 1024];
+          memcpy(copy, buffer, buf_len);
+          runner_.Schedule([this, copy, buf_len] {
+            QuicReceivedPacket packet(copy, buf_len, GetClock()->Now());
+            client_peer_->ProcessUdpPacket(client_connection_->self_address(),
+                                           server_connection_->self_address(),
+                                           packet);
+            delete[] copy;
+          });
+          return WriteResult(WRITE_STATUS_OK, buf_len);
+        }));
+  }
+
+  void StartHandshake() {
+    server_peer_->Initialize();
+    client_peer_->Initialize();
+    runner_.Run();
+  }
+
+  // Test handshake establishment and sending/receiving of data for two
+  // directions.
+  void TestStreamConnection() {
+    ASSERT_TRUE(server_peer_->IsCryptoHandshakeConfirmed());
+    ASSERT_TRUE(client_peer_->IsCryptoHandshakeConfirmed());
+    ASSERT_TRUE(server_peer_->IsEncryptionEstablished());
+    ASSERT_TRUE(client_peer_->IsEncryptionEstablished());
+
+    // Create an outgoing stream from the client and say hello.
+    QUIC_LOG(INFO) << "Sending client -> server";
+    client_peer_->ProcessPacketFromNetwork(TestPacketIn("hello"));
+    client_peer_->ProcessPacketFromNetwork(TestPacketIn("world"));
+    runner_.Run();
+    // The server should see the data, the client hasn't received
+    // anything yet.
+    EXPECT_THAT(server_writer_->data(),
+                ElementsAre(TestPacketOut("hello"), TestPacketOut("world")));
+    EXPECT_TRUE(client_writer_->data().empty());
+    EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
+    EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
+
+    // Let's pretend some service responds.
+    QUIC_LOG(INFO) << "Sending server -> client";
+    server_peer_->ProcessPacketFromNetwork(TestPacketIn("Hello Again"));
+    server_peer_->ProcessPacketFromNetwork(TestPacketIn("Again"));
+    runner_.Run();
+    EXPECT_THAT(server_writer_->data(),
+                ElementsAre(TestPacketOut("hello"), TestPacketOut("world")));
+    EXPECT_THAT(
+        client_writer_->data(),
+        ElementsAre(TestPacketOut("Hello Again"), TestPacketOut("Again")));
+    EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
+    EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
+
+    // Try to send long payloads that are larger than the QUIC MTU but
+    // smaller than the QBONE max size.
+    // This should trigger the non-ephemeral stream code path.
+    string long_data(QboneConstants::kMaxQbonePacketBytes - sizeof(ip6_hdr) - 1,
+                     'A');
+    QUIC_LOG(INFO) << "Sending server -> client long data";
+    server_peer_->ProcessPacketFromNetwork(TestPacketIn(long_data));
+    runner_.Run();
+    EXPECT_THAT(client_writer_->data(), Contains(TestPacketOut(long_data)));
+    EXPECT_THAT(server_writer_->data(),
+                Not(Contains(TestPacketOut(long_data))));
+    EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
+    EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
+
+    QUIC_LOG(INFO) << "Sending client -> server long data";
+    client_peer_->ProcessPacketFromNetwork(TestPacketIn(long_data));
+    runner_.Run();
+    EXPECT_THAT(server_writer_->data(), Contains(TestPacketOut(long_data)));
+    EXPECT_THAT(client_peer_->GetNumSentClientHellos(), Eq(2));
+    EXPECT_THAT(client_peer_->GetNumReceivedServerConfigUpdates(), Eq(0));
+    EXPECT_THAT(client_peer_->GetNumEphemeralPackets(), Eq(2));
+    EXPECT_THAT(client_peer_->GetNumStreamedPackets(), Eq(1));
+    EXPECT_THAT(server_peer_->GetNumEphemeralPackets(), Eq(2));
+    EXPECT_THAT(server_peer_->GetNumStreamedPackets(), Eq(1));
+
+    // All streams are ephemeral and should be gone.
+    EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
+    EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
+  }
+
+  // Test that client and server are not connected after handshake failure.
+  void TestDisconnectAfterFailedHandshake() {
+    EXPECT_FALSE(client_peer_->IsEncryptionEstablished());
+    EXPECT_FALSE(client_peer_->IsCryptoHandshakeConfirmed());
+
+    EXPECT_FALSE(server_peer_->IsEncryptionEstablished());
+    EXPECT_FALSE(server_peer_->IsCryptoHandshakeConfirmed());
+  }
+
+ protected:
+  QuicEpollServer epoll_server_;
+  std::unique_ptr<QuicAlarmFactory> alarm_factory_;
+  FakeTaskRunner runner_;
+  MockQuicConnectionHelper helper_;
+  QuicConnection* client_connection_;
+  QuicConnection* server_connection_;
+  QuicCompressedCertsCache compressed_certs_cache_;
+
+  std::unique_ptr<QuicCryptoClientConfig> client_crypto_config_;
+  std::unique_ptr<QuicCryptoServerConfig> server_crypto_config_;
+  std::unique_ptr<DataSavingQbonePacketWriter> client_writer_;
+  std::unique_ptr<DataSavingQbonePacketWriter> server_writer_;
+  std::unique_ptr<DataSavingQboneControlHandler<QboneClientRequest>>
+      client_handler_;
+  std::unique_ptr<DataSavingQboneControlHandler<QboneServerRequest>>
+      server_handler_;
+
+  std::unique_ptr<QboneServerSession> server_peer_;
+  std::unique_ptr<QboneClientSession> client_peer_;
+};
+
+TEST_F(QboneSessionTest, StreamConnection) {
+  CreateClientAndServerSessions();
+  StartHandshake();
+  TestStreamConnection();
+}
+
+TEST_F(QboneSessionTest, ClientRejection) {
+  CreateClientAndServerSessions(false /*client_handshake_success*/,
+                                true /*server_handshake_success*/,
+                                true /*send_qbone_alpn*/);
+  StartHandshake();
+  TestDisconnectAfterFailedHandshake();
+}
+
+TEST_F(QboneSessionTest, BadAlpn) {
+  CreateClientAndServerSessions(true /*client_handshake_success*/,
+                                true /*server_handshake_success*/,
+                                false /*send_qbone_alpn*/);
+  StartHandshake();
+  TestDisconnectAfterFailedHandshake();
+}
+
+TEST_F(QboneSessionTest, ServerRejection) {
+  CreateClientAndServerSessions(true /*client_handshake_success*/,
+                                false /*server_handshake_success*/,
+                                true /*send_qbone_alpn*/);
+  StartHandshake();
+  TestDisconnectAfterFailedHandshake();
+}
+
+// Test that data streams are not created before handshake.
+TEST_F(QboneSessionTest, CannotCreateDataStreamBeforeHandshake) {
+  CreateClientAndServerSessions();
+  EXPECT_QUIC_BUG(client_peer_->ProcessPacketFromNetwork(TestPacketIn("hello")),
+                  "Failed to create an outgoing QBONE stream");
+  EXPECT_QUIC_BUG(server_peer_->ProcessPacketFromNetwork(TestPacketIn("hello")),
+                  "Failed to create an outgoing QBONE stream");
+  EXPECT_EQ(0u, server_peer_->GetNumActiveStreams());
+  EXPECT_EQ(0u, client_peer_->GetNumActiveStreams());
+}
+
+TEST_F(QboneSessionTest, ControlRequests) {
+  CreateClientAndServerSessions();
+  StartHandshake();
+  EXPECT_TRUE(client_handler_->data().empty());
+  EXPECT_FALSE(client_handler_->error());
+  EXPECT_TRUE(server_handler_->data().empty());
+  EXPECT_FALSE(server_handler_->error());
+
+  QboneClientRequest client_request;
+  client_request.SetExtension(client_placeholder, "hello from the server");
+  EXPECT_TRUE(server_peer_->SendClientRequest(client_request));
+  runner_.Run();
+  ASSERT_FALSE(client_handler_->data().empty());
+  EXPECT_THAT(client_handler_->data()[0].GetExtension(client_placeholder),
+              Eq("hello from the server"));
+  EXPECT_FALSE(client_handler_->error());
+
+  QboneServerRequest server_request;
+  server_request.SetExtension(server_placeholder, "hello from the client");
+  EXPECT_TRUE(client_peer_->SendServerRequest(server_request));
+  runner_.Run();
+  ASSERT_FALSE(server_handler_->data().empty());
+  EXPECT_THAT(server_handler_->data()[0].GetExtension(server_placeholder),
+              Eq("hello from the client"));
+  EXPECT_FALSE(server_handler_->error());
+}
+
+}  // namespace
+}  // namespace test
+}  // namespace quic
diff --git a/quic/qbone/qbone_stream.cc b/quic/qbone/qbone_stream.cc
new file mode 100644
index 0000000..b7ac007
--- /dev/null
+++ b/quic/qbone/qbone_stream.cc
@@ -0,0 +1,61 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_stream.h"
+
+#include "net/third_party/quiche/src/quic/core/quic_data_reader.h"
+#include "net/third_party/quiche/src/quic/core/quic_data_writer.h"
+#include "net/third_party/quiche/src/quic/core/quic_types.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_string_piece.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_text_utils.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_session_base.h"
+
+namespace quic {
+
+QboneWriteOnlyStream::QboneWriteOnlyStream(QuicStreamId id,
+                                           QuicSession* session)
+    : QuicStream(id, session, /*is_static=*/false, WRITE_UNIDIRECTIONAL) {
+  // QBONE uses a LIFO queue to try to always make progress. An individual
+  // packet may persist for upto to 10 seconds in memory.
+  MaybeSetTtl(QuicTime::Delta::FromSeconds(10));
+}
+
+void QboneWriteOnlyStream::WritePacketToQuicStream(QuicStringPiece packet) {
+  // Streams are one way and ephemeral. This function should only be
+  // called once.
+  WriteOrBufferData(packet, /* fin= */ true, nullptr);
+}
+
+QboneReadOnlyStream::QboneReadOnlyStream(QuicStreamId id,
+                                         QboneSessionBase* session)
+    : QuicStream(id,
+                 session,
+                 /*is_static=*/false,
+                 READ_UNIDIRECTIONAL),
+      session_(session) {
+  // QBONE uses a LIFO queue to try to always make progress. An individual
+  // packet may persist for upto to 10 seconds in memory.
+  MaybeSetTtl(QuicTime::Delta::FromSeconds(10));
+}
+
+QboneReadOnlyStream::~QboneReadOnlyStream() {}
+
+void QboneReadOnlyStream::OnDataAvailable() {
+  // Read in data and buffer it, attempt to frame to see if there's a packet.
+  sequencer()->Read(&buffer_);
+  if (sequencer()->IsClosed()) {
+    session_->ProcessPacketFromPeer(buffer_);
+    OnFinRead();
+    return;
+  }
+  if (buffer_.size() > QboneConstants::kMaxQbonePacketBytes) {
+    if (!rst_sent()) {
+      Reset(QUIC_BAD_APPLICATION_PAYLOAD);
+    }
+    StopReading();
+  }
+}
+
+}  // namespace quic
diff --git a/quic/qbone/qbone_stream.h b/quic/qbone/qbone_stream.h
new file mode 100644
index 0000000..7368005
--- /dev/null
+++ b/quic/qbone/qbone_stream.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2019 The Chromium 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 QUICHE_QUIC_QBONE_QBONE_STREAM_H_
+#define QUICHE_QUIC_QBONE_QBONE_STREAM_H_
+
+#include "net/third_party/quiche/src/quic/core/quic_session.h"
+#include "net/third_party/quiche/src/quic/core/quic_stream.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_export.h"
+
+namespace quic {
+
+class QboneSessionBase;
+
+// QboneWriteOnlyStream is responsible for sending data for a single
+// packet to the other side.
+// Note that the stream will be created HalfClosed (reads will be closed).
+class QUIC_EXPORT_PRIVATE QboneWriteOnlyStream : public QuicStream {
+ public:
+  QboneWriteOnlyStream(QuicStreamId id, QuicSession* session);
+
+  // QuicStream implementation. Qbone writers are ephemeral and don't
+  // read any data.
+  void OnDataAvailable() override {}
+
+  // Write a network packet over the quic stream.
+  void WritePacketToQuicStream(QuicStringPiece packet);
+};
+
+// QboneReadOnlyStream will be used if we find an incoming stream that
+// isn't fully contained. It will buffer the data when available and
+// attempt to parse it as a packet to send to the network when a FIN
+// is found.
+// Note that the stream will be created HalfClosed (writes will be closed).
+class QUIC_EXPORT_PRIVATE QboneReadOnlyStream : public QuicStream {
+ public:
+  QboneReadOnlyStream(QuicStreamId id, QboneSessionBase* session);
+
+  ~QboneReadOnlyStream() override;
+
+  // QuicStream overrides.
+  // OnDataAvailable is called when there is data in the quic stream buffer.
+  // This will copy the buffer locally and attempt to parse it to write out
+  // packets to the network.
+  void OnDataAvailable() override;
+
+ private:
+  string buffer_;
+  QboneSessionBase* session_;
+};
+
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_QBONE_QBONE_STREAM_H_
diff --git a/quic/qbone/qbone_stream_test.cc b/quic/qbone/qbone_stream_test.cc
new file mode 100644
index 0000000..1920aa5
--- /dev/null
+++ b/quic/qbone/qbone_stream_test.cc
@@ -0,0 +1,249 @@
+// Copyright (c) 2019 The Chromium 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/third_party/quiche/src/quic/qbone/qbone_stream.h"
+
+#include "net/third_party/quiche/src/quic/core/crypto/quic_random.h"
+#include "net/third_party/quiche/src/quic/core/quic_session.h"
+#include "net/third_party/quiche/src/quic/core/quic_simple_buffer_allocator.h"
+#include "net/third_party/quiche/src/quic/core/quic_utils.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_ptr_util.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_test_loopback.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_constants.h"
+#include "net/third_party/quiche/src/quic/qbone/qbone_session_base.h"
+#include "net/third_party/quiche/src/quic/test_tools/mock_clock.h"
+#include "net/third_party/quiche/src/quic/test_tools/quic_test_utils.h"
+#include "net/third_party/quiche/src/spdy/core/spdy_protocol.h"
+
+namespace quic {
+
+namespace {
+
+using ::testing::_;
+using ::testing::StrictMock;
+
+// MockQuicSession that does not create streams and writes data from
+// QuicStream to a string.
+class MockQuicSession : public QboneSessionBase {
+ public:
+  MockQuicSession(QuicConnection* connection, const QuicConfig& config)
+      : QboneSessionBase(connection,
+                         nullptr /*visitor*/,
+                         config,
+                         CurrentSupportedVersions(),
+                         nullptr /*writer*/) {}
+
+  ~MockQuicSession() override {}
+
+  // Writes outgoing data from QuicStream to a string.
+  QuicConsumedData WritevData(QuicStream* stream,
+                              QuicStreamId id,
+                              size_t write_length,
+                              QuicStreamOffset offset,
+                              StreamSendingState state) override {
+    if (!writable_) {
+      return QuicConsumedData(0, false);
+    }
+
+    return QuicConsumedData(write_length, state != StreamSendingState::NO_FIN);
+  }
+
+  QboneReadOnlyStream* CreateIncomingStream(QuicStreamId id) override {
+    return nullptr;
+  }
+
+  const QuicCryptoStream* GetCryptoStream() const override { return nullptr; }
+  QuicCryptoStream* GetMutableCryptoStream() override { return nullptr; }
+
+  // Called by QuicStream when they want to close stream.
+  MOCK_METHOD3(SendRstStream,
+               void(QuicStreamId, QuicRstStreamErrorCode, QuicStreamOffset));
+
+  // Sets whether data is written to buffer, or else if this is write blocked.
+  void set_writable(bool writable) { writable_ = writable; }
+
+  // Tracks whether the stream is write blocked and its priority.
+  void RegisterReliableStream(QuicStreamId stream_id) {
+    // The priority effectively does not matter. Put all streams on the same
+    // priority.
+    write_blocked_streams()->RegisterStream(
+        stream_id,
+        /*is_static_stream=*/false,
+        /* precedence= */ spdy::SpdyStreamPrecedence(3));
+  }
+
+  // The session take ownership of the stream.
+  void ActivateReliableStream(std::unique_ptr<QuicStream> stream) {
+    ActivateStream(std::move(stream));
+  }
+
+  std::unique_ptr<QuicCryptoStream> CreateCryptoStream() override {
+    return nullptr;
+  }
+
+  MOCK_METHOD1(ProcessPacketFromPeer, void(QuicStringPiece));
+  MOCK_METHOD1(ProcessPacketFromNetwork, void(QuicStringPiece));
+
+ private:
+  // Whether data is written to write_buffer_.
+  bool writable_ = true;
+};
+
+// Packet writer that does nothing. This is required for QuicConnection but
+// isn't used for writing data.
+class DummyPacketWriter : public QuicPacketWriter {
+ public:
+  DummyPacketWriter() {}
+
+  // QuicPacketWriter overrides.
+  WriteResult WritePacket(const char* buffer,
+                          size_t buf_len,
+                          const QuicIpAddress& self_address,
+                          const QuicSocketAddress& peer_address,
+                          PerPacketOptions* options) override {
+    return WriteResult(WRITE_STATUS_ERROR, 0);
+  }
+
+  bool IsWriteBlocked() const override { return false; };
+
+  void SetWritable() override {}
+
+  QuicByteCount GetMaxPacketSize(
+      const QuicSocketAddress& peer_address) const override {
+    return 0;
+  }
+
+  bool SupportsReleaseTime() const override { return false; }
+
+  bool IsBatchMode() const override { return false; }
+
+  char* GetNextWriteLocation(const QuicIpAddress& self_address,
+                             const QuicSocketAddress& peer_address) override {
+    return nullptr;
+  }
+
+  WriteResult Flush() override { return WriteResult(WRITE_STATUS_OK, 0); }
+};
+
+class QboneReadOnlyStreamTest : public ::testing::Test,
+                                public QuicConnectionHelperInterface {
+ public:
+  void CreateReliableQuicStream() {
+    // Arbitrary values for QuicConnection.
+    Perspective perspective = Perspective::IS_SERVER;
+    bool owns_writer = true;
+
+    alarm_factory_ = QuicMakeUnique<test::MockAlarmFactory>();
+
+    connection_.reset(new QuicConnection(
+        test::TestConnectionId(0), QuicSocketAddress(TestLoopback(), 0),
+        this /*QuicConnectionHelperInterface*/, alarm_factory_.get(),
+        new DummyPacketWriter(), owns_writer, perspective,
+        ParsedVersionOfIndex(CurrentSupportedVersions(), 0)));
+    clock_.AdvanceTime(QuicTime::Delta::FromSeconds(1));
+    session_ = QuicMakeUnique<StrictMock<MockQuicSession>>(connection_.get(),
+                                                           QuicConfig());
+    stream_ = new QboneReadOnlyStream(kStreamId, session_.get());
+    session_->ActivateReliableStream(
+        std::unique_ptr<QboneReadOnlyStream>(stream_));
+  }
+
+  ~QboneReadOnlyStreamTest() override {}
+
+  const QuicClock* GetClock() const override { return &clock_; }
+
+  QuicRandom* GetRandomGenerator() override {
+    return QuicRandom::GetInstance();
+  }
+
+  QuicBufferAllocator* GetStreamSendBufferAllocator() override {
+    return &buffer_allocator_;
+  }
+
+ protected:
+  // The QuicSession will take the ownership.
+  QboneReadOnlyStream* stream_;
+  std::unique_ptr<StrictMock<MockQuicSession>> session_;
+  std::unique_ptr<QuicAlarmFactory> alarm_factory_;
+  std::unique_ptr<QuicConnection> connection_;
+  // Used to implement the QuicConnectionHelperInterface.
+  SimpleBufferAllocator buffer_allocator_;
+  MockClock clock_;
+  const QuicStreamId kStreamId = QuicUtils::GetFirstUnidirectionalStreamId(
+      CurrentSupportedVersions()[0].transport_version,
+      Perspective::IS_CLIENT);
+};
+
+// Read an entire string.
+TEST_F(QboneReadOnlyStreamTest, ReadDataWhole) {
+  string packet = "Stuff";
+  CreateReliableQuicStream();
+  QuicStreamFrame frame(kStreamId, true, 0, packet);
+  EXPECT_CALL(*session_, ProcessPacketFromPeer("Stuff"));
+  stream_->OnStreamFrame(frame);
+}
+
+// Test buffering.
+TEST_F(QboneReadOnlyStreamTest, ReadBuffered) {
+  CreateReliableQuicStream();
+  string packet = "Stuf";
+  {
+    QuicStreamFrame frame(kStreamId, false, 0, packet);
+    stream_->OnStreamFrame(frame);
+  }
+  // We didn't write 5 bytes yet...
+
+  packet = "f";
+  EXPECT_CALL(*session_, ProcessPacketFromPeer("Stuff"));
+  {
+    QuicStreamFrame frame(kStreamId, true, 4, packet);
+    stream_->OnStreamFrame(frame);
+  }
+}
+
+TEST_F(QboneReadOnlyStreamTest, ReadOutOfOrder) {
+  CreateReliableQuicStream();
+  string packet = "f";
+  {
+    QuicStreamFrame frame(kStreamId, true, 4, packet);
+    stream_->OnStreamFrame(frame);
+  }
+
+  packet = "S";
+  {
+    QuicStreamFrame frame(kStreamId, false, 0, packet);
+    stream_->OnStreamFrame(frame);
+  }
+
+  packet = "tuf";
+  EXPECT_CALL(*session_, ProcessPacketFromPeer("Stuff"));
+  {
+    QuicStreamFrame frame(kStreamId, false, 1, packet);
+    stream_->OnStreamFrame(frame);
+  }
+}
+
+// Test buffering too many bytes.
+TEST_F(QboneReadOnlyStreamTest, ReadBufferedTooLarge) {
+  CreateReliableQuicStream();
+  string packet = "0123456789";
+  int iterations = (QboneConstants::kMaxQbonePacketBytes / packet.size()) + 2;
+  EXPECT_CALL(*session_,
+              SendRstStream(kStreamId, QUIC_BAD_APPLICATION_PAYLOAD, _));
+  for (int i = 0; i < iterations; ++i) {
+    QuicStreamFrame frame(kStreamId, i == (iterations - 1), i * packet.size(),
+                          packet);
+    if (!stream_->reading_stopped()) {
+      stream_->OnStreamFrame(frame);
+    }
+  }
+  // We should have nothing written to the network and the stream
+  // should have stopped reading.
+  EXPECT_TRUE(stream_->reading_stopped());
+}
+
+}  // namespace
+
+}  // namespace quic