[overnet] Stream links, utilization fixes

Change-Id: Idcf5f82b1dbf71f255646a2b8514b95dac456fe8
diff --git a/garnet/packages/prod/BUILD.gn b/garnet/packages/prod/BUILD.gn
index 07ad31cb..11963f3 100644
--- a/garnet/packages/prod/BUILD.gn
+++ b/garnet/packages/prod/BUILD.gn
@@ -385,6 +385,7 @@
   testonly = true
   public_deps = [
     "//src/connectivity/overnet/overnetstack",
+    "//src/connectivity/overnet/tools/ascendd",
     "//src/connectivity/overnet/tools/onet",
   ]
 }
diff --git a/sdk/fidl/fuchsia.overnet.protocol/BUILD.gn b/sdk/fidl/fuchsia.overnet.protocol/BUILD.gn
index f2a1031..e64a99c 100644
--- a/sdk/fidl/fuchsia.overnet.protocol/BUILD.gn
+++ b/sdk/fidl/fuchsia.overnet.protocol/BUILD.gn
@@ -9,6 +9,7 @@
     "labels.fidl",
     "peer_protocol.fidl",
     "routing.fidl",
+    "stream_socket.fidl",
     "zircon_proxy.fidl",
   ]
 }
diff --git a/sdk/fidl/fuchsia.overnet.protocol/labels.fidl b/sdk/fidl/fuchsia.overnet.protocol/labels.fidl
index 015976a..badab0a 100644
--- a/sdk/fidl/fuchsia.overnet.protocol/labels.fidl
+++ b/sdk/fidl/fuchsia.overnet.protocol/labels.fidl
@@ -14,6 +14,9 @@
     uint64 id;
 };
 
+/// Node-local link label
+using LinkId = uint64;
+
 /// Reliability and ordering constraints for a stream.
 enum ReliabilityAndOrdering {
     /// Datagrams are delivered reliably in an ordered fashion.
diff --git a/sdk/fidl/fuchsia.overnet.protocol/routing.fidl b/sdk/fidl/fuchsia.overnet.protocol/routing.fidl
index dc588a8..f14eaec 100644
--- a/sdk/fidl/fuchsia.overnet.protocol/routing.fidl
+++ b/sdk/fidl/fuchsia.overnet.protocol/routing.fidl
@@ -37,7 +37,7 @@
     /// An identifier (chosen by node `from`) to label this link.
     /// `from` must guarantee that the tuple (from, to, local_id) is unique
     /// for each of it's held links.
-    uint64 local_id;
+    LinkId local_id;
     /// A monotonically increasing version counter for this links status.
     uint64 version;
 
diff --git a/sdk/fidl/fuchsia.overnet.protocol/stream_socket.fidl b/sdk/fidl/fuchsia.overnet.protocol/stream_socket.fidl
new file mode 100644
index 0000000..37984f0
--- /dev/null
+++ b/sdk/fidl/fuchsia.overnet.protocol/stream_socket.fidl
@@ -0,0 +1,15 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library fuchsia.overnet.protocol;
+
+// Introduction packet sent on stream oriented links between Overnet nodes
+table StreamSocketGreeting {
+    // Protocol identification string; different kinds of streams might choose a different value here
+    1: string:32 magic_string;
+    // Overnet NodeId of the sender
+    2: NodeId node_id;
+    // Sender's LinkId for this link
+    3: LinkId local_link_id;
+};
diff --git a/src/connectivity/overnet/lib/BUILD.gn b/src/connectivity/overnet/lib/BUILD.gn
index e645b12..203336e9 100644
--- a/src/connectivity/overnet/lib/BUILD.gn
+++ b/src/connectivity/overnet/lib/BUILD.gn
@@ -18,7 +18,6 @@
     "packet_protocol:lib",
     "protocol:lib",
     "routing:lib",
-    "stats:lib",
     "vocabulary:lib",
   ]
 }
@@ -26,6 +25,7 @@
 test("overnet_unittests") {
   deps = [
     "datagram_stream:tests",
+    "embedded:tests",
     "endpoint:tests",
     "environment:tests",
     "labels:tests",
diff --git a/src/connectivity/overnet/lib/datagram_stream/BUILD.gn b/src/connectivity/overnet/lib/datagram_stream/BUILD.gn
index dd0e301..3325422 100644
--- a/src/connectivity/overnet/lib/datagram_stream/BUILD.gn
+++ b/src/connectivity/overnet/lib/datagram_stream/BUILD.gn
@@ -27,13 +27,14 @@
     "datagram_stream.cc",
     "datagram_stream.h",
   ]
-  deps = [
+  public_deps = [
     ":linearizer",
     ":receive_mode",
     "//sdk/fidl/fuchsia.overnet.protocol",
     "//src/connectivity/overnet/lib/environment:timer",
     "//src/connectivity/overnet/lib/environment:trace",
     "//src/connectivity/overnet/lib/labels:seq_num",
+    "//src/connectivity/overnet/lib/stats:stream",
     "//src/connectivity/overnet/lib/packet_protocol",
     "//src/connectivity/overnet/lib/routing:router",
     "//src/connectivity/overnet/lib/vocabulary:internal_list",
@@ -65,6 +66,7 @@
   ]
   deps = [
     "//src/connectivity/overnet/lib/environment:trace",
+    "//src/connectivity/overnet/lib/stats:stream",
     "//src/connectivity/overnet/lib/vocabulary:callback",
     "//src/connectivity/overnet/lib/vocabulary:optional",
     "//src/connectivity/overnet/lib/vocabulary:slice",
diff --git a/src/connectivity/overnet/lib/datagram_stream/datagram_stream.cc b/src/connectivity/overnet/lib/datagram_stream/datagram_stream.cc
index 7078437..ee54605 100644
--- a/src/connectivity/overnet/lib/datagram_stream/datagram_stream.cc
+++ b/src/connectivity/overnet/lib/datagram_stream/datagram_stream.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/datagram_stream/datagram_stream.h"
+
 #include <sstream>
 
 namespace overnet {
@@ -128,7 +129,7 @@
       // TODO(ctiller): What should mss be? Hardcoding to 2048 for now.
       packet_protocol_(
           timer_, [router] { return (*router->rng())(); }, this,
-          PacketProtocol::NullCodec(), 2048) {}
+          PacketProtocol::NullCodec(), 2048, true) {}
 
 void DatagramStream::Register() {
   ScopedModule<DatagramStream> scoped_module(this);
@@ -574,6 +575,7 @@
   }
   push_offset_ += chunk_length;
   Chunk chunk{chunk_start, end_byte == payload_length_, std::move(item)};
+  stream()->stream_stats_.send_chunk_push++;
   stream()->SendChunk(*this, std::move(chunk), std::move(started));
 }
 
@@ -742,6 +744,7 @@
                            << " message_id_length=" << (int)message_id_length;
       if (args->max_length <=
           message_id_length + varint::WireSizeFor(pending_send.chunk.offset)) {
+        stream->stream_stats_.send_chunk_cancel_packet_too_small++;
         stream->SendChunk(pending_send.state, std::move(pending_send.chunk),
                           std::move(cb));
         return ChunkAndOptState{Nothing, pending_send.state};
@@ -751,12 +754,16 @@
                  varint::WireSizeFor(pending_send.chunk.offset));
       uint64_t take_len =
           varint::MaximumLengthWithPrefix(args->max_length - message_id_length);
-      OVERNET_TRACE(DEBUG) << "TAKE " << take_len;
+      OVERNET_TRACE(DEBUG) << "TAKE " << take_len << " from "
+                           << pending_send.chunk.slice.length();
       if (take_len < pending_send.chunk.slice.length()) {
+        stream->stream_stats_.send_chunk_split_packet_too_small++;
         Chunk first = pending_send.chunk.TakeUntilSliceOffset(take_len);
         stream->SendChunk(pending_send.state, std::move(pending_send.chunk),
                           Callback<void>::Ignored());
         pending_send.chunk = std::move(first);
+      } else {
+        stream->stream_stats_.send_chunk_take_entire_chunk++;
       }
       return ChunkAndOptState{pending_send.chunk, pending_send.state};
     }
@@ -874,6 +881,7 @@
   if (status.code() == StatusCode::UNAVAILABLE &&
       !state.stream()->IsClosedForSending()) {
     // Send failed, still open, and retryable: retry.
+    stream_stats_.send_chunk_nacked++;
     SendChunk(std::move(state), std::move(chunk), Callback<void>::Ignored());
   }
 }
diff --git a/src/connectivity/overnet/lib/datagram_stream/datagram_stream.h b/src/connectivity/overnet/lib/datagram_stream/datagram_stream.h
index eb37630..97faf9a 100644
--- a/src/connectivity/overnet/lib/datagram_stream/datagram_stream.h
+++ b/src/connectivity/overnet/lib/datagram_stream/datagram_stream.h
@@ -4,7 +4,6 @@
 
 #pragma once
 
-#include <queue>  // TODO(ctiller): switch to a short queue (inlined 1-2 elems, linked list)
 #include "src/connectivity/overnet/lib/datagram_stream/linearizer.h"
 #include "src/connectivity/overnet/lib/datagram_stream/receive_mode.h"
 #include "src/connectivity/overnet/lib/environment/timer.h"
@@ -12,6 +11,7 @@
 #include "src/connectivity/overnet/lib/labels/seq_num.h"
 #include "src/connectivity/overnet/lib/packet_protocol/packet_protocol.h"
 #include "src/connectivity/overnet/lib/routing/router.h"
+#include "src/connectivity/overnet/lib/stats/stream.h"
 #include "src/connectivity/overnet/lib/vocabulary/internal_list.h"
 #include "src/connectivity/overnet/lib/vocabulary/slice.h"
 
@@ -137,7 +137,12 @@
         Module::DATAGRAM_STREAM_INCOMING_MESSAGE;
 
     IncomingMessage(DatagramStream* stream, uint64_t msg_id)
-        : linearizer_(2 * stream->packet_protocol_.maximum_send_size()),
+        : protocol_(&stream->packet_protocol_),
+          // TODO(ctiller): What should the bound be here? 4*mss is a guess,
+          // nothing more.
+          linearizer_(std::max(uint64_t(4 * protocol_->maximum_send_size()),
+                               protocol_->bdp_estimate()),
+                      &stream->stream_stats_),
           msg_id_(msg_id) {}
 
     void Pull(StatusOrCallback<Optional<Slice>>&& done) {
@@ -152,6 +157,7 @@
 
     [[nodiscard]] bool Push(Chunk&& chunk) {
       ScopedModule<IncomingMessage> in_im(this);
+      linearizer_.UpdateMaxBuffer(protocol_->bdp_estimate());
       return linearizer_.Push(std::forward<Chunk>(chunk));
     }
 
@@ -167,6 +173,7 @@
     InternalListNode<IncomingMessage> incoming_link;
 
    private:
+    PacketProtocol* const protocol_;
     Linearizer linearizer_;
     const uint64_t msg_id_;
   };
@@ -341,6 +348,9 @@
 
   NodeId peer() const { return peer_; }
 
+  const LinkStats* link_stats() const { return packet_protocol_.stats(); }
+  const StreamStats* stream_stats() const { return &stream_stats_; }
+
  protected:
   // Must be called by derived classes, after construction and before any other
   // methods.
@@ -376,6 +386,7 @@
   uint64_t largest_incoming_message_id_seen_ = 0;
   receive_mode::ParameterizedReceiveMode receive_mode_;
   PacketProtocol packet_protocol_;
+  StreamStats stream_stats_;
   StreamRef close_ref_{this};  // Keep stream alive until closed
 
   enum class CloseState : uint8_t {
diff --git a/src/connectivity/overnet/lib/datagram_stream/linearizer.cc b/src/connectivity/overnet/lib/datagram_stream/linearizer.cc
index 1812919..6275edd 100644
--- a/src/connectivity/overnet/lib/datagram_stream/linearizer.cc
+++ b/src/connectivity/overnet/lib/datagram_stream/linearizer.cc
@@ -13,7 +13,8 @@
 
 namespace overnet {
 
-Linearizer::Linearizer(uint64_t max_buffer) : max_buffer_(max_buffer) {}
+Linearizer::Linearizer(uint64_t max_buffer, StreamStats* stats)
+    : max_buffer_(max_buffer), stats_(stats) {}
 
 Linearizer::~Linearizer() {
   switch (read_mode_) {
@@ -140,6 +141,7 @@
   if (chunk_start > offset_ && chunk_end > offset_ + max_buffer_) {
     OVERNET_TRACE(DEBUG) << "Push reject: past end of buffering window;"
                          << " max_buffer=" << max_buffer_;
+    stats_->linearizer_reject_past_end_of_buffering++;
     return false;
   }
 
@@ -180,6 +182,7 @@
   }
 
   if (chunk_end == chunk_start) {
+    stats_->linearizer_empty_chunk++;
     return true;
   }
 
@@ -188,6 +191,7 @@
   if (read_mode_ == ReadMode::ReadSlice && chunk_start == offset_ &&
       (pending_push_.empty() || pending_push_.begin()->first > chunk_end)) {
     OVERNET_TRACE(DEBUG) << "Push: fast-path";
+    stats_->linearizer_fast_path_taken++;
     offset_ += chunk.slice.length();
     auto push = std::move(ReadSliceToIdle().done);
     if (length_) {
@@ -206,10 +210,12 @@
   if (chunk_start < offset_) {
     if (chunk_end > offset_) {
       OVERNET_TRACE(DEBUG) << "Push: trim begin";
+      stats_->linearizer_partial_ignore_begin++;
       chunk.TrimBegin(offset_ - chunk_start);
       chunk_start = chunk.offset;
     } else {
       OVERNET_TRACE(DEBUG) << "Push: all prior";
+      stats_->linearizer_ignore_all_prior++;
       return true;
     }
   }
@@ -220,8 +226,10 @@
   // exit conditions, and we've got some common checks to do once it's finished.
   if (pending_push_.empty()) {
     OVERNET_TRACE(DEBUG) << "Push: first pending";
+    stats_->linearizer_new_pending_queue++;
     pending_push_.emplace(chunk.offset, std::move(chunk.slice));
   } else {
+    stats_->linearizer_integrations++;
     IntegratePush(std::move(chunk));
   }
 
@@ -256,15 +264,18 @@
     OVERNET_TRACE(DEBUG) << "coincident with existing; common_length="
                          << common_length;
     if (0 != memcmp(chunk.slice.begin(), lb->second.begin(), common_length)) {
+      stats_->linearizer_integration_errors++;
       Close(Status(StatusCode::DATA_LOSS,
                    "Linearizer received different bytes for the same span"))
           .Ignore();
     } else if (chunk.slice.length() <= lb->second.length()) {
       // New chunk is shorter than what's there (or the same length): We're
       // done.
+      stats_->linearizer_integration_coincident_shorter++;
     } else {
       // New chunk is bigger than what's there: we create a new (tail) chunk and
       // continue integration
+      stats_->linearizer_integration_coincident_longer++;
       chunk.TrimBegin(lb->second.length());
       IntegratePush(std::move(chunk));
     }
@@ -289,13 +300,16 @@
                            << common_length;
       if (0 != memcmp(before->second.begin() + (chunk.offset - before->first),
                       chunk.slice.begin(), common_length)) {
+        stats_->linearizer_integration_errors++;
         Close(Status(StatusCode::DATA_LOSS,
                      "Linearizer received different bytes for the same span"))
             .Ignore();
       } else if (before_end >= chunk.offset + chunk.slice.length()) {
         // New chunk is a subset of the one before: we're done.
+        stats_->linearizer_integration_prior_longer++;
       } else {
         // Trim the new chunk and continue integration.
+        stats_->linearizer_integration_prior_partial++;
         chunk.TrimBegin(before_end - chunk.offset);
         IntegratePush(std::move(chunk));
       }
@@ -320,6 +334,7 @@
       if (0 != memcmp(after->second.begin(),
                       chunk.slice.begin() + (after->first - chunk.offset),
                       common_length)) {
+        stats_->linearizer_integration_errors++;
         Close(Status(StatusCode::DATA_LOSS,
                      "Linearizer received different bytes for the same span"))
             .Ignore();
@@ -327,6 +342,7 @@
       } else if (after->first + after->second.length() <
                  chunk.offset + chunk.slice.length()) {
         OVERNET_TRACE(DEBUG) << "Split and integrate separately";
+        stats_->linearizer_integration_subsequent_splits++;
         // Split chunk into two and integrate each separately
         Chunk tail = chunk;
         chunk.TrimEnd(chunk.offset + chunk.slice.length() - after->first);
@@ -336,6 +352,7 @@
         return;
       } else {
         // Trim so the new chunk no longer overlaps.
+        stats_->linearizer_integration_subsequent_covers++;
         chunk.TrimEnd(chunk.offset + chunk.slice.length() - after->first);
       }
     }
@@ -344,6 +361,7 @@
   // We now have a non-overlapping chunk that we can insert.
   OVERNET_TRACE(DEBUG) << "add pending start=" << chunk.offset
                        << " end=" << (chunk.offset + chunk.slice.length());
+  stats_->linearizer_integration_inserts++;
   pending_push_.emplace_hint(lb, chunk.offset, std::move(chunk.slice));
 }
 
diff --git a/src/connectivity/overnet/lib/datagram_stream/linearizer.h b/src/connectivity/overnet/lib/datagram_stream/linearizer.h
index b79cdfe..8d30cd3 100644
--- a/src/connectivity/overnet/lib/datagram_stream/linearizer.h
+++ b/src/connectivity/overnet/lib/datagram_stream/linearizer.h
@@ -5,7 +5,9 @@
 #pragma once
 
 #include <map>
+
 #include "src/connectivity/overnet/lib/environment/trace.h"
+#include "src/connectivity/overnet/lib/stats/stream.h"
 #include "src/connectivity/overnet/lib/vocabulary/callback.h"
 #include "src/connectivity/overnet/lib/vocabulary/optional.h"
 #include "src/connectivity/overnet/lib/vocabulary/slice.h"
@@ -15,7 +17,7 @@
 
 class Linearizer final {
  public:
-  explicit Linearizer(uint64_t max_buffer);
+  explicit Linearizer(uint64_t max_buffer, StreamStats* stats);
   ~Linearizer();
 
   // Input interface.
@@ -24,6 +26,10 @@
   // Returns true if successful, false on failure.
   [[nodiscard]] bool Push(Chunk chunk);
 
+  void UpdateMaxBuffer(uint64_t new_max_buffer) {
+    max_buffer_ = std::max(max_buffer_, new_max_buffer);
+  }
+
   // Output interface.
   void Pull(StatusOrCallback<Optional<Slice>> ready);
   void PullAll(StatusOrCallback<Optional<std::vector<Slice>>> ready);
@@ -82,7 +88,7 @@
   };
 #endif
 
-  const uint64_t max_buffer_;
+  uint64_t max_buffer_;
   uint64_t offset_ = 0;
   Optional<uint64_t> length_;
   std::map<uint64_t, Slice> pending_push_;
@@ -128,6 +134,7 @@
 
   ReadMode read_mode_ = ReadMode::Idle;
   ReadData read_data_;
+  StreamStats* const stats_;
 
   void IdleToClosed(const Status& status);
   void IdleToReadSlice(StatusOrCallback<Optional<Slice>> done);
diff --git a/src/connectivity/overnet/lib/datagram_stream/linearizer_fuzzer.h b/src/connectivity/overnet/lib/datagram_stream/linearizer_fuzzer.h
index 6bc38f4a..a1d1190 100644
--- a/src/connectivity/overnet/lib/datagram_stream/linearizer_fuzzer.h
+++ b/src/connectivity/overnet/lib/datagram_stream/linearizer_fuzzer.h
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include <vector>
+
 #include "src/connectivity/overnet/lib/datagram_stream/linearizer.h"
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
 #include "src/connectivity/overnet/lib/testing/test_timer.h"
@@ -62,7 +63,8 @@
   StatusCode closed_status_;
   bool waiting_for_pull_ = false;
 
-  Linearizer linearizer_{kBuffer};
+  StreamStats stats_;
+  Linearizer linearizer_{kBuffer, &stats_};
 };
 
 }  // namespace linearizer_fuzzer
diff --git a/src/connectivity/overnet/lib/datagram_stream/linearizer_test.cc b/src/connectivity/overnet/lib/datagram_stream/linearizer_test.cc
index a32ea0a..3cc7395 100644
--- a/src/connectivity/overnet/lib/datagram_stream/linearizer_test.cc
+++ b/src/connectivity/overnet/lib/datagram_stream/linearizer_test.cc
@@ -3,6 +3,7 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/datagram_stream/linearizer.h"
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "src/connectivity/overnet/lib/datagram_stream/linearizer_fuzzer.h"
@@ -20,6 +21,26 @@
 template <class T>
 void Ignore(T x) {}
 
+class StatsDumper final : public StatsVisitor {
+ public:
+  void Counter(const char* name, uint64_t value) override {
+    std::cout << name << " = " << value << "\n";
+
+    // Maintainer note:
+    // uncomment this line to get easily copy/pasted EXPECT macros to update the
+    // expectations in tests.
+
+    // std::cout << "EXPECT_EQ(stats." << name << ", uint64_t(" << value
+    //           << "));\n";
+  }
+};
+
+template <class T>
+void DumpStats(const T* stats) {
+  StatsDumper dumper;
+  stats->Accept(&dumper);
+}
+
 class MockCallbacks {
  public:
   MOCK_METHOD1(PullDone, void(const StatusOr<Optional<Slice>>&));
@@ -32,11 +53,15 @@
   }
 };
 
-TEST(Linearizer, NoOp) { Linearizer(1024); }
+TEST(Linearizer, NoOp) {
+  StreamStats stats;
+  Linearizer(1024, &stats);
+}
 
 TEST(Linearizer, Push0_Pull0) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   // Push at offset 0 then a Pull should complete immediately
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
@@ -44,11 +69,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("a"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Pull0_Push0) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   // Push at offset 0 then a Pull should complete immediately
   linearizer.Pull(cb.NewPull());
@@ -56,11 +105,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("a"))))));
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push0_Push1_Pull0_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
@@ -75,11 +148,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("b"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push1_Push0_Pull0_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
@@ -94,11 +191,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("b"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push0_Pull0_Push1_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
 
@@ -113,11 +234,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("b"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(2));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push1_Pull0_Push0_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
   linearizer.Pull(cb.NewPull());
@@ -132,11 +277,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("b"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Pull0_Push1_Push0_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   linearizer.Pull(cb.NewPull());
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
@@ -151,11 +320,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("b"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push0_Push0_Pull0) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
 
@@ -166,11 +359,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("a"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push0_PushBad0_Pull0) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("b")}));
@@ -178,11 +395,35 @@
 
   EXPECT_CALL(cb, PullDone(Property(&StatusOr<Optional<Slice>>::is_ok, false)));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push1_Push01_Pull0_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("ab")}));
@@ -197,11 +438,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("b"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(1));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push01_Push1_Pull0_Pull1) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("ab")}));
 
@@ -212,11 +477,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("ab"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push12_Push01_Pull0_Pull1_Pull2) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("bc")}));
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("ab")}));
@@ -231,11 +520,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("bc"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(1));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push01_Push12_Pull0_Pull1_Pull2) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("ab")}));
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("bc")}));
@@ -250,21 +563,69 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("c"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push_Close) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("a")}));
 
   // Final status will be an error because the message is incomplete.
   EXPECT_TRUE(linearizer.Close(Status::Ok()).is_error());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push1_Push012_Pull0_Pull1_Pull2) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("abc")}));
@@ -285,11 +646,35 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("c"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(2));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 TEST(Linearizer, Push012_Push1_Pull0) {
   StrictMock<MockCallbacks> cb;
-  Linearizer linearizer(128);
+  StreamStats stats;
+  Linearizer linearizer(128, &stats);
 
   Ignore(linearizer.Push(Chunk{0, false, Slice::FromStaticString("abc")}));
   Ignore(linearizer.Push(Chunk{1, false, Slice::FromStaticString("b")}));
@@ -299,6 +684,29 @@
       cb, PullDone(Property(&StatusOr<Optional<Slice>>::get,
                             Pointee(Pointee(Slice::FromStaticString("abc"))))));
   linearizer.Pull(cb.NewPull());
+
+  DumpStats(&stats);
+
+  EXPECT_EQ(stats.linearizer_reject_past_end_of_buffering, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_empty_chunk, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_fast_path_taken, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_ignore_all_prior, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_partial_ignore_begin, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_new_pending_queue, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integrations, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_inserts, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_errors, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_shorter, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_coincident_longer, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_prior_longer, uint64_t(1));
+  EXPECT_EQ(stats.linearizer_integration_prior_partial, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_splits, uint64_t(0));
+  EXPECT_EQ(stats.linearizer_integration_subsequent_covers, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_cancel_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_split_packet_too_small, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_take_entire_chunk, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_nacked, uint64_t(0));
+  EXPECT_EQ(stats.send_chunk_push, uint64_t(0));
 }
 
 ///////////////////////////////////////////////////////////////////////////////
diff --git a/src/connectivity/overnet/lib/embedded/BUILD.gn b/src/connectivity/overnet/lib/embedded/BUILD.gn
index 5dff6aa..ac3e952 100644
--- a/src/connectivity/overnet/lib/embedded/BUILD.gn
+++ b/src/connectivity/overnet/lib/embedded/BUILD.gn
@@ -2,6 +2,25 @@
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+###############################################################################
+# AGGREGATE LIBRARIES
+
+source_set("lib") {
+}
+
+source_set("tests") {
+  testonly = true
+  deps = []
+
+  if (!is_fuchsia) {
+    deps += [
+      ":stream_socket_link_test",
+    ]
+  }
+}
+
+###############################################################################
+
 source_set("host_reactor") {
   sources = [
     "host_reactor.cc",
@@ -55,10 +74,10 @@
   ]
 }
 
-source_set("embedded") {
+source_set("basic_embedded") {
   sources = [
-    "overnet_embedded.cc",
-    "overnet_embedded.h",
+    "basic_overnet_embedded.cc",
+    "basic_overnet_embedded.h",
   ]
   public_deps = [
     ":host_reactor",
@@ -68,13 +87,24 @@
   ]
 }
 
+source_set("embedded") {
+  sources = [
+    "overnet_embedded.h",
+  ]
+  public_deps = [
+    ":basic_embedded",
+    ":stream_client",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+  ]
+}
+
 source_set("omdp_nub") {
   sources = [
     "omdp_nub.cc",
     "omdp_nub.h",
   ]
   public_deps = [
-    ":embedded",
+    ":basic_embedded",
     ":udp_nub",
     "//src/connectivity/overnet/lib/omdp",
     "//src/connectivity/overnet/lib/vocabulary:ip_addr",
@@ -89,9 +119,71 @@
     "udp_nub.h",
   ]
   public_deps = [
-    ":embedded",
+    ":basic_embedded",
     "//src/connectivity/overnet/lib/links:packet_nub",
     "//src/connectivity/overnet/lib/vocabulary:ip_addr",
     "//src/connectivity/overnet/lib/vocabulary:socket",
   ]
 }
+
+source_set("stream_socket_link") {
+  sources = [
+    "stream_socket_link.cc",
+    "stream_socket_link.h",
+  ]
+  deps = [
+    ":basic_embedded",
+    "//src/connectivity/overnet/lib/links:stream_link",
+    "//src/connectivity/overnet/lib/labels:node_id",
+    "//src/connectivity/overnet/lib/protocol:fidl",
+    "//src/connectivity/overnet/lib/protocol:stream_framer",
+    "//src/connectivity/overnet/lib/protocol:varint",
+    "//src/connectivity/overnet/lib/vocabulary:socket",
+  ]
+}
+
+source_set("stream_socket_link_test") {
+  testonly = true
+  sources = [
+    "stream_socket_link_test.cc",
+  ]
+  deps = [
+    "//third_party/googletest:gtest",
+    ":stream_socket_link",
+    "//src/connectivity/overnet/lib/environment:trace",
+    "//src/connectivity/overnet/lib/embedded:basic_embedded",
+    "//src/connectivity/overnet/lib/protocol:fidl",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+    "//src/connectivity/overnet/lib/protocol:unreliable_framer",
+    "//src/connectivity/overnet/lib/vocabulary:socket",
+    "//src/connectivity/overnet/lib/testing:flags",
+  ]
+}
+
+source_set("stream_server") {
+  sources = [
+    "stream_server.cc",
+    "stream_server.h",
+  ]
+  deps = [
+    ":basic_embedded",
+    ":stream_socket_link",
+    "//src/connectivity/overnet/lib/vocabulary:ip_addr",
+    "//src/connectivity/overnet/lib/vocabulary:socket",
+    "//src/connectivity/overnet/lib/protocol:stream_framer",
+  ]
+}
+
+source_set("stream_client") {
+  sources = [
+    "stream_client.cc",
+    "stream_client.h",
+  ]
+  deps = [
+    ":basic_embedded",
+    ":stream_socket_link",
+    "//src/connectivity/overnet/lib/vocabulary:ip_addr",
+    "//src/connectivity/overnet/lib/vocabulary:socket",
+    "//src/connectivity/overnet/lib/protocol:stream_framer",
+  ]
+}
diff --git a/src/connectivity/overnet/lib/embedded/basic_overnet_embedded.cc b/src/connectivity/overnet/lib/embedded/basic_overnet_embedded.cc
new file mode 100644
index 0000000..6b6a36c
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/basic_overnet_embedded.cc
@@ -0,0 +1,124 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+
+#include <random>
+
+namespace overnet {
+
+BasicOvernetEmbedded::BasicOvernetEmbedded(bool allow_non_determinism)
+    : endpoint_{&reactor_, GenerateNodeId(), allow_non_determinism},
+      shutdown_([this] {
+        bool done = false;
+        endpoint_.Close([&done] { done = true; });
+        auto last_print = timer()->Now();
+        while (!done) {
+          if (timer()->Now() - last_print > TimeDelta::FromSeconds(1)) {
+            OVERNET_TRACE(INFO) << "Waiting to exit";
+            last_print = timer()->Now();
+          }
+          reactor_.Step();
+        }
+      }) {}
+
+BasicOvernetEmbedded::~BasicOvernetEmbedded() = default;
+
+NodeId BasicOvernetEmbedded::GenerateNodeId() {
+  std::random_device rng_dev;
+  std::uniform_int_distribution<uint64_t> distrib;
+  return NodeId(distrib(rng_dev));
+}
+
+void BasicOvernetEmbedded::ListPeers(uint64_t last_seen_version,
+                                     ListPeersCallback callback) {
+  endpoint_.OnNodeDescriptionTableChange(
+      last_seen_version,
+      StatusCallback(ALLOCATED_CALLBACK, [this, callback = std::move(callback)](
+                                             const Status& status) {
+        if (status.is_error()) {
+          return;
+        }
+        std::vector<fuchsia::overnet::embedded::Peer> response;
+        auto new_version = endpoint_.ForEachNodeDescription(
+            [&response, self_node = endpoint_.node_id()](
+                overnet::NodeId id,
+                const fuchsia::overnet::protocol::PeerDescription& m) {
+              fuchsia::overnet::embedded::Peer peer;
+              peer.id = fidl::ToEmbedded(id.as_fidl());
+              peer.is_self = id == self_node;
+              peer.description = fidl::ToEmbedded(m);
+              response.emplace_back(std::move(peer));
+            });
+        callback(new_version, std::move(response));
+      }));
+}
+
+void BasicOvernetEmbedded::RegisterService(
+    std::string service_name,
+    std::unique_ptr<fuchsia::overnet::embedded::ServiceProvider_Proxy>
+        service_provider) {
+  services_.emplace(std::move(service_name), std::move(service_provider));
+}
+
+void BasicOvernetEmbedded::ConnectToService(
+    fuchsia::overnet::protocol::embedded::NodeId node, std::string service_name,
+    ClosedPtr<ZxChannel> channel) {
+  if (node == fidl::ToEmbedded(endpoint_.node_id().as_fidl())) {
+    auto it = services_.find(service_name);
+    if (it != services_.end()) {
+      it->second->ConnectToService(std::move(channel));
+    } else {
+      OVERNET_TRACE(ERROR) << "Service not found: " << service_name;
+    }
+    return;
+  }
+  auto new_stream = endpoint_.InitiateStream(
+      NodeId(node.id),
+      fuchsia::overnet::protocol::ReliabilityAndOrdering::ReliableOrdered,
+      service_name);
+  if (new_stream.is_error()) {
+    OVERNET_TRACE(ERROR) << "Failed to create stream to initiate service: "
+                         << service_name;
+    return;
+  }
+  channel->Bind(std::move(*new_stream));
+}
+
+BasicOvernetEmbedded::Actor::Actor(BasicOvernetEmbedded* root) : root_(root) {
+  assert(root->actors_);
+  root->actors_->push_back(this);
+}
+
+BasicOvernetEmbedded::Actor::~Actor() {
+  if (root_->actors_) {
+    root_->actors_->erase(
+        std::remove(root_->actors_->begin(), root_->actors_->end(), this));
+  }
+}
+
+int BasicOvernetEmbedded::Run() {
+  auto shutdown = std::move(shutdown_);
+  ZX_ASSERT(!shutdown.empty());
+
+  {
+    assert(actors_);
+    auto actors = std::move(actors_);
+    for (Actor* actor : *actors) {
+      if (auto status = actor->Start(); status.is_error()) {
+        OVERNET_TRACE(ERROR)
+            << "Failed to start actor '" << actor->Name() << "': " << status;
+        return 1;
+      }
+    }
+  }
+
+  if (auto status = reactor_.Run(); status.is_error()) {
+    OVERNET_TRACE(ERROR) << "Run loop failed: " << status;
+    return 1;
+  }
+  return 0;
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h b/src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h
new file mode 100644
index 0000000..adf5bcf
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h
@@ -0,0 +1,81 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <fuchsia/overnet/cpp/overnet_embedded.h>
+
+#include "src/connectivity/overnet/lib/embedded/host_reactor.h"
+#include "src/connectivity/overnet/lib/endpoint/router_endpoint.h"
+#include "src/connectivity/overnet/lib/environment/trace_cout.h"
+
+namespace overnet {
+
+class BasicOvernetEmbedded : public fuchsia::overnet::embedded::Overnet {
+ public:
+  class Actor {
+   public:
+    Actor(BasicOvernetEmbedded* root);
+    virtual ~Actor();
+    virtual const char* Name() const = 0;
+    virtual Status Start() = 0;
+
+   protected:
+    BasicOvernetEmbedded* root() const { return root_; }
+
+   private:
+    BasicOvernetEmbedded* const root_;
+  };
+
+  BasicOvernetEmbedded(bool allow_non_determinism = true);
+  ~BasicOvernetEmbedded();
+
+  void ListPeers(uint64_t last_seen_version,
+                 ListPeersCallback callback) override;
+  void RegisterService(
+      std::string service_name,
+      std::unique_ptr<fuchsia::overnet::embedded::ServiceProvider_Proxy>
+          service_provider) override;
+  void ConnectToService(fuchsia::overnet::protocol::embedded::NodeId node,
+                        std::string service_name,
+                        ClosedPtr<ZxChannel> channel) override;
+
+  int Run();
+
+  void Exit(const Status& status) { reactor_.Exit(status); }
+
+  Timer* timer() { return &reactor_; }
+  NodeId node_id() { return endpoint_.node_id(); }
+  RouterEndpoint* endpoint() { return &endpoint_; }
+  HostReactor* reactor() { return &reactor_; }
+
+ private:
+  static NodeId GenerateNodeId();
+  void MaybeShutdown();
+
+  TraceCout initial_log_{nullptr};
+  ScopedRenderer initial_renderer_{&initial_log_};
+  HostReactor reactor_;
+  TraceCout running_log_{&reactor_};
+  ScopedRenderer running_renderer_{&running_log_};
+  RouterEndpoint endpoint_{&reactor_, GenerateNodeId(), true};
+  std::map<std::string,
+           std::unique_ptr<fuchsia::overnet::embedded::ServiceProvider_Proxy>>
+      services_;
+  std::unique_ptr<std::vector<Actor*>> actors_ =
+      std::make_unique<std::vector<Actor*>>();
+  Callback<void> shutdown_;
+};
+
+template <class Service>
+std::unique_ptr<typename Service::Proxy_> ConnectToService(
+    fuchsia::overnet::embedded::Overnet* overnet,
+    const fuchsia::overnet::protocol::embedded::NodeId& peer) {
+  auto [client, server] = ZxChannel::MakePair();
+  overnet->ConnectToService(fidl::Clone(peer), Service::Name_,
+                            std::move(server));
+  return std::make_unique<typename Service::Proxy_>(std::move(client));
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/host_reactor.cc b/src/connectivity/overnet/lib/embedded/host_reactor.cc
index d0aed7d..36a6c49 100644
--- a/src/connectivity/overnet/lib/embedded/host_reactor.cc
+++ b/src/connectivity/overnet/lib/embedded/host_reactor.cc
@@ -19,6 +19,15 @@
   for (auto tmr : pending_timeouts_) {
     FireTimeout(tmr.second, overnet::Status::Cancelled());
   }
+  auto fds = std::move(fds_);
+  fds.clear();
+}
+
+void HostReactor::CancelIO(int fd) {
+  if (auto it = fds_.find(fd); it != fds_.end()) {
+    auto st = std::move(it->second);
+    fds_.erase(it);
+  }
 }
 
 void HostReactor::InitTimeout(overnet::Timeout* timeout,
@@ -124,11 +133,22 @@
         next = &next_store;
         next->tv_sec = dt.as_us() / 1000000;
         next->tv_nsec = 1000 * (dt.as_us() % 1000000);
+      } else {
+        next = &next_store;
+        next->tv_sec = 0;
+        next->tv_nsec = 0;
       }
     }
 
-    // Actually poll.
+// Actually poll.
+#if __linux__
+    // Use ppoll for higher timing resolution
     int r = ppoll(pollfds.data(), pollfds.size(), next, nullptr);
+#else
+    // Use poll for compatibility
+    int r = poll(pollfds.data(), pollfds.size(),
+                 next ? next->tv_sec * 1000 + next->tv_nsec / 1000000 : -1);
+#endif
     if (r < 0) {
       const int e = errno;
       if (e == EINTR) {
diff --git a/src/connectivity/overnet/lib/embedded/host_reactor.h b/src/connectivity/overnet/lib/embedded/host_reactor.h
index 0de8d96..79c2faf 100644
--- a/src/connectivity/overnet/lib/embedded/host_reactor.h
+++ b/src/connectivity/overnet/lib/embedded/host_reactor.h
@@ -5,9 +5,11 @@
 #pragma once
 
 #include <time.h>
+
 #include <map>
 #include <mutex>
 #include <unordered_map>
+
 #include "src/connectivity/overnet/lib/environment/timer.h"
 
 namespace overnet {
@@ -47,11 +49,40 @@
   Status Run();
   void Step();
 
-  void OnRead(int fd, StatusCallback cb) { fds_[fd].on_read = std::move(cb); }
-  void OnWrite(int fd, StatusCallback cb) { fds_[fd].on_read = std::move(cb); }
+  void OnRead(int fd, StatusCallback cb) {
+    if (!shutting_down_) {
+      fds_[fd].on_read = std::move(cb);
+    }
+  }
+  void OnWrite(int fd, StatusCallback cb) {
+    if (!shutting_down_) {
+      fds_[fd].on_write = std::move(cb);
+    }
+  }
+  void CancelIO(int fd);
 
   void Exit(const Status& status) { exit_status_ = status; }
 
+  // Wait until either succeeds() returns true, or false if until is reached.
+  // Return true if succeeds(), otherwise false.
+  template <class F>
+  bool WaitUntil(F succeeds, TimeStamp until) {
+    if (succeeds()) {
+      return true;
+    }
+    bool timeout_hit = false;
+    Timeout timeout(this, until, [&timeout_hit](const Status& status) {
+      timeout_hit = true;
+    });
+    while (!timeout_hit) {
+      Step();
+      if (succeeds()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
  private:
   void InitTimeout(Timeout* timeout, TimeStamp when) override;
   void CancelTimeout(Timeout* timeout, Status status) override;
diff --git a/src/connectivity/overnet/lib/embedded/omdp_nub.cc b/src/connectivity/overnet/lib/embedded/omdp_nub.cc
index 6dbf59c..06d8984 100644
--- a/src/connectivity/overnet/lib/embedded/omdp_nub.cc
+++ b/src/connectivity/overnet/lib/embedded/omdp_nub.cc
@@ -16,18 +16,18 @@
 std::random_device rng_dev;
 static const size_t kMaximumPacketSize = 1400;
 
-OmdpNub::OmdpNub(OvernetEmbedded* root, UdpNub* udp_nub)
-    : OvernetEmbedded::Actor(root),
+OmdpNub::OmdpNub(BasicOvernetEmbedded* root, UdpNub* udp_nub)
+    : BasicOvernetEmbedded::Actor(root),
       Omdp(root->node_id().get(), root->timer(), []() { return rng_dev(); }),
       reactor_(root->reactor()),
       udp_nub_(udp_nub),
-      timer_(root->timer()),
-      incoming_(socket(AF_INET6, SOCK_DGRAM, 0)) {}
+      timer_(root->timer()) {}
 
 OmdpNub::~OmdpNub() = default;
 
 Status OmdpNub::Start() {
-  return incoming_.SetOptReusePort(true)
+  return incoming_.Create(AF_INET6, SOCK_DGRAM, 0)
+      .Then([&] { return incoming_.SetOptReusePort(true); })
       .Then([&] {
         auto addr = *IpAddr::AnyIpv6().WithPort(kMulticastGroupAddr.port());
         OVERNET_TRACE(DEBUG) << "Bind incoming OMDP socket to: " << addr;
@@ -53,6 +53,8 @@
     return;
   }
   OVERNET_TRACE(DEBUG) << "BROADCAST " << data << " to " << kMulticastGroupAddr;
+  std::vector<Status> failures;
+  bool any_successes = false;
   for (size_t i = 0;
        interfaces[i].if_index != 0 || interfaces[i].if_name != nullptr; i++) {
     if (auto status =
@@ -65,7 +67,14 @@
                   out << "On " << interfaces[i].if_name;
                   return out.str();
                 });
-        status.is_error()) {
+        status.is_ok()) {
+      any_successes = true;
+    } else {
+      failures.emplace_back(std::move(status));
+    }
+  }
+  if (!any_successes) {
+    for (const auto& status : failures) {
       OVERNET_TRACE(WARNING) << "Omdp broadcast failed: " << status;
     }
   }
@@ -73,7 +82,7 @@
 }
 
 void OmdpNub::OnNewNode(uint64_t node_id, IpAddr addr) {
-  udp_nub_->Initiate(addr, NodeId(node_id));
+  udp_nub_->Initiate({addr}, NodeId(node_id));
 }
 
 void OmdpNub::WaitForInbound() {
diff --git a/src/connectivity/overnet/lib/embedded/omdp_nub.h b/src/connectivity/overnet/lib/embedded/omdp_nub.h
index 85299ac..a9ea5ff 100644
--- a/src/connectivity/overnet/lib/embedded/omdp_nub.h
+++ b/src/connectivity/overnet/lib/embedded/omdp_nub.h
@@ -5,16 +5,18 @@
 #pragma once
 
 #include <fbl/ref_ptr.h>
+
 #include "src/connectivity/overnet/lib/embedded/udp_nub.h"
 #include "src/connectivity/overnet/lib/omdp/omdp.h"
 
 namespace overnet {
 
-class OmdpNub final : public OvernetEmbedded::Actor, private Omdp {
+class OmdpNub final : public BasicOvernetEmbedded::Actor, private Omdp {
  public:
-  OmdpNub(OvernetEmbedded* root, UdpNub* udp_nub);
+  OmdpNub(BasicOvernetEmbedded* root, UdpNub* udp_nub);
   ~OmdpNub();
   Status Start() override;
+  const char* Name() const override { return "OmdpNub"; }
 
  private:
   void OnNewNode(uint64_t node_id, IpAddr addr) override;
diff --git a/src/connectivity/overnet/lib/embedded/overnet_embedded.cc b/src/connectivity/overnet/lib/embedded/overnet_embedded.cc
deleted file mode 100644
index 3ec6edf..0000000
--- a/src/connectivity/overnet/lib/embedded/overnet_embedded.cc
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/embedded/overnet_embedded.h"
-#include <random>
-
-namespace overnet {
-
-OvernetEmbedded::OvernetEmbedded()
-    : shutdown_([this] {
-        bool done = false;
-        endpoint_.Close([&done] { done = true; });
-        while (!done) {
-          OVERNET_TRACE(INFO) << "Waiting to exit";
-          reactor_.Step();
-        }
-      }) {}
-
-OvernetEmbedded::~OvernetEmbedded() = default;
-
-NodeId OvernetEmbedded::GenerateNodeId() {
-  std::random_device rng_dev;
-  std::uniform_int_distribution<uint64_t> distrib;
-  return NodeId(distrib(rng_dev));
-}
-
-void OvernetEmbedded::ListPeers(uint64_t last_seen_version,
-                                ListPeersCallback callback) {
-  endpoint_.OnNodeDescriptionTableChange(
-      last_seen_version,
-      Callback<void>(
-          ALLOCATED_CALLBACK, [this, callback = std::move(callback)] {
-            std::vector<fuchsia::overnet::embedded::Peer> response;
-            auto new_version = endpoint_.ForEachNodeDescription(
-                [&response, self_node = endpoint_.node_id()](
-                    overnet::NodeId id,
-                    const fuchsia::overnet::protocol::PeerDescription& m) {
-                  fuchsia::overnet::embedded::Peer peer;
-                  peer.id = fidl::ToEmbedded(id.as_fidl());
-                  peer.is_self = id == self_node;
-                  peer.description = fidl::ToEmbedded(m);
-                  response.emplace_back(std::move(peer));
-                });
-            callback(new_version, std::move(response));
-          }));
-}
-
-void OvernetEmbedded::RegisterService(
-    std::string service_name,
-    std::unique_ptr<fuchsia::overnet::embedded::ServiceProvider_Proxy>
-        service_provider) {
-  services_.emplace(std::move(service_name), std::move(service_provider));
-}
-
-void OvernetEmbedded::ConnectToService(
-    fuchsia::overnet::protocol::embedded::NodeId node, std::string service_name,
-    ClosedPtr<ZxChannel> channel) {
-  if (node == fidl::ToEmbedded(endpoint_.node_id().as_fidl())) {
-    auto it = services_.find(service_name);
-    if (it != services_.end()) {
-      it->second->ConnectToService(std::move(channel));
-    } else {
-      OVERNET_TRACE(ERROR) << "Service not found: " << service_name;
-    }
-    return;
-  }
-  auto new_stream = endpoint_.InitiateStream(
-      NodeId(node.id),
-      fuchsia::overnet::protocol::ReliabilityAndOrdering::ReliableOrdered,
-      service_name);
-  if (new_stream.is_error()) {
-    OVERNET_TRACE(ERROR) << "Failed to create stream to initiate service: "
-                         << service_name;
-    return;
-  }
-  channel->Bind(std::move(*new_stream));
-}
-
-OvernetEmbedded::Actor::Actor(OvernetEmbedded* root) : root_(root) {
-  assert(root->actors_);
-  root->actors_->push_back(this);
-}
-
-OvernetEmbedded::Actor::~Actor() {
-  if (root_->actors_) {
-    root_->actors_->erase(
-        std::remove(root_->actors_->begin(), root_->actors_->end(), this));
-  }
-}
-
-int OvernetEmbedded::Run() {
-  auto shutdown = std::move(shutdown_);
-  ZX_ASSERT(!shutdown.empty());
-
-  {
-    assert(actors_);
-    auto actors = std::move(actors_);
-    for (Actor* actor : *actors) {
-      if (auto status = actor->Start(); status.is_error()) {
-        OVERNET_TRACE(ERROR) << "Failed to start actor: " << status;
-        return 1;
-      }
-    }
-  }
-
-  if (auto status = reactor_.Run(); status.is_error()) {
-    OVERNET_TRACE(ERROR) << "Run loop failed: " << status;
-    return 1;
-  }
-  return 0;
-}
-
-}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/overnet_embedded.h b/src/connectivity/overnet/lib/embedded/overnet_embedded.h
index 3063405..a82e725 100644
--- a/src/connectivity/overnet/lib/embedded/overnet_embedded.h
+++ b/src/connectivity/overnet/lib/embedded/overnet_embedded.h
@@ -4,76 +4,20 @@
 
 #pragma once
 
-#include <fuchsia/overnet/cpp/overnet_embedded.h>
-#include "src/connectivity/overnet/lib/embedded/host_reactor.h"
-#include "src/connectivity/overnet/lib/endpoint/router_endpoint.h"
-#include "src/connectivity/overnet/lib/environment/trace_cout.h"
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+#include "src/connectivity/overnet/lib/embedded/stream_client.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
 
 namespace overnet {
 
-class OvernetEmbedded final : public fuchsia::overnet::embedded::Overnet {
+class OvernetEmbedded : public BasicOvernetEmbedded {
  public:
-  class Actor {
-   public:
-    Actor(OvernetEmbedded* root);
-    virtual ~Actor();
-    virtual Status Start() = 0;
-
-   protected:
-    OvernetEmbedded* root() const { return root_; }
-
-   private:
-    OvernetEmbedded* const root_;
-  };
-
-  OvernetEmbedded();
-  ~OvernetEmbedded();
-
-  void ListPeers(uint64_t last_seen_version,
-                 ListPeersCallback callback) override;
-  void RegisterService(
-      std::string service_name,
-      std::unique_ptr<fuchsia::overnet::embedded::ServiceProvider_Proxy>
-          service_provider) override;
-  void ConnectToService(fuchsia::overnet::protocol::embedded::NodeId node,
-                        std::string service_name,
-                        ClosedPtr<ZxChannel> channel) override;
-
-  int Run();
-
-  void Exit(const Status& status) { reactor_.Exit(status); }
-
-  Timer* timer() { return &reactor_; }
-  NodeId node_id() { return endpoint_.node_id(); }
-  RouterEndpoint* endpoint() { return &endpoint_; }
-  HostReactor* reactor() { return &reactor_; }
+  OvernetEmbedded(
+      IpAddr ascendd_addr = *overnet::IpAddr::Unix("/tmp/ascendd.socket"))
+      : stream_client_{this, ascendd_addr} {}
 
  private:
-  static NodeId GenerateNodeId();
-  void MaybeShutdown();
-
-  TraceCout initial_log_{nullptr};
-  ScopedRenderer initial_renderer_{&initial_log_};
-  HostReactor reactor_;
-  TraceCout running_log_{&reactor_};
-  ScopedRenderer running_renderer_{&running_log_};
-  RouterEndpoint endpoint_{&reactor_, GenerateNodeId(), true};
-  std::map<std::string,
-           std::unique_ptr<fuchsia::overnet::embedded::ServiceProvider_Proxy>>
-      services_;
-  std::unique_ptr<std::vector<Actor*>> actors_ =
-      std::make_unique<std::vector<Actor*>>();
-  Callback<void> shutdown_;
+  overnet::StreamClient<overnet::ReliableFramer> stream_client_;
 };
 
-template <class Service>
-std::unique_ptr<typename Service::Proxy_> ConnectToService(
-    fuchsia::overnet::embedded::Overnet* overnet,
-    const fuchsia::overnet::protocol::embedded::NodeId& peer) {
-  auto [client, server] = ZxChannel::MakePair();
-  overnet->ConnectToService(fidl::Clone(peer), Service::Name_,
-                            std::move(server));
-  return std::make_unique<typename Service::Proxy_>(std::move(client));
-}
-
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_client.cc b/src/connectivity/overnet/lib/embedded/stream_client.cc
new file mode 100644
index 0000000..3640a378
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_client.cc
@@ -0,0 +1,30 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/embedded/stream_client.h"
+
+#include "src/connectivity/overnet/lib/embedded/stream_socket_link.h"
+#include "src/connectivity/overnet/lib/vocabulary/socket.h"
+
+namespace overnet {
+
+StreamClientBase::StreamClientBase(BasicOvernetEmbedded* app, IpAddr target)
+    : BasicOvernetEmbedded::Actor(app), target_(target) {}
+
+Status StreamClientBase::Start() {
+  Socket socket;
+  return socket.Create(target_.addr.sa_family, SOCK_STREAM, 0)
+      .Then([&] { return socket.Connect(target_); })
+      .Then([&] {
+        RegisterStreamSocketLink(
+            root(), std::move(socket), CreateFramer(), true,
+            TimeDelta::PositiveInf(), [app = root()] {
+              app->Exit(Status(StatusCode::UNAVAILABLE,
+                               "Stream server disconnected"));
+            });
+        return Status::Ok();
+      });
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_client.h b/src/connectivity/overnet/lib/embedded/stream_client.h
new file mode 100644
index 0000000..6fb2e03
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_client.h
@@ -0,0 +1,38 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+#include "src/connectivity/overnet/lib/protocol/stream_framer.h"
+#include "src/connectivity/overnet/lib/vocabulary/ip_addr.h"
+
+namespace overnet {
+
+class StreamClientBase : public BasicOvernetEmbedded::Actor {
+ public:
+  StreamClientBase(BasicOvernetEmbedded* app, IpAddr target);
+
+  const char* Name() const override final { return "StreamClient"; }
+  Status Start() override final;
+
+ private:
+  const IpAddr target_;
+
+  virtual std::unique_ptr<StreamFramer> CreateFramer() const = 0;
+};
+
+template <class T>
+class StreamClient final : public StreamClientBase {
+ public:
+  StreamClient(BasicOvernetEmbedded* app, IpAddr target)
+      : StreamClientBase(app, target) {}
+
+ private:
+  std::unique_ptr<StreamFramer> CreateFramer() const override {
+    return std::make_unique<T>();
+  }
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_server.cc b/src/connectivity/overnet/lib/embedded/stream_server.cc
new file mode 100644
index 0000000..83b849a
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_server.cc
@@ -0,0 +1,46 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/embedded/stream_server.h"
+
+#include "src/connectivity/overnet/lib/embedded/stream_socket_link.h"
+
+namespace overnet {
+
+StreamServerBase::StreamServerBase(BasicOvernetEmbedded* app, IpAddr bind)
+    : Actor(app), app_(app), bind_(bind) {}
+
+Status StreamServerBase::Start() {
+  return listener_.Create(bind_.addr.sa_family, SOCK_STREAM, 0)
+      .Then([&] { return listener_.Bind(bind_); })
+      .Then([&] { return listener_.Listen(); })
+      .Then([&] {
+        AwaitRead();
+        return Status::Ok();
+      })
+      .WithLazyContext([this] {
+        std::ostringstream out;
+        out << "Building stream server " << bind_;
+        return out.str();
+      });
+}
+
+void StreamServerBase::AwaitRead() {
+  app_->reactor()->OnRead(listener_.get(), [&](const Status& status) {
+    if (status.is_error()) {
+      return;
+    }
+    auto fd = listener_.Accept();
+    if (fd.is_error()) {
+      OVERNET_TRACE(ERROR) << "Failed to accept socket: " << fd.AsStatus();
+      return;
+    }
+    RegisterStreamSocketLink(app_, std::move(*fd), CreateFramer(), true,
+                             TimeDelta::PositiveInf(),
+                             Callback<void>::Ignored());
+    AwaitRead();
+  });
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_server.h b/src/connectivity/overnet/lib/embedded/stream_server.h
new file mode 100644
index 0000000..f383906
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_server.h
@@ -0,0 +1,42 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+#include "src/connectivity/overnet/lib/protocol/stream_framer.h"
+#include "src/connectivity/overnet/lib/vocabulary/ip_addr.h"
+#include "src/connectivity/overnet/lib/vocabulary/socket.h"
+
+namespace overnet {
+
+class StreamServerBase : public BasicOvernetEmbedded::Actor {
+ public:
+  StreamServerBase(BasicOvernetEmbedded* app, IpAddr bind);
+
+  const char* Name() const override { return "StreamServer"; }
+  Status Start() override;
+
+ private:
+  void AwaitRead();
+  virtual std::unique_ptr<StreamFramer> CreateFramer() = 0;
+
+  BasicOvernetEmbedded* const app_;
+  const IpAddr bind_;
+  Socket listener_;
+};
+
+template <class T>
+class StreamServer final : public StreamServerBase {
+ public:
+  StreamServer(BasicOvernetEmbedded* app, IpAddr bind)
+      : StreamServerBase(app, bind) {}
+
+ private:
+  virtual std::unique_ptr<StreamFramer> CreateFramer() {
+    return std::make_unique<T>();
+  }
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_socket_link.cc b/src/connectivity/overnet/lib/embedded/stream_socket_link.cc
new file mode 100644
index 0000000..91992a6
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_socket_link.cc
@@ -0,0 +1,283 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/embedded/stream_socket_link.h"
+
+#include <fuchsia/overnet/protocol/cpp/fidl.h>
+
+#include "src/connectivity/overnet/lib/labels/node_id.h"
+#include "src/connectivity/overnet/lib/links/stream_link.h"
+#include "src/connectivity/overnet/lib/protocol/fidl.h"
+#include "src/connectivity/overnet/lib/vocabulary/optional.h"
+
+namespace overnet {
+
+namespace {
+struct ValidatedArgs {
+  NodeId peer;
+  uint64_t remote_link_label;
+};
+
+class Link final : public StreamLink {
+ public:
+  Link(BasicOvernetEmbedded* app, const ValidatedArgs& args,
+       uint64_t local_label, std::unique_ptr<StreamFramer> framer,
+       Socket socket, Callback<void> destroyed)
+      : StreamLink(app->endpoint(), args.peer, std::move(framer), local_label),
+        reactor_(app->reactor()),
+        socket_(std::move(socket)),
+        destroyed_(std::move(destroyed)) {
+    BeginRead();
+  }
+
+  ~Link() {
+    if (socket_.IsValid()) {
+      auto socket = std::move(socket_);
+      reactor_->CancelIO(socket.get());
+    }
+  }
+
+  void Emit(Slice slice, Callback<Status> done) override {
+    auto status = socket_.Write(slice);
+    if (status.is_error()) {
+      done(status.AsStatus());
+      return;
+    }
+    if (status->length() > 0) {
+      reactor_->OnWrite(
+          socket_.get(),
+          StatusCallback(ALLOCATED_CALLBACK,
+                         [this, slice = std::move(slice),
+                          done = std::move(done)](Status status) mutable {
+                           if (status.is_error()) {
+                             done(std::move(status));
+                             return;
+                           }
+                           Emit(std::move(slice), std::move(done));
+                         }));
+      return;
+    }
+    done(Status::Ok());
+  }
+
+ private:
+  void BeginRead() {
+    reactor_->OnRead(socket_.get(), [this](const Status& status) {
+      if (status.is_error()) {
+        return;
+      }
+      TimeStamp now = reactor_->Now();
+      // Read some data. Choose a read size of maximum_segment_size + epsilon to
+      // try and pull in full segments at a time.
+      auto read = socket_.Read(maximum_segment_size() + 64);
+      if (read.is_ok() && read->has_value()) {
+        Process(now, std::move(**read));
+        BeginRead();
+      } else {
+        OVERNET_TRACE(ERROR) << read;
+      }
+    });
+  }
+
+  HostReactor* const reactor_;
+  Socket socket_;
+  Callback<void> destroyed_;
+};
+
+class Handshake {
+ public:
+  static inline const std::string kGreetingString = "Fuchsia Socket Stream";
+
+  Handshake(BasicOvernetEmbedded* app, Socket socket,
+            std::unique_ptr<StreamFramer> framer, bool eager_announce,
+            TimeDelta read_timeout, Callback<void> destroyed)
+      : app_(app),
+        socket_(std::move(socket)),
+        framer_(std::move(framer)),
+        link_label_(app->endpoint()->GenerateLinkLabel()),
+        destroyed_(std::move(destroyed)),
+        eager_announce_(eager_announce),
+        read_timeout_(read_timeout) {
+    if (eager_announce) {
+      SendGreeting();
+    }
+    AwaitGreeting();
+  }
+
+ private:
+  void DoneWriting(Status status) {
+    OVERNET_TRACE(DEBUG) << "Finished writing stream handshake: " << status;
+    if (status.is_error() && socket_.IsValid()) {
+      app_->reactor()->CancelIO(socket_.get());
+      socket_.Close();
+    }
+    Done();
+  }
+
+  void DoneReading(Status status) {
+    OVERNET_TRACE(DEBUG) << "Finished reading stream handshake: " << status;
+    if (status.is_error() && socket_.IsValid()) {
+      app_->reactor()->CancelIO(socket_.get());
+      socket_.Close();
+    } else if (!eager_announce_) {
+      SendGreeting();
+    }
+    Done();
+  }
+
+  void Done() {
+    if (--dones_pending_ == 0) {
+      if (socket_.IsValid()) {
+        app_->reactor()->CancelIO(socket_.get());
+        app_->endpoint()->RegisterPeer(validated_args_->peer);
+        app_->endpoint()->RegisterLink(MakeLink<Link>(
+            app_, *validated_args_, link_label_, std::move(framer_),
+            std::move(socket_), std::move(destroyed_)));
+      }
+
+      delete this;
+    }
+  }
+
+  void SendGreeting() {
+    fuchsia::overnet::protocol::StreamSocketGreeting send_greeting;
+    send_greeting.set_magic_string(kGreetingString);
+    send_greeting.set_node_id(app_->node_id().as_fidl());
+    send_greeting.set_local_link_id(link_label_);
+    auto bytes = Encode(&send_greeting);
+    if (bytes.is_error()) {
+      DoneWriting(bytes.AsStatus());
+      return;
+    }
+
+    SendBytes(framer_->Frame(std::move(*bytes)));
+  }
+
+  void SendBytes(Slice bytes) {
+    app_->reactor()->OnWrite(
+        socket_.get(),
+        StatusCallback(ALLOCATED_CALLBACK, [this, bytes = std::move(bytes)](
+                                               const Status& status) mutable {
+          if (status.is_error()) {
+            DoneWriting(status);
+            return;
+          }
+
+          auto write_status = socket_.Write(bytes);
+          if (write_status.is_error()) {
+            DoneWriting(write_status.AsStatus());
+            return;
+          }
+          if (write_status->length() > 0) {
+            SendBytes(std::move(*write_status));
+            return;
+          }
+          DoneWriting(Status::Ok());
+        }));
+  }
+
+  void AwaitGreeting() {
+    app_->reactor()->OnRead(socket_.get(), [this](const Status& status) {
+      if (status.is_error()) {
+        DoneReading(status);
+        return;
+      }
+
+      auto read = socket_.Read(2 * framer_->maximum_segment_size);
+      if (read.is_error()) {
+        DoneReading(read.AsStatus());
+        return;
+      }
+      if (!read->has_value()) {
+        DoneReading(Status(StatusCode::UNKNOWN, "End of file handshaking"));
+        return;
+      }
+
+      framer_->Push(std::move(**read));
+
+      if (!ContinueReading()) {
+        AwaitGreeting();
+      }
+    });
+  }
+
+  // Returns true if reading is done.
+  bool ContinueReading() {
+    auto frame = framer_->Pop();
+    if (frame.is_error()) {
+      DoneReading(frame.AsStatus().WithContext("Handshaking stream link"));
+      return true;
+    }
+    if (!frame->has_value()) {
+      if (read_timeout_ != TimeDelta::PositiveInf()) {
+        skip_timeout_.Reset(app_->timer(), app_->timer()->Now() + read_timeout_,
+                            [this](const Status& status) {
+                              if (status.is_error()) {
+                                return;
+                              }
+                              auto noise = framer_->SkipNoise();
+                              OVERNET_TRACE(DEBUG)
+                                  << "Skip input noise: " << noise;
+                              ContinueReading();
+                            });
+      }
+      return false;
+    }
+
+    auto decoded = Decode<fuchsia::overnet::protocol::StreamSocketGreeting>(
+        std::move(**frame));
+    if (decoded.is_error()) {
+      DoneReading(decoded.AsStatus());
+      return true;
+    }
+
+    if (!decoded->has_magic_string()) {
+      DoneReading(Status(StatusCode::INVALID_ARGUMENT, "No magic string"));
+      return true;
+    }
+    if (decoded->magic_string() != kGreetingString) {
+      DoneReading(Status(StatusCode::INVALID_ARGUMENT, "Bad magic string"));
+      return true;
+    }
+    if (!decoded->has_node_id()) {
+      DoneReading(Status(StatusCode::INVALID_ARGUMENT, "No node id"));
+      return true;
+    }
+    if (!decoded->has_local_link_id()) {
+      DoneReading(Status(StatusCode::INVALID_ARGUMENT, "No local link id"));
+      return true;
+    }
+    validated_args_.Reset(
+        ValidatedArgs{decoded->node_id(), decoded->local_link_id()});
+    DoneReading(Status::Ok());
+    return true;
+  }
+
+  BasicOvernetEmbedded* const app_;
+  Socket socket_;
+  std::unique_ptr<StreamFramer> framer_;
+  const uint64_t link_label_;
+  int dones_pending_ = 2;
+  Optional<ValidatedArgs> validated_args_;
+  Callback<void> destroyed_;
+  const bool eager_announce_;
+  const TimeDelta read_timeout_;
+  Optional<Timeout> skip_timeout_;
+};
+}  // namespace
+
+void RegisterStreamSocketLink(BasicOvernetEmbedded* app, Socket socket,
+                              std::unique_ptr<StreamFramer> framer,
+                              bool eager_announce, TimeDelta read_timeout,
+                              Callback<void> destroyed) {
+  if (auto status = socket.SetNonBlocking(true); status.is_error()) {
+    OVERNET_TRACE(WARNING)
+        << "Failed to set non-blocking for stream link socket: " << status;
+    return;
+  }
+  new Handshake(app, std::move(socket), std::move(framer), eager_announce,
+                read_timeout, std::move(destroyed));
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_socket_link.h b/src/connectivity/overnet/lib/embedded/stream_socket_link.h
new file mode 100644
index 0000000..5797909
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_socket_link.h
@@ -0,0 +1,19 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+#include "src/connectivity/overnet/lib/links/stream_link.h"
+#include "src/connectivity/overnet/lib/protocol/stream_framer.h"
+#include "src/connectivity/overnet/lib/vocabulary/socket.h"
+
+namespace overnet {
+
+void RegisterStreamSocketLink(BasicOvernetEmbedded* app, Socket socket,
+                              std::unique_ptr<StreamFramer> framer,
+                              bool eager_announce, TimeDelta read_timeout,
+                              Callback<void> destroyed);
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/stream_socket_link_test.cc b/src/connectivity/overnet/lib/embedded/stream_socket_link_test.cc
new file mode 100644
index 0000000..43d0e0c
--- /dev/null
+++ b/src/connectivity/overnet/lib/embedded/stream_socket_link_test.cc
@@ -0,0 +1,207 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/embedded/stream_socket_link.h"
+
+#include <gtest/gtest.h>
+#include <unistd.h>
+
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+#include "src/connectivity/overnet/lib/protocol/fidl.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+#include "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
+#include "src/connectivity/overnet/lib/testing/flags.h"
+#include "src/connectivity/overnet/lib/vocabulary/socket.h"
+
+namespace overnet {
+namespace stream_socket_link_test {
+
+template <class F, bool E, bool RT>
+struct TestTraits {
+  using Framer = F;
+  static constexpr bool kEagerAnnounce = E;
+  static constexpr TimeDelta kReadTimeout =
+      RT ? TimeDelta::FromMilliseconds(100) : TimeDelta::PositiveInf();
+};
+
+using ReliableEager = TestTraits<ReliableFramer, true, false>;
+using ReliableUneager = TestTraits<ReliableFramer, false, false>;
+using UnreliableEager = TestTraits<UnreliableFramer, true, true>;
+using UnreliableUneager = TestTraits<UnreliableFramer, false, true>;
+
+template <class Traits>
+class TypedTest : public ::testing::Test {
+ public:
+  void SetUp() override {
+    int sv[2];
+    ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, sv));
+    mocked_socket_ = Socket(sv[1]);
+    ASSERT_TRUE(mocked_socket_.SetNonBlocking(true).is_ok());
+    RegisterStreamSocketLink(&app_, Socket(sv[0]),
+                             std::make_unique<typename Traits::Framer>(),
+                             Traits::kEagerAnnounce, Traits::kReadTimeout,
+                             [this] { destroyed_ = true; });
+  }
+
+  void TearDown() override {
+    app_.Exit(Status::Ok());
+    app_.Run();
+  }
+
+  void Write(Slice stuff) {
+    auto write_result = mocked_socket_.Write(std::move(stuff));
+    ASSERT_TRUE(write_result.is_ok()) << write_result;
+    if (write_result->length()) {
+      bool done = false;
+      app_.reactor()->OnWrite(
+          mocked_socket_.get(),
+          StatusCallback(ALLOCATED_CALLBACK,
+                         [this, &done, remaining = std::move(*write_result)](
+                             const Status& status) mutable {
+                           ASSERT_TRUE(status.is_ok()) << status;
+                           done = true;
+                           Write(std::move(remaining));
+                         }));
+      StepUntil(&done);
+    }
+  }
+
+  StatusOr<Optional<Slice>> PollFrame() { return mocked_side_framer_.Pop(); }
+
+  StatusOr<Slice> ReadFrame() {
+    auto poll_result = PollFrame();
+    if (poll_result.is_error()) {
+      return poll_result.AsStatus();
+    }
+    if (poll_result->has_value()) {
+      return std::move(**poll_result);
+    }
+    bool done = false;
+    app_.reactor()->OnRead(
+        mocked_socket_.get(),
+        StatusCallback(ALLOCATED_CALLBACK, [this, &done](const Status& status) {
+          ASSERT_TRUE(status.is_ok()) << status;
+          done = true;
+          auto input = mocked_socket_.Read(4096);
+          ASSERT_TRUE(input.is_ok()) << input;
+          if (input->has_value()) {
+            mocked_side_framer_.Push(std::move(**input));
+          }
+        }));
+    StepUntil(&done);
+    return ReadFrame();
+  }
+
+  template <class F>
+  StatusOr<F> ReadAndDecode() {
+    return ReadFrame().Then(
+        [](Slice slice) { return Decode<F>(std::move(slice)); });
+  }
+
+  Slice Frame(Slice stuff) { return mocked_side_framer_.Frame(stuff); }
+
+  bool RouterHasRouteTo(NodeId node_id) {
+    return app_.endpoint()->HasRouteTo(node_id);
+  }
+
+  template <class F>
+  bool WaitFor(F succeeds, TimeDelta timeout) {
+    return app_.reactor()->WaitUntil(std::move(succeeds),
+                                     app_.timer()->Now() + timeout);
+  }
+
+  void StepUntil(bool* done) {
+    Timeout timeout(app_.timer(),
+                    app_.timer()->Now() + TimeDelta::FromSeconds(5),
+                    [](const Status& status) {
+                      if (status.is_ok()) {
+                        abort();
+                      }
+                    });
+    while (!*done) {
+      app_.reactor()->Step();
+    }
+  }
+
+ private:
+  BasicOvernetEmbedded app_{false};
+  ScopedSeverity scoped_severity{FLAGS_verbose ? Severity::DEBUG
+                                               : Severity::INFO};
+  typename Traits::Framer mocked_side_framer_;
+  Socket mocked_socket_;
+  bool destroyed_ = false;
+};
+
+TYPED_TEST_SUITE_P(TypedTest);
+
+static Slice EncodedGreeting() {
+  fuchsia::overnet::protocol::StreamSocketGreeting greeting;
+  greeting.set_magic_string("Fuchsia Socket Stream");
+  greeting.set_node_id(NodeId(0x1111'2222'3333'4444).as_fidl());
+  greeting.set_local_link_id(1);
+  return *Encode(&greeting);
+}
+
+TYPED_TEST_P(TypedTest, Connects) {
+  this->Write(this->Frame(EncodedGreeting()));
+
+  auto got_greeting = this->template ReadAndDecode<
+      fuchsia::overnet::protocol::StreamSocketGreeting>();
+  ASSERT_TRUE(got_greeting.is_ok()) << got_greeting;
+  ASSERT_TRUE(got_greeting->has_magic_string());
+  EXPECT_EQ(got_greeting->magic_string(), "Fuchsia Socket Stream");
+  EXPECT_TRUE(got_greeting->has_node_id());
+  EXPECT_TRUE(got_greeting->has_local_link_id());
+
+  EXPECT_TRUE(this->WaitFor(
+      [this] { return this->RouterHasRouteTo(NodeId(0x1111'2222'3333'4444)); },
+      TimeDelta::FromMilliseconds(200)));
+}
+
+REGISTER_TYPED_TEST_SUITE_P(TypedTest, Connects);
+using FramerTypes = ::testing::Types<ReliableEager, ReliableUneager,
+                                     UnreliableEager, UnreliableUneager>;
+INSTANTIATE_TYPED_TEST_SUITE_P(StreamSocketLinkSuite, TypedTest, FramerTypes);
+
+struct UnreliableUneagerTest : public TypedTest<UnreliableUneager>,
+                               public ::testing::WithParamInterface<Slice> {};
+
+TEST_P(UnreliableUneagerTest, Connects) {
+  OVERNET_TRACE(INFO) << GetParam();
+  this->Write(GetParam());
+
+  auto got_greeting = this->template ReadAndDecode<
+      fuchsia::overnet::protocol::StreamSocketGreeting>();
+  ASSERT_TRUE(got_greeting.is_ok()) << got_greeting;
+  ASSERT_TRUE(got_greeting->has_magic_string());
+  EXPECT_EQ(got_greeting->magic_string(), "Fuchsia Socket Stream");
+  EXPECT_TRUE(got_greeting->has_node_id());
+  EXPECT_TRUE(got_greeting->has_local_link_id());
+
+  EXPECT_TRUE(this->WaitFor(
+      [this] { return this->RouterHasRouteTo(NodeId(0x1111'2222'3333'4444)); },
+      TimeDelta::FromMilliseconds(200)));
+}
+
+const Slice kUnreliableStreamGreetingFrame =
+    UnreliableFramer().Frame(EncodedGreeting());
+
+INSTANTIATE_TEST_SUITE_P(
+    UnreliableUneagerSuite, UnreliableUneagerTest,
+    ::testing::Values(
+        kUnreliableStreamGreetingFrame,
+        Slice::Join(
+            {kUnreliableStreamGreetingFrame,
+             Slice::FromStaticString("\nsome random trailing bytes\n")}),
+        Slice::Join(
+            {kUnreliableStreamGreetingFrame,
+             Slice::FromStaticString(
+                 "some random trailing bytes without the extra newlines")}),
+        Slice::Join({Slice::FromStaticString("not a frame"),
+                     kUnreliableStreamGreetingFrame}),
+        Slice::Join({Slice::FromStaticString("\n\xff"),
+                     kUnreliableStreamGreetingFrame})));
+
+}  // namespace stream_socket_link_test
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/embedded/udp_nub.cc b/src/connectivity/overnet/lib/embedded/udp_nub.cc
index 19f32e0..b26b464 100644
--- a/src/connectivity/overnet/lib/embedded/udp_nub.cc
+++ b/src/connectivity/overnet/lib/embedded/udp_nub.cc
@@ -3,14 +3,15 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/embedded/udp_nub.h"
+
 #include <sys/socket.h>
 #include <unistd.h>
 
 namespace overnet {
 
-UdpNub::UdpNub(OvernetEmbedded* root)
-    : OvernetEmbedded::Actor(root),
-      UdpNubBase(root->timer(), root->node_id()),
+UdpNub::UdpNub(BasicOvernetEmbedded* root)
+    : BasicOvernetEmbedded::Actor(root),
+      UdpNubBase(root->endpoint()),
       endpoint_(root->endpoint()),
       timer_(root->timer()),
       reactor_(root->reactor()) {}
@@ -67,13 +68,7 @@
   WaitForInbound();
 }
 
-Status UdpNub::CreateFD() {
-  socket_ = Socket(::socket(AF_INET6, SOCK_DGRAM, 0));
-  if (!socket_.IsValid()) {
-    return StatusFromErrno("Failed to create socket");
-  }
-  return Status::Ok();
-}
+Status UdpNub::CreateFD() { return socket_.Create(AF_INET6, SOCK_DGRAM, 0); }
 
 Status UdpNub::SetOptionSharePort() { return socket_.SetOptReusePort(true); }
 
diff --git a/src/connectivity/overnet/lib/embedded/udp_nub.h b/src/connectivity/overnet/lib/embedded/udp_nub.h
index e128c28..ca3f3f8 100644
--- a/src/connectivity/overnet/lib/embedded/udp_nub.h
+++ b/src/connectivity/overnet/lib/embedded/udp_nub.h
@@ -4,7 +4,7 @@
 
 #pragma once
 
-#include "src/connectivity/overnet/lib/embedded/overnet_embedded.h"
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
 #include "src/connectivity/overnet/lib/links/packet_nub.h"
 #include "src/connectivity/overnet/lib/vocabulary/ip_addr.h"
 #include "src/connectivity/overnet/lib/vocabulary/socket.h"
@@ -13,18 +13,17 @@
 
 using UdpNubBase = PacketNub<IpAddr, 1500, HashIpAddr, EqIpAddr>;
 
-class UdpNub final : public OvernetEmbedded::Actor, public UdpNubBase {
+class UdpNub final : public BasicOvernetEmbedded::Actor, public UdpNubBase {
  public:
-  explicit UdpNub(OvernetEmbedded* master);
+  explicit UdpNub(BasicOvernetEmbedded* app);
   ~UdpNub();
 
+  const char* Name() const override { return "UdpNub"; }
   Status Start() override;
   uint16_t port() { return port_; }
   NodeId node_id() { return endpoint_->node_id(); }
   void SendTo(IpAddr addr, Slice slice) override;
 
-  Router* GetRouter() override { return endpoint_; }
-
   void Publish(overnet::LinkPtr<> link) override {
     NodeId node = NodeId(link->GetLinkStatus().to);
     OVERNET_TRACE(DEBUG) << "NewLink: " << node << "\n";
diff --git a/src/connectivity/overnet/lib/endpoint/BUILD.gn b/src/connectivity/overnet/lib/endpoint/BUILD.gn
index 1c6e692..d269217 100644
--- a/src/connectivity/overnet/lib/endpoint/BUILD.gn
+++ b/src/connectivity/overnet/lib/endpoint/BUILD.gn
@@ -50,7 +50,10 @@
     ":router_endpoint",
     "//src/connectivity/overnet/lib/environment:trace_cout",
     "//src/connectivity/overnet/lib/links:packet_link",
+    "//src/connectivity/overnet/lib/links:stream_link",
     "//src/connectivity/overnet/lib/protocol:fidl",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+    "//src/connectivity/overnet/lib/protocol:unreliable_framer",
     "//src/connectivity/overnet/lib/testing:flags",
     "//src/connectivity/overnet/lib/testing:test_timer",
     "//third_party/googletest:gtest",
diff --git a/src/connectivity/overnet/lib/endpoint/router_endpoint.cc b/src/connectivity/overnet/lib/endpoint/router_endpoint.cc
index 0748f52..f39271b 100644
--- a/src/connectivity/overnet/lib/endpoint/router_endpoint.cc
+++ b/src/connectivity/overnet/lib/endpoint/router_endpoint.cc
@@ -3,8 +3,10 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/endpoint/router_endpoint.h"
+
 #include <iostream>
 #include <memory>
+
 #include "garnet/public/lib/fostr/fidl/fuchsia/overnet/protocol/formatting.h"
 #include "src/connectivity/overnet/lib/protocol/coding.h"
 #include "src/connectivity/overnet/lib/protocol/fidl.h"
@@ -43,15 +45,19 @@
 }
 
 void RouterEndpoint::SendGossipTo(NodeId target) {
-  OVERNET_TRACE(DEBUG) << node_id() << " send gossip to " << target;
   // Are we still gossiping?
   if (!gossip_timer_.get()) {
+    OVERNET_TRACE(DEBUG) << node_id() << " send gossip to " << target
+                         << " [skipped: STOPPED GOSSIPING]";
     return;
   }
   auto con = connection_streams_.find(target);
   if (con == connection_streams_.end()) {
+    OVERNET_TRACE(DEBUG) << node_id() << " send gossip to " << target
+                         << " [skipped: NO CONNECTION]";
     return;
   }
+  OVERNET_TRACE(DEBUG) << node_id() << " send gossip to " << target;
   SendGossipUpdate(con->second.proxy(), target);
 }
 
@@ -406,7 +412,7 @@
 }
 
 void RouterEndpoint::OnNodeDescriptionTableChange(uint64_t last_seen_version,
-                                                  Callback<void> on_change) {
+                                                  StatusCallback on_change) {
   if (last_seen_version == node_description_table_version_) {
     on_node_description_table_change_.emplace_back(std::move(on_change));
   }
@@ -416,7 +422,9 @@
 
 void RouterEndpoint::NewNodeDescriptionTableVersion() {
   ++node_description_table_version_;
-  std::vector<Callback<void>>().swap(on_node_description_table_change_);
+  for (auto& cb : std::move(on_node_description_table_change_)) {
+    cb(Status::Ok());
+  }
 }
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/endpoint/router_endpoint.h b/src/connectivity/overnet/lib/endpoint/router_endpoint.h
index 33ac895..e8a0535 100644
--- a/src/connectivity/overnet/lib/endpoint/router_endpoint.h
+++ b/src/connectivity/overnet/lib/endpoint/router_endpoint.h
@@ -5,7 +5,9 @@
 #pragma once
 
 #include <fuchsia/overnet/protocol/cpp/fidl.h>
+
 #include <queue>
+
 #include "garnet/public/lib/fostr/fidl/fuchsia/overnet/protocol/formatting.h"
 #include "src/connectivity/overnet/lib/datagram_stream/datagram_stream.h"
 #include "src/connectivity/overnet/lib/routing/router.h"
@@ -142,7 +144,7 @@
   }
 
   void OnNodeDescriptionTableChange(uint64_t last_seen_version,
-                                    Callback<void> on_change);
+                                    StatusCallback on_change);
 
   template <class F>
   uint64_t ForEachNodeDescription(F f) {
@@ -254,7 +256,7 @@
 
   std::unordered_map<NodeId, ConnectionStream> connection_streams_;
   uint64_t node_description_table_version_ = 1;
-  std::vector<Callback<void>> on_node_description_table_change_;
+  std::vector<StatusCallback> on_node_description_table_change_;
   Optional<Timeout> gossip_timer_;
   Optional<Timeout> description_timer_;
   TimeDelta gossip_interval_ = InitialGossipInterval();
diff --git a/src/connectivity/overnet/lib/endpoint/router_endpoint_integration_test.cc b/src/connectivity/overnet/lib/endpoint/router_endpoint_integration_test.cc
index 076bc2d..463a817 100644
--- a/src/connectivity/overnet/lib/endpoint/router_endpoint_integration_test.cc
+++ b/src/connectivity/overnet/lib/endpoint/router_endpoint_integration_test.cc
@@ -4,11 +4,15 @@
 
 #include <array>
 #include <tuple>
+
 #include "gtest/gtest.h"
 #include "src/connectivity/overnet/lib/endpoint/router_endpoint.h"
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
 #include "src/connectivity/overnet/lib/links/packet_link.h"
+#include "src/connectivity/overnet/lib/links/stream_link.h"
 #include "src/connectivity/overnet/lib/protocol/fidl.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+#include "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
 #include "src/connectivity/overnet/lib/testing/flags.h"
 #include "src/connectivity/overnet/lib/testing/test_timer.h"
 
@@ -23,40 +27,60 @@
   int outstanding_packets;
 };
 
-class DeliverySimulator {
+class Simulator {
  public:
-  virtual ~DeliverySimulator() {}
+  virtual ~Simulator() = default;
+  virtual void MakeLinks(RouterEndpoint* a, RouterEndpoint* b, uint64_t id1,
+                         uint64_t id2) const = 0;
+};
+
+struct NamedSimulator {
+  std::string name;
+  std::unique_ptr<Simulator> simulator;
+};
+
+class PacketPacer {
+ public:
+  virtual ~PacketPacer() {}
   // Returns Nothing if the slice should be dropped, or a delay if it should be
   // delivered.
   virtual Optional<TimeDelta> ChoosePacketDelivery(LinkState link_state,
                                                    size_t slice_size) const = 0;
-
-  virtual std::string name() = 0;
-
+  virtual std::string name() const = 0;
   virtual Bandwidth SimulatedBandwidth() const = 0;
+  virtual uint32_t MaximumSegmentSize() const = 0;
 };
 
 // Very fast reliable packet delivery: it's often easier to debug problems
 // with HappyDelivery than with packet loss enabled (assuming it shows up).
-class HappyDelivery : public DeliverySimulator {
+class HappyDelivery : public PacketPacer {
  public:
+  HappyDelivery(uint32_t mss) : mss_(mss) {}
+
   Optional<TimeDelta> ChoosePacketDelivery(LinkState link_state,
                                            size_t slice_size) const override {
     return TimeDelta::FromMicroseconds(1);
   }
 
-  std::string name() override { return "HappyDelivery"; }
+  std::string name() const override {
+    return "HappyDelivery(" + std::to_string(mss_) + ")";
+  }
 
   Bandwidth SimulatedBandwidth() const override {
     return Bandwidth::FromKilobitsPerSecond(1024 * 1024);
   }
+
+  virtual uint32_t MaximumSegmentSize() const override { return mss_; }
+
+ private:
+  const uint32_t mss_;
 };
 
 // Windowed number of outstanding packets
-class WindowedDelivery : public DeliverySimulator {
+class WindowedDelivery : public PacketPacer {
  public:
-  WindowedDelivery(int max_outstanding, TimeDelta window)
-      : max_outstanding_(max_outstanding), window_(window) {}
+  WindowedDelivery(int max_outstanding, TimeDelta window, uint32_t mss)
+      : max_outstanding_(max_outstanding), window_(window), mss_(mss) {}
 
   Optional<TimeDelta> ChoosePacketDelivery(LinkState link_state,
                                            size_t slice_size) const override {
@@ -65,29 +89,35 @@
     return window_;
   }
 
-  std::string name() override {
+  std::string name() const override {
     std::ostringstream out;
-    out << "WindowedDelivery{max:" << max_outstanding_ << " over " << window_
-        << "}";
+    out << "WindowedDelivery(" << max_outstanding_
+        << ", TimeDelta::FromMicroseconds(" << window_.as_us() << "), " << mss_
+        << ")";
     return out.str();
   }
 
   Bandwidth SimulatedBandwidth() const override {
-    return Bandwidth::BytesPerTime(256 * max_outstanding_, window_);
+    return Bandwidth::BytesPerTime(MaximumSegmentSize() * max_outstanding_,
+                                   window_);
   }
 
+  virtual uint32_t MaximumSegmentSize() const override { return mss_; }
+
  private:
   const int max_outstanding_;
   const TimeDelta window_;
+  const uint32_t mss_;
 };
 
-class InProcessLinkImpl final
+class InProcessPacketLinkImpl final
     : public PacketLink,
-      public std::enable_shared_from_this<InProcessLinkImpl> {
+      public std::enable_shared_from_this<InProcessPacketLinkImpl> {
  public:
-  InProcessLinkImpl(RouterEndpoint* src, RouterEndpoint* dest, uint64_t link_id,
-                    const DeliverySimulator* simulator)
-      : PacketLink(src, dest->node_id(), 256, link_id),
+  InProcessPacketLinkImpl(RouterEndpoint* src, RouterEndpoint* dest,
+                          uint64_t link_id, const PacketPacer* simulator)
+      : PacketLink(src, dest->node_id(), simulator->MaximumSegmentSize(),
+                   link_id),
         timer_(dest->timer()),
         link_id_(link_id),
         simulator_(simulator),
@@ -95,14 +125,14 @@
     src->RegisterPeer(dest->node_id());
   }
 
-  ~InProcessLinkImpl() {
+  ~InProcessPacketLinkImpl() {
     auto strong_partner = partner_.lock();
     if (strong_partner != nullptr) {
       strong_partner->partner_.reset();
     }
   }
 
-  void Partner(std::shared_ptr<InProcessLinkImpl> other) {
+  void Partner(std::shared_ptr<InProcessPacketLinkImpl> other) {
     partner_ = other;
     other->partner_ = shared_from_this();
   }
@@ -141,19 +171,95 @@
  private:
   Timer* const timer_;
   const uint64_t link_id_;
-  const DeliverySimulator* const simulator_;
-  std::weak_ptr<InProcessLinkImpl> partner_;
+  const PacketPacer* const simulator_;
+  std::weak_ptr<InProcessPacketLinkImpl> partner_;
   const NodeId from_;
   int outstanding_packets_ = 0;
 };
 
+template <class Framer>
+class InProcessStreamLinkImpl final
+    : public StreamLink,
+      public std::enable_shared_from_this<InProcessStreamLinkImpl<Framer>> {
+ public:
+  InProcessStreamLinkImpl(RouterEndpoint* src, RouterEndpoint* dest,
+                          uint64_t link_id, Bandwidth bandwidth)
+      : StreamLink(src, dest->node_id(), std::make_unique<Framer>(), link_id),
+        timer_(src->timer()),
+        bandwidth_(bandwidth),
+        from_(src->node_id()) {
+    src->RegisterPeer(dest->node_id());
+  }
+
+  void Partner(std::shared_ptr<InProcessStreamLinkImpl> other) {
+    partner_ = other;
+    other->partner_ = this->shared_from_this();
+  }
+
+  void Emit(Slice slice, StatusCallback done) override {
+    OVERNET_TRACE(DEBUG) << "Emit: " << slice;
+
+    assert(!send_op_.has_value());
+    send_op_.Reset(SendOp{std::move(slice), std::move(done)});
+
+    SendNext();
+  }
+
+ private:
+  void SendNext() {
+    auto max_delivery =
+        std::max(uint64_t(1),
+                 bandwidth_.BytesSentForTime(TimeDelta::FromMicroseconds(100)));
+    Slice chunk;
+    StatusCallback cb;
+    if (max_delivery >= send_op_->slice.length()) {
+      chunk = std::move(send_op_->slice);
+      cb = std::move(send_op_->done);
+      send_op_.Reset();
+    } else {
+      chunk = send_op_->slice.TakeUntilOffset(max_delivery);
+    }
+
+    auto now = timer_->Now();
+    next_send_completes_ = std::max(now, next_send_completes_) +
+                           bandwidth_.SendTimeForBytes(chunk.length());
+    timer_->At(next_send_completes_, [chunk = std::move(chunk),
+                                      at = next_send_completes_,
+                                      self = this->shared_from_this()] {
+      auto strong_partner = self->partner_.lock();
+      OVERNET_TRACE(DEBUG) << (strong_partner == nullptr ? "DROP" : "EMIT")
+                           << " BYTES from " << self->from_ << " " << chunk;
+      if (strong_partner) {
+        strong_partner->Process(at, chunk);
+      }
+    });
+
+    if (!cb.empty()) {
+      cb(Status::Ok());
+    } else {
+      SendNext();
+    }
+  }
+
+  Timer* const timer_;
+  std::weak_ptr<InProcessStreamLinkImpl> partner_;
+  const Bandwidth bandwidth_;
+  const NodeId from_;
+  struct SendOp {
+    Slice slice;
+    Callback<Status> done;
+  };
+  Optional<SendOp> send_op_;
+  TimeStamp next_send_completes_ = TimeStamp::Epoch();
+};
+
+template <class Impl>
 class InProcessLink final : public Link {
  public:
-  InProcessLink(RouterEndpoint* src, RouterEndpoint* dest, uint64_t link_id,
-                const DeliverySimulator* simulator)
-      : impl_(new InProcessLinkImpl(src, dest, link_id, simulator)) {}
+  template <class... Arg>
+  InProcessLink(Arg&&... args) : impl_(new Impl(std::forward<Arg>(args)...)) {}
 
-  std::shared_ptr<InProcessLinkImpl> get() { return impl_; }
+  std::shared_ptr<Impl> get() { return impl_; }
 
   void Close(Callback<void> quiesced) override {
     impl_->Close(Callback<void>(
@@ -167,37 +273,64 @@
   const LinkStats* GetStats() const override { return impl_->GetStats(); }
 
  private:
-  std::shared_ptr<InProcessLinkImpl> impl_;
+  std::shared_ptr<Impl> impl_;
 };
 
 class StatsDumper final : public StatsVisitor {
  public:
+  StatsDumper(const char* indent) : indent_(indent) {}
   void Counter(const char* name, uint64_t value) override {
-    OVERNET_TRACE(INFO) << "    " << name << " = " << value;
+    OVERNET_TRACE(INFO) << indent_ << name << " = " << value;
   }
+
+ private:
+  const char* const indent_;
 };
 
+template <class T>
+void DumpStats(const char* indent, const T* stats) {
+  StatsDumper dumper(indent);
+  stats->Accept(&dumper);
+}
+
 void DumpStats(const char* label, RouterEndpoint* endpoint) {
   OVERNET_TRACE(INFO) << "STATS DUMP FOR: '" << label << "' -- "
                       << endpoint->node_id();
   endpoint->ForEachLink([endpoint](NodeId target, const Link* link) {
     OVERNET_TRACE(INFO) << "  LINK: " << endpoint->node_id() << "->" << target;
-    StatsDumper dumper;
-    link->GetStats()->Accept(&dumper);
+    DumpStats("    ", link->GetStats());
   });
 }
 
+LinkStats AccumulateLinkStats(RouterEndpoint* endpoint) {
+  LinkStats out;
+  endpoint->ForEachLink([&out](NodeId target, const Link* link) {
+    out.Merge(*link->GetStats());
+  });
+  return out;
+}
+
 class Env {
  public:
   virtual ~Env() {}
 
-  virtual TimeDelta AllowedTime(uint64_t length) const = 0;
-
   virtual RouterEndpoint* endpoint1() = 0;
   virtual RouterEndpoint* endpoint2() = 0;
 
+  virtual void DumpAllStats() = 0;
+
+  uint64_t OutgoingPacketsFromSource() {
+    return AccumulateLinkStats(endpoint1()).outgoing_packet_count;
+  }
+
+  uint64_t IncomingPacketsAtDestination() {
+    return AccumulateLinkStats(endpoint2()).incoming_packet_count;
+  }
+
   void AwaitConnected() {
-    OVERNET_TRACE(INFO) << "Test waiting for connection";
+    OVERNET_TRACE(INFO) << "Test waiting for connection between "
+                        << endpoint1()->node_id() << " and "
+                        << endpoint2()->node_id();
     while (!endpoint1()->HasRouteTo(endpoint2()->node_id()) ||
            !endpoint2()->HasRouteTo(endpoint1()->node_id())) {
       endpoint1()->BlockUntilNoBackgroundUpdatesProcessing();
@@ -237,25 +370,71 @@
                                                : Severity::INFO};
 };
 
+class PacketLinkSimulator final : public Simulator {
+ public:
+  PacketLinkSimulator(std::unique_ptr<PacketPacer> pacer)
+      : pacer_(std::move(pacer)) {}
+
+  void MakeLinks(RouterEndpoint* ep1, RouterEndpoint* ep2, uint64_t id1,
+                 uint64_t id2) const override {
+    auto link1 = MakeLink<InProcessLink<InProcessPacketLinkImpl>>(ep1, ep2, id1,
+                                                                  pacer_.get());
+    auto link2 = MakeLink<InProcessLink<InProcessPacketLinkImpl>>(ep2, ep1, id2,
+                                                                  pacer_.get());
+    link1->get()->Partner(link2->get());
+    ep1->RegisterLink(std::move(link1));
+    ep2->RegisterLink(std::move(link2));
+  }
+
+ private:
+  std::unique_ptr<PacketPacer> pacer_;
+};
+
+const char* FramerName(const ReliableFramer& framer) {
+  return "ReliableFramer";
+};
+
+const char* FramerName(const UnreliableFramer& framer) {
+  return "UnreliableFramer";
+};
+
+template <class Framer>
+class StreamLinkSimulator final : public Simulator {
+ public:
+  StreamLinkSimulator(Bandwidth bandwidth) : bandwidth_(bandwidth) {}
+
+  void MakeLinks(RouterEndpoint* ep1, RouterEndpoint* ep2, uint64_t id1,
+                 uint64_t id2) const override {
+    auto link1 = MakeLink<InProcessLink<InProcessStreamLinkImpl<Framer>>>(
+        ep1, ep2, id1, bandwidth_);
+    auto link2 = MakeLink<InProcessLink<InProcessStreamLinkImpl<Framer>>>(
+        ep2, ep1, id2, bandwidth_);
+    link1->get()->Partner(link2->get());
+    ep1->RegisterLink(std::move(link1));
+    ep2->RegisterLink(std::move(link2));
+  }
+
+ private:
+  const Bandwidth bandwidth_;
+};
+
 class TwoNode final : public Env {
  public:
-  TwoNode(const DeliverySimulator* simulator, uint64_t node_id_1,
+  TwoNode(const NamedSimulator* simulator, uint64_t node_id_1,
           uint64_t node_id_2)
-      : simulator_(simulator) {
+      : simulator_(simulator->simulator.get()) {
     endpoint1_ = new RouterEndpoint(timer(), NodeId(node_id_1), false);
     endpoint2_ = new RouterEndpoint(timer(), NodeId(node_id_2), false);
-    auto link1 = MakeLink<InProcessLink>(endpoint1_, endpoint2_, 1, simulator);
-    auto link2 = MakeLink<InProcessLink>(endpoint2_, endpoint1_, 2, simulator);
-    link1->get()->Partner(link2->get());
-    endpoint1_->RegisterLink(std::move(link1));
-    endpoint2_->RegisterLink(std::move(link2));
+    simulator_->MakeLinks(endpoint1_, endpoint2_, 1, 2);
+  }
+
+  void DumpAllStats() override {
+    DumpStats("1", endpoint1_);
+    DumpStats("2", endpoint2_);
   }
 
   virtual ~TwoNode() {
     if (!testing::Test::HasFailure()) {
-      DumpStats("1", endpoint1_);
-      DumpStats("2", endpoint2_);
-
       bool done = false;
       endpoint1_->Close(Callback<void>(ALLOCATED_CALLBACK, [&done, this]() {
         endpoint2_->Close(Callback<void>(ALLOCATED_CALLBACK, [&done, this]() {
@@ -269,48 +448,37 @@
     }
   }
 
-  TimeDelta AllowedTime(uint64_t data_length) const override {
-    // TODO(ctiller): make this just
-    // 'simulator_->SimulatedBandwidth().SendTimeForBytes(data_length)'
-    return TimeDelta::FromSeconds(1) +
-           simulator_->SimulatedBandwidth().SendTimeForBytes(3 * data_length);
-  }
-
   RouterEndpoint* endpoint1() override { return endpoint1_; }
   RouterEndpoint* endpoint2() override { return endpoint2_; }
 
  private:
-  const DeliverySimulator* const simulator_;
+  const Simulator* const simulator_;
   RouterEndpoint* endpoint1_;
   RouterEndpoint* endpoint2_;
 };
 
 class ThreeNode final : public Env {
  public:
-  ThreeNode(const DeliverySimulator* simulator, uint64_t node_id_1,
-            uint64_t node_id_2, uint64_t node_id_h)
-      : simulator_(simulator) {
+  ThreeNode(const NamedSimulator* simulator_1_h,
+            const NamedSimulator* simulator_h_2, uint64_t node_id_1,
+            uint64_t node_id_h, uint64_t node_id_2)
+      : simulator_1_h_(simulator_1_h->simulator.get()),
+        simulator_h_2_(simulator_h_2->simulator.get()) {
     endpoint1_ = new RouterEndpoint(timer(), NodeId(node_id_1), false);
     endpointH_ = new RouterEndpoint(timer(), NodeId(node_id_h), false);
     endpoint2_ = new RouterEndpoint(timer(), NodeId(node_id_2), false);
-    auto link1H = MakeLink<InProcessLink>(endpoint1_, endpointH_, 1, simulator);
-    auto linkH1 = MakeLink<InProcessLink>(endpointH_, endpoint1_, 2, simulator);
-    auto link2H = MakeLink<InProcessLink>(endpoint2_, endpointH_, 3, simulator);
-    auto linkH2 = MakeLink<InProcessLink>(endpointH_, endpoint2_, 4, simulator);
-    link1H->get()->Partner(linkH1->get());
-    link2H->get()->Partner(linkH2->get());
-    endpoint1_->RegisterLink(std::move(link1H));
-    endpoint2_->RegisterLink(std::move(link2H));
-    endpointH_->RegisterLink(std::move(linkH1));
-    endpointH_->RegisterLink(std::move(linkH2));
+    simulator_1_h_->MakeLinks(endpoint1_, endpointH_, 1, 2);
+    simulator_h_2_->MakeLinks(endpointH_, endpoint2_, 3, 4);
+  }
+
+  void DumpAllStats() override {
+    DumpStats("1", endpoint1_);
+    DumpStats("H", endpointH_);
+    DumpStats("2", endpoint2_);
   }
 
   virtual ~ThreeNode() {
     if (!testing::Test::HasFailure()) {
-      DumpStats("1", endpoint1_);
-      DumpStats("H", endpointH_);
-      DumpStats("2", endpoint2_);
-
       bool done = false;
       endpointH_->Close(Callback<void>(ALLOCATED_CALLBACK, [this, &done]() {
         endpoint1_->Close(Callback<void>(ALLOCATED_CALLBACK, [this, &done]() {
@@ -327,18 +495,12 @@
     }
   }
 
-  TimeDelta AllowedTime(uint64_t data_length) const override {
-    // TODO(ctiller): make this just
-    // 'simulator_->SimulatedBandwidth().SendTimeForBytes(data_length)'
-    return TimeDelta::FromSeconds(4) +
-           simulator_->SimulatedBandwidth().SendTimeForBytes(5 * data_length);
-  }
-
   RouterEndpoint* endpoint1() override { return endpoint1_; }
   RouterEndpoint* endpoint2() override { return endpoint2_; }
 
  private:
-  const DeliverySimulator* const simulator_;
+  const Simulator* const simulator_1_h_;
+  const Simulator* const simulator_h_2_;
   RouterEndpoint* endpoint1_;
   RouterEndpoint* endpointH_;
   RouterEndpoint* endpoint2_;
@@ -352,66 +514,6 @@
 
 using MakeEnv = std::shared_ptr<MakeEnvInterface>;
 
-template <class T, class... Arg>
-MakeEnv MakeMakeEnv(const char* name, Arg&&... args) {
-  class Impl final : public MakeEnvInterface {
-   public:
-    Impl(const char* name, std::tuple<Arg...> args)
-        : name_(name), args_(args) {}
-    const char* name() const { return name_.c_str(); }
-    std::shared_ptr<Env> Make() const {
-      return std::apply(
-          [](Arg... args) { return std::make_shared<T>(args...); }, args_);
-    }
-
-   private:
-    const std::string name_;
-    const std::tuple<Arg...> args_;
-  };
-  return MakeEnv(
-      new Impl(name, std::tuple<Arg...>(std::forward<Arg>(args)...)));
-}
-
-static const auto kSimulators = [] {
-  std::vector<std::unique_ptr<DeliverySimulator>> out;
-  out.emplace_back(new HappyDelivery());
-  out.emplace_back(new WindowedDelivery(3, TimeDelta::FromMilliseconds(3)));
-  return out;
-}();
-
-template <class Env, int N>
-void AddVariations(const char* base_name, std::vector<MakeEnv>* envs) {
-  for (const auto& simulator : kSimulators) {
-    std::array<int, N> ary;
-    for (int i = 0; i < N; i++) {
-      ary[i] = i + 1;
-    }
-    do {
-      std::ostringstream name;
-      name << base_name;
-      for (auto i : ary)
-        name << i;
-      name << ":";
-      name << simulator->name();
-      envs->emplace_back(std::apply(
-          [&name, simulator = simulator.get()](auto... args) {
-            return MakeMakeEnv<Env>(
-                name.str().c_str(),
-                std::forward<DeliverySimulator* const>(simulator),
-                std::forward<int>(args)...);
-          },
-          ary));
-    } while (std::next_permutation(ary.begin(), ary.end()));
-  }
-}
-
-const auto kEnvVariations = [] {
-  std::vector<MakeEnv> envs;
-  AddVariations<TwoNode, 2>("TwoNode", &envs);
-  AddVariations<ThreeNode, 3>("ThreeNode", &envs);
-  return envs;
-}();
-
 class RouterEndpoint_IntegrationEnv : public ::testing::TestWithParam<MakeEnv> {
 };
 
@@ -469,18 +571,40 @@
   EXPECT_TRUE(found);
 }
 
-INSTANTIATE_TEST_SUITE_P(RouterEndpoint_IntegrationEnv_Instance,
-                         RouterEndpoint_IntegrationEnv,
-                         ::testing::ValuesIn(kEnvVariations.begin(),
-                                             kEnvVariations.end()));
-
 struct OneMessageArgs {
   MakeEnv make_env;
   Slice body;
+  TimeDelta allowed_time;
+  uint64_t expected_packets;
 };
 
 std::ostream& operator<<(std::ostream& out, OneMessageArgs args) {
-  return out << "env=" << args.make_env->name() << " body=" << args.body;
+  return out << args.make_env->name();
+}
+
+template <class T>
+std::string ChangeArg(std::string input, int arg, T value) {
+  std::ostringstream out;
+  for (int i = 0; i < arg - 1; i++) {
+    auto cpos = input.find(',');
+    assert(cpos != std::string::npos);
+    out << input.substr(0, cpos + 1);
+    input = input.substr(cpos + 1);
+  }
+  out << ' ' << value;
+  out << input.substr(input.find(','));
+  return out.str();
+}
+
+std::string EscapeChars(std::string chars, std::string input) {
+  std::string output;
+  for (auto c : input) {
+    if (chars.find(c) != std::string::npos) {
+      output += '\\';
+    }
+    output += c;
+  }
+  return output;
 }
 
 class RouterEndpoint_OneMessageIntegration
@@ -492,8 +616,7 @@
 
   const std::string kService = "abc";
 
-  const TimeDelta kAllowedTime =
-      env->AllowedTime(kService.length() + GetParam().body.length());
+  const TimeDelta kAllowedTime = GetParam().allowed_time;
   std::cout << "Allowed time for body: " << kAllowedTime << std::endl;
 
   env->AwaitConnected();
@@ -524,6 +647,9 @@
               EXPECT_EQ(GetParam().body, pull_text) << pull_text.AsStdString();
               delete op;
               *got_pull_cb = true;
+              OVERNET_TRACE(INFO) << "STATS DUMP FOR RECEIVING DATAGRAM STREAM";
+              DumpStats("  ", stream->link_stats());
+              DumpStats("  ", stream->stream_stats());
             }));
       });
 
@@ -538,28 +664,298 @@
   RouterEndpoint::SendOp(stream.get(), GetParam().body.length())
       .Push(GetParam().body, Callback<void>::Ignored());
 
+  auto start_flush = env->timer()->Now();
+
   env->FlushTodo([got_pull_cb]() { return *got_pull_cb; },
-                 env->timer()->Now() + kAllowedTime);
+                 start_flush + kAllowedTime);
+
+  auto taken_time = env->timer()->Now() - start_flush;
+  auto taken_seconds = (taken_time.as_us() + 999999) / 1000000;
+  auto allowed_seconds = kAllowedTime.as_us() / 1000000;
+  EXPECT_EQ(taken_seconds, allowed_seconds)
+      << "sed -i 's/" << GetParam().make_env->name() << "/"
+      << EscapeChars("&",
+                     ChangeArg(GetParam().make_env->name(), 2, taken_seconds))
+      << "/g' " << __FILE__;
 
   ASSERT_TRUE(*got_pull_cb);
+
+  OVERNET_TRACE(INFO) << "STATS DUMP FOR SENDING DATAGRAM STREAM";
+  DumpStats("  ", stream->link_stats());
+  DumpStats("  ", stream->stream_stats());
+
+  env->DumpAllStats();
+
+  EXPECT_EQ(env->IncomingPacketsAtDestination(), GetParam().expected_packets)
+      << "sed -i 's/" << GetParam().make_env->name() << "/"
+      << EscapeChars("&", ChangeArg(GetParam().make_env->name(), 3,
+                                    env->IncomingPacketsAtDestination()))
+      << "/g' " << __FILE__;
 }
 
-const std::vector<OneMessageArgs> kOneMessageArgTests = [] {
-  std::vector<OneMessageArgs> out;
-  const std::vector<size_t> kLengths = {1, 32, 1024, 32768, 1048576};
-  for (MakeEnv make_env : kEnvVariations) {
-    for (auto payload_length : kLengths) {
-      out.emplace_back(
-          OneMessageArgs{make_env, Slice::RepeatedChar(payload_length, 'a')});
+template <class T, class... Arg>
+MakeEnv MakeMakeEnv(const char* name, Arg&&... args) {
+  class Impl final : public MakeEnvInterface {
+   public:
+    Impl(const char* name, std::tuple<Arg...> args)
+        : name_(name), args_(args) {}
+    const char* name() const { return name_.c_str(); }
+    std::shared_ptr<Env> Make() const {
+      return std::apply(
+          [](Arg... args) { return std::make_shared<T>(args...); }, args_);
     }
-  }
-  return out;
-}();
 
-INSTANTIATE_TEST_SUITE_P(RouterEndpoint_OneMessageIntegration_Instance,
-                         RouterEndpoint_OneMessageIntegration,
-                         ::testing::ValuesIn(kOneMessageArgTests.begin(),
-                                             kOneMessageArgTests.end()));
+   private:
+    const std::string name_;
+    const std::tuple<Arg...> args_;
+  };
+  return MakeEnv(
+      new Impl(name, std::tuple<Arg...>(std::forward<Arg>(args)...)));
+}
+
+// Simulators get abbreviations here because otherwise the environment names get
+// too difficult to communicate.
+#define DECL_SIM(name, inst) \
+  const NamedSimulator name{#name, std::unique_ptr<Simulator>(inst)};
+
+DECL_SIM(Happy, new PacketLinkSimulator(std::make_unique<HappyDelivery>(1500)));
+DECL_SIM(Win_3_3, new PacketLinkSimulator(std::make_unique<WindowedDelivery>(
+                      3, TimeDelta::FromMilliseconds(3), 256)));
+DECL_SIM(ReliableStream, new StreamLinkSimulator<ReliableFramer>(
+                             Bandwidth::FromKilobitsPerSecond(1000)));
+DECL_SIM(UnreliableStream, new StreamLinkSimulator<UnreliableFramer>(
+                               Bandwidth::FromKilobitsPerSecond(115)));
+
+#define MAKE_MAKE_ENV(env, ...) \
+  MakeMakeEnv<env>("MAKE_MAKE_ENV(" #env ", " #__VA_ARGS__ ")", __VA_ARGS__)
+
+INSTANTIATE_TEST_SUITE_P(
+    RouterEndpoint_IntegrationEnv_Instance, RouterEndpoint_IntegrationEnv,
+    ::testing::Values(
+        MAKE_MAKE_ENV(TwoNode, &Happy, 1, 2),
+        MAKE_MAKE_ENV(TwoNode, &Happy, 2, 1),
+        MAKE_MAKE_ENV(TwoNode, &Win_3_3, 1, 2),
+        MAKE_MAKE_ENV(TwoNode, &Win_3_3, 2, 1),
+        MAKE_MAKE_ENV(TwoNode, &ReliableStream, 1, 2),
+        MAKE_MAKE_ENV(TwoNode, &ReliableStream, 2, 1),
+        MAKE_MAKE_ENV(TwoNode, &UnreliableStream, 1, 2),
+        MAKE_MAKE_ENV(TwoNode, &UnreliableStream, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Happy, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Happy, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Happy, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Happy, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Happy, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Happy, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Win_3_3, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Win_3_3, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Win_3_3, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Win_3_3, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Win_3_3, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &Win_3_3, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &ReliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &ReliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &ReliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &ReliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &ReliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &ReliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &UnreliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &UnreliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &UnreliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &UnreliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &UnreliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Happy, &UnreliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Happy, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Happy, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Happy, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Happy, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Happy, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Happy, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Win_3_3, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Win_3_3, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Win_3_3, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Win_3_3, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Win_3_3, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &Win_3_3, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &ReliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &ReliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &ReliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &ReliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &ReliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &ReliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &UnreliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &UnreliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &UnreliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &UnreliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &UnreliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &Win_3_3, &UnreliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Happy, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Happy, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Happy, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Happy, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Happy, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Happy, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Win_3_3, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Win_3_3, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Win_3_3, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Win_3_3, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Win_3_3, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &Win_3_3, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &ReliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &ReliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &ReliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &ReliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &ReliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &ReliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &UnreliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &UnreliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &UnreliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &UnreliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &UnreliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &ReliableStream, &UnreliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Happy, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Happy, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Happy, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Happy, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Happy, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Happy, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Win_3_3, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Win_3_3, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Win_3_3, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Win_3_3, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Win_3_3, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &Win_3_3, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &ReliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &ReliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &ReliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &ReliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &ReliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &ReliableStream, 3, 2, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &UnreliableStream, 1, 2, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &UnreliableStream, 1, 3, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &UnreliableStream, 2, 1, 3),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &UnreliableStream, 2, 3, 1),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &UnreliableStream, 3, 1, 2),
+        MAKE_MAKE_ENV(ThreeNode, &UnreliableStream, &UnreliableStream, 3, 2,
+                      1)));
+
+#define ONE_MESSAGE_TEST(length, allowed_time, expected_packets, env, ...)   \
+  OneMessageArgs {                                                           \
+    MakeMakeEnv<env>("ONE_MESSAGE_TEST(" #length ", " #allowed_time          \
+                     ", " #expected_packets ", " #env ", " #__VA_ARGS__ ")", \
+                     __VA_ARGS__),                                           \
+        Slice::RepeatedChar(length, 'a'),                                    \
+        TimeDelta::FromSeconds(allowed_time), expected_packets               \
+  }
+
+// clang-format off
+INSTANTIATE_TEST_SUITE_P(
+    RouterEndpoint_OneMessageIntegration_Instance,
+    RouterEndpoint_OneMessageIntegration,
+    ::testing::Values(
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &Happy, 1, 2),
+        ONE_MESSAGE_TEST(32, 1, 3, TwoNode, &Happy, 1, 2),
+        ONE_MESSAGE_TEST(1024, 1, 3, TwoNode, &Happy, 1, 2),
+        ONE_MESSAGE_TEST(32768, 1, 26, TwoNode, &Happy, 1, 2),
+        ONE_MESSAGE_TEST(1048576, 1, 720, TwoNode, &Happy, 1, 2),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &Happy, 2, 1),
+        ONE_MESSAGE_TEST(1048576, 1, 720, TwoNode, &Happy, 2, 1),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &Win_3_3, 1, 2),
+        ONE_MESSAGE_TEST(32, 1, 3, TwoNode, &Win_3_3, 1, 2),
+        ONE_MESSAGE_TEST(1024, 1, 8, TwoNode, &Win_3_3, 1, 2),
+        ONE_MESSAGE_TEST(32768, 1, 155, TwoNode, &Win_3_3, 1, 2),
+        ONE_MESSAGE_TEST(1048576, 7, 4837, TwoNode, &Win_3_3, 1, 2),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &Win_3_3, 2, 1),
+        ONE_MESSAGE_TEST(1048576, 7, 4837, TwoNode, &Win_3_3, 2, 1),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &ReliableStream, 1, 2),
+        ONE_MESSAGE_TEST(32, 1, 3, TwoNode, &ReliableStream, 1, 2),
+        ONE_MESSAGE_TEST(1024, 1, 3, TwoNode, &ReliableStream, 1, 2),
+        ONE_MESSAGE_TEST(32768, 1, 26, TwoNode, &ReliableStream, 1, 2),
+        ONE_MESSAGE_TEST(1048576, 9, 534, TwoNode, &ReliableStream, 1, 2),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &ReliableStream, 2, 1),
+        ONE_MESSAGE_TEST(1048576, 9, 534, TwoNode, &ReliableStream, 2, 1),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &UnreliableStream, 1, 2),
+        ONE_MESSAGE_TEST(32, 1, 3, TwoNode, &UnreliableStream, 1, 2),
+        ONE_MESSAGE_TEST(1024, 1, 7, TwoNode, &UnreliableStream, 1, 2),
+        ONE_MESSAGE_TEST(32768, 3, 165, TwoNode, &UnreliableStream, 1, 2),
+        ONE_MESSAGE_TEST(1048576, 79, 4813, TwoNode, &UnreliableStream, 1, 2),
+        ONE_MESSAGE_TEST(1, 1, 3, TwoNode, &UnreliableStream, 2, 1),
+        ONE_MESSAGE_TEST(1048576, 79, 4813, TwoNode, &UnreliableStream, 2, 1),
+        ONE_MESSAGE_TEST(1, 1, 22, ThreeNode, &Happy, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 22, ThreeNode, &Happy, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 22, ThreeNode, &Happy, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 71, ThreeNode, &Happy, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 2, 791, ThreeNode, &Happy, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 24, ThreeNode, &Happy, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 24, ThreeNode, &Happy, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 30, ThreeNode, &Happy, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 189, ThreeNode, &Happy, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 6, 4977, ThreeNode, &Happy, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 18, ThreeNode, &Happy, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 18, ThreeNode, &Happy, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 18, ThreeNode, &Happy, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 43, ThreeNode, &Happy, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 9, 748, ThreeNode, &Happy, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 22, ThreeNode, &Happy, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 22, ThreeNode, &Happy, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 28, ThreeNode, &Happy, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 179, ThreeNode, &Happy, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 4861, ThreeNode, &Happy, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 24, ThreeNode, &Win_3_3, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 24, ThreeNode, &Win_3_3, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 28, ThreeNode, &Win_3_3, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 139, ThreeNode, &Win_3_3, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 6, 5082, ThreeNode, &Win_3_3, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 26, ThreeNode, &Win_3_3, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 26, ThreeNode, &Win_3_3, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 30, ThreeNode, &Win_3_3, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 183, ThreeNode, &Win_3_3, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 6, 4880, ThreeNode, &Win_3_3, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 18, ThreeNode, &Win_3_3, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 18, ThreeNode, &Win_3_3, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 23, ThreeNode, &Win_3_3, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 170, ThreeNode, &Win_3_3, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 10, 4805, ThreeNode, &Win_3_3, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 22, ThreeNode, &Win_3_3, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 22, ThreeNode, &Win_3_3, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 27, ThreeNode, &Win_3_3, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 178, ThreeNode, &Win_3_3, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 4859, ThreeNode, &Win_3_3, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 24, ThreeNode, &ReliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 24, ThreeNode, &ReliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 25, ThreeNode, &ReliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 67, ThreeNode, &ReliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 9, 1361, ThreeNode, &ReliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 18, ThreeNode, &ReliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 18, ThreeNode, &ReliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 18, ThreeNode, &ReliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 1, 37, ThreeNode, &ReliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 9, 545, ThreeNode, &ReliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 25, ThreeNode, &ReliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 25, ThreeNode, &ReliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 29, ThreeNode, &ReliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 179, ThreeNode, &ReliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 4862, ThreeNode, &ReliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 25, ThreeNode, &UnreliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 26, ThreeNode, &UnreliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 38, ThreeNode, &UnreliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 251, ThreeNode, &UnreliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 6454, ThreeNode, &UnreliableStream, &Happy, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 35, ThreeNode, &UnreliableStream, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 35, ThreeNode, &UnreliableStream, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 44, ThreeNode, &UnreliableStream, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 256, ThreeNode, &UnreliableStream, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 6473, ThreeNode, &UnreliableStream, &Win_3_3, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 23, ThreeNode, &UnreliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 23, ThreeNode, &UnreliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 30, ThreeNode, &UnreliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 182, ThreeNode, &UnreliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 4886, ThreeNode, &UnreliableStream, &ReliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1, 1, 28, ThreeNode, &UnreliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32, 1, 28, ThreeNode, &UnreliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1024, 1, 32, ThreeNode, &UnreliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(32768, 3, 184, ThreeNode, &UnreliableStream, &UnreliableStream, 1, 2, 3),
+        ONE_MESSAGE_TEST(1048576, 84, 4884, ThreeNode, &UnreliableStream, &UnreliableStream, 1, 2, 3)));
+// clang-format on
 
 }  // namespace router_endpoint2node
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/BUILD.gn b/src/connectivity/overnet/lib/links/BUILD.gn
index 250f17b..9149c0a 100644
--- a/src/connectivity/overnet/lib/links/BUILD.gn
+++ b/src/connectivity/overnet/lib/links/BUILD.gn
@@ -22,6 +22,7 @@
     ":packet_link_test",
     ":packet_nub_test",
     ":stream_link_test",
+    ":packet_stuffer_test",
   ]
 }
 
@@ -37,6 +38,7 @@
     "//src/connectivity/overnet/lib/routing:router",
   ]
   deps = [
+    ":packet_stuffer",
     "//src/connectivity/overnet/lib/environment:trace",
     "//src/connectivity/overnet/lib/packet_protocol",
   ]
@@ -64,7 +66,9 @@
     "stream_link.h",
   ]
   public_deps = [
+    ":packet_stuffer",
     "//src/connectivity/overnet/lib/routing:router",
+    "//src/connectivity/overnet/lib/protocol:stream_framer",
   ]
   deps = [
     "//src/connectivity/overnet/lib/environment:trace",
@@ -81,6 +85,8 @@
     "//src/connectivity/overnet/lib/environment:trace_cout",
     "//src/connectivity/overnet/lib/testing:flags",
     "//src/connectivity/overnet/lib/testing:test_timer",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+    "//src/connectivity/overnet/lib/protocol:unreliable_framer",
     "//third_party/googletest:gmock",
     "//third_party/googletest:gtest",
   ]
@@ -103,6 +109,8 @@
     "//src/connectivity/overnet/lib/protocol:fidl",
     "//src/connectivity/overnet/lib/protocol:varint",
     "//src/connectivity/overnet/lib/testing:test_timer",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+    "//src/connectivity/overnet/lib/protocol:unreliable_framer",
   ]
 }
 
@@ -117,6 +125,8 @@
     "//src/connectivity/overnet/lib/protocol:fidl",
     "//src/connectivity/overnet/lib/protocol:varint",
     "//src/connectivity/overnet/lib/testing:test_timer",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+    "//src/connectivity/overnet/lib/protocol:unreliable_framer",
   ]
 }
 
@@ -183,3 +193,32 @@
     "//src/connectivity/overnet/lib/environment:trace_cout",
   ]
 }
+
+# packet_stuffer
+source_set("packet_stuffer") {
+  sources = [
+    "packet_stuffer.cc",
+    "packet_stuffer.h",
+  ]
+  public_deps = [
+    "//src/connectivity/overnet/lib/routing:router",
+  ]
+  deps = [
+    "//src/connectivity/overnet/lib/environment:trace",
+  ]
+}
+
+source_set("packet_stuffer_test") {
+  testonly = true
+  sources = [
+    "packet_stuffer_test.cc",
+  ]
+  deps = [
+    ":packet_stuffer",
+    "//src/connectivity/overnet/lib/environment:trace_cout",
+    "//src/connectivity/overnet/lib/testing:flags",
+    "//src/connectivity/overnet/lib/testing:test_timer",
+    "//third_party/googletest:gmock",
+    "//third_party/googletest:gtest",
+  ]
+}
diff --git a/src/connectivity/overnet/lib/links/packet_link.cc b/src/connectivity/overnet/lib/links/packet_link.cc
index 488b2b3..c42c1bd 100644
--- a/src/connectivity/overnet/lib/links/packet_link.cc
+++ b/src/connectivity/overnet/lib/links/packet_link.cc
@@ -15,31 +15,27 @@
       timer_(router->timer()),
       peer_(peer),
       label_(label),
-      protocol_{router_->timer(), [router] { return (*router->rng())(); }, this,
-                PacketProtocol::NullCodec(), mss} {}
+      protocol_{router_->timer(),
+                [router] { return (*router->rng())(); },
+                this,
+                PacketProtocol::NullCodec(),
+                mss,
+                false},
+      packet_stuffer_(router->node_id(), peer) {}
 
 void PacketLink::Close(Callback<void> quiesced) {
   ScopedModule<PacketLink> scoped_module(this);
-  stashed_.Reset();
-  while (!outgoing_.empty()) {
-    outgoing_.pop();
-  }
+  closed_ = true;
+  packet_stuffer_.DropPendingMessages();
   protocol_.Close(std::move(quiesced));
 }
 
 void PacketLink::Forward(Message message) {
-  // TODO(ctiller): do some real thinking about what this value should be
-  constexpr size_t kMaxBufferedMessages = 32;
   ScopedModule<PacketLink> scoped_module(this);
-  if (outgoing_.size() >= kMaxBufferedMessages) {
-    auto drop = std::move(outgoing_.front());
-    outgoing_.pop();
-  }
-  bool send_immediately = !sending_ && outgoing_.empty();
+  const bool send_immediately =
+      packet_stuffer_.Forward(std::move(message)) && !sending_;
   OVERNET_TRACE(DEBUG) << "Forward sending=" << sending_
-                       << " outgoing=" << outgoing_.size()
                        << " imm=" << send_immediately;
-  outgoing_.emplace(std::move(message));
   if (send_immediately) {
     SchedulePacket();
   }
@@ -76,18 +72,22 @@
 
 void PacketLink::SchedulePacket() {
   assert(!sending_);
-  assert(!outgoing_.empty() || stashed_.has_value());
+  assert(packet_stuffer_.HasPendingMessages());
   auto r = new LinkSendRequest(this);
   OVERNET_TRACE(DEBUG) << "Schedule " << r;
   protocol_.Send(PacketProtocol::SendRequestHdl(r));
 }
 
 PacketLink::LinkSendRequest::LinkSendRequest(PacketLink* link) : link_(link) {
+  OVERNET_TRACE(DEBUG) << "LinkSendRequest[" << this << "]: Create";
   assert(!link->sending_);
   link->sending_ = true;
 }
 
-PacketLink::LinkSendRequest::~LinkSendRequest() { assert(!blocking_sends_); }
+PacketLink::LinkSendRequest::~LinkSendRequest() {
+  assert(!blocking_sends_);
+  OVERNET_TRACE(DEBUG) << "LinkSendRequest[" << this << "]: Destroy";
+}
 
 Slice PacketLink::LinkSendRequest::GenerateBytes(LazySliceArgs args) {
   auto link = link_;
@@ -97,10 +97,10 @@
   assert(blocking_sends_);
   assert(link->sending_);
   blocking_sends_ = false;
-  auto pkt = link->BuildPacket(args);
+  auto pkt = link->packet_stuffer_.BuildPacket(args);
   link->sending_ = false;
   OVERNET_TRACE(DEBUG) << "LinkSendRequest[" << this << "]: Generated " << pkt;
-  if (link->stashed_.has_value() || !link->outgoing_.empty()) {
+  if (link->packet_stuffer_.HasPendingMessages()) {
     link->SchedulePacket();
   }
   return pkt;
@@ -121,93 +121,6 @@
   delete this;
 }
 
-Slice PacketLink::BuildPacket(LazySliceArgs args) {
-  OVERNET_TRACE(DEBUG)
-      << "Build outgoing=" << outgoing_.size() << " stashed="
-      << (stashed_ ? [&](){ 
-        std::ostringstream fmt;
-        fmt << stashed_->message << "+" << stashed_->payload.length() << "b";
-        return fmt.str();
-      }() : "nil");
-  auto remaining_length = args.max_length;
-  auto add_serialized_msg = [&remaining_length, this](
-                                const RoutableMessage& wire,
-                                Slice payload) -> bool {
-    auto serialized = wire.Write(router_->node_id(), peer_, std::move(payload));
-    const auto serialized_length = serialized.length();
-    const auto length_length = varint::WireSizeFor(serialized_length);
-    const auto segment_length = length_length + serialized_length;
-    OVERNET_TRACE(DEBUG) << "AddMsg segment_length=" << segment_length
-                         << " remaining_length=" << remaining_length
-                         << (segment_length > remaining_length ? "  => SKIP"
-                                                               : "")
-                         << "; serialized:" << serialized;
-    if (segment_length > remaining_length) {
-      return false;
-    }
-    send_slices_.push_back(serialized.WithPrefix(
-        length_length, [length_length, serialized_length](uint8_t* p) {
-          varint::Write(serialized_length, length_length, p);
-        }));
-    remaining_length -= segment_length;
-    return true;
-  };
-
-  static const uint32_t kMinMSS = 64;
-  if (stashed_.has_value()) {
-    if (add_serialized_msg(stashed_->message, stashed_->payload)) {
-      stashed_.Reset();
-    } else {
-      if (args.has_other_content) {
-        // Skip sending any other messages: we'll retry this message
-        // without an ack momentarily.
-        remaining_length = 0;
-      } else {
-        // There's no chance we'll ever send this message: drop it.
-        abort();
-        stashed_.Reset();
-        OVERNET_TRACE(DEBUG) << "drop stashed";
-      }
-    }
-  }
-  while (!outgoing_.empty() && remaining_length > kMinMSS) {
-    // Ensure there's space with the routing header included.
-    Optional<size_t> max_len_before_prefix =
-        outgoing_.front().header.MaxPayloadLength(router_->node_id(), peer_,
-                                                  remaining_length);
-    if (!max_len_before_prefix.has_value() || *max_len_before_prefix <= 1) {
-      break;
-    }
-    // And ensure there's space with the segment length header.
-    auto max_len = varint::MaximumLengthWithPrefix(*max_len_before_prefix);
-    // Pull out the message.
-    Message msg = std::move(outgoing_.front());
-    outgoing_.pop();
-    // Serialize it.
-    auto payload = msg.make_payload(LazySliceArgs{
-        Border::None(), std::min(msg.mss, static_cast<uint32_t>(max_len)),
-        args.has_other_content || !send_slices_.empty()});
-    if (payload.length() == 0) {
-      continue;
-    }
-    // Add the serialized version to the outgoing queue.
-    if (!add_serialized_msg(msg.header, payload)) {
-      // If it fails, stash it, and retry the next loop around.
-      // This may happen if the sender is unable to trim to the maximum length.
-      OVERNET_TRACE(DEBUG) << "stash too long";
-      stashed_.Reset(std::move(msg.header), std::move(payload));
-      break;
-    }
-  }
-
-  Slice send =
-      Slice::Join(send_slices_.begin(), send_slices_.end(),
-                  args.desired_border.WithAddedPrefix(SeqNum::kMaxWireLength));
-  send_slices_.clear();
-
-  return send;
-}
-
 void PacketLink::SendPacket(SeqNum seq, LazySlice data) {
   if (send_packet_queue_ != nullptr) {
     send_packet_queue_->emplace(std::move(data));
@@ -228,7 +141,6 @@
       seq.Write(p);
     });
     OVERNET_TRACE(DEBUG) << "Emit " << send_slice;
-    stats_.outgoing_packet_count++;
     Emit(std::move(send_slice));
 
     if (send_packet_queue.empty()) {
@@ -243,7 +155,10 @@
 }
 
 void PacketLink::Process(TimeStamp received, Slice packet) {
-  stats_.incoming_packet_count++;
+  if (closed_) {
+    return;
+  }
+
   ScopedModule<PacketLink> scoped_module(this);
   const uint8_t* const begin = packet.begin();
   const uint8_t* p = begin;
@@ -260,8 +175,9 @@
   ++p;
 
   // Packets without sequence numbers are used to end the three way handshake.
-  if (p == end)
+  if (p == end) {
     return;
+  }
 
   auto seq_status = SeqNum::Parse(&p, end);
   if (seq_status.is_error()) {
@@ -293,32 +209,8 @@
 }
 
 Status PacketLink::ProcessBody(TimeStamp received, Slice packet) {
-  while (packet.length()) {
-    const uint8_t* const begin = packet.begin();
-    const uint8_t* p = begin;
-    const uint8_t* const end = packet.end();
-
-    uint64_t serialized_length;
-    if (!varint::Read(&p, end, &serialized_length)) {
-      return Status(StatusCode::INVALID_ARGUMENT,
-                    "Failed to parse segment length");
-    }
-    assert(end >= p);
-    if (static_cast<uint64_t>(end - p) < serialized_length) {
-      return Status(StatusCode::INVALID_ARGUMENT,
-                    "Message body extends past end of packet");
-    }
-    packet.TrimBegin(p - begin);
-    auto msg_status = RoutableMessage::Parse(
-        packet.TakeUntilOffset(serialized_length), router_->node_id(), peer_);
-    if (msg_status.is_error()) {
-      return msg_status.AsStatus();
-    }
-    router_->Forward(Message::SimpleForwarder(std::move(msg_status->message),
-                                              std::move(msg_status->payload),
-                                              received));
-  }
-  return Status::Ok();
+  return packet_stuffer_.ParseAndForwardTo(received, std::move(packet),
+                                           router_);
 }
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/packet_link.h b/src/connectivity/overnet/lib/links/packet_link.h
index bccb507..5cee610 100644
--- a/src/connectivity/overnet/lib/links/packet_link.h
+++ b/src/connectivity/overnet/lib/links/packet_link.h
@@ -7,6 +7,7 @@
 #include <queue>
 
 #include "src/connectivity/overnet/lib/environment/trace.h"
+#include "src/connectivity/overnet/lib/links/packet_stuffer.h"
 #include "src/connectivity/overnet/lib/packet_protocol/packet_protocol.h"
 #include "src/connectivity/overnet/lib/routing/router.h"
 
@@ -24,13 +25,12 @@
   fuchsia::overnet::protocol::LinkStatus GetLinkStatus() override final;
   // Mark this link as inoperable
   virtual void Tombstone();
-  const LinkStats* GetStats() const override final { return &stats_; }
+  const LinkStats* GetStats() const override final { return protocol_.stats(); }
 
  private:
   void SchedulePacket();
   void SendPacket(SeqNum seq, LazySlice packet) override final;
   Status ProcessBody(TimeStamp received, Slice packet);
-  Slice BuildPacket(LazySliceArgs args);
 
   Router* const router_;
   Timer* const timer_;
@@ -38,9 +38,8 @@
   const uint64_t label_;
   uint64_t metrics_version_ = 1;
   PacketProtocol protocol_;
-  Optional<MessageWithPayload> stashed_;
-  LinkStats stats_;
   bool sending_ = false;
+  bool closed_ = false;
 
   class LinkSendRequest final : public PacketProtocol::SendRequest {
    public:
@@ -56,14 +55,11 @@
     bool blocking_sends_ = true;
   };
 
-  // data for a send
-  std::vector<Slice> send_slices_;
-
-  std::queue<Message> outgoing_;
-
   // Pointer to a queue that's being used to queue pending sends.
   // This queue is held on the stack within SendPacket().
   std::queue<LazySlice>* send_packet_queue_ = nullptr;
+
+  PacketStuffer packet_stuffer_;
 };
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/packet_nub.h b/src/connectivity/overnet/lib/links/packet_nub.h
index ec4c7086..f0257fc 100644
--- a/src/connectivity/overnet/lib/links/packet_nub.h
+++ b/src/connectivity/overnet/lib/links/packet_nub.h
@@ -14,6 +14,7 @@
 #include "src/connectivity/overnet/lib/environment/timer.h"
 #include "src/connectivity/overnet/lib/labels/node_id.h"
 #include "src/connectivity/overnet/lib/links/packet_link.h"
+#include "src/connectivity/overnet/lib/routing/router.h"
 #include "src/connectivity/overnet/lib/vocabulary/slice.h"
 
 namespace overnet {
@@ -89,9 +90,7 @@
   class NubLink final : public PacketLink {
    public:
     NubLink(PacketNub* nub, LinkDataPtr link, NodeId peer, uint64_t label)
-        : PacketLink(nub->GetRouter(), peer, kMSS, label),
-          nub_(nub),
-          link_(link) {}
+        : PacketLink(nub->router_, peer, kMSS, label), nub_(nub), link_(link) {}
 
     ~NubLink() { Delist(); }
 
@@ -179,11 +178,13 @@
   static constexpr size_t kHelloSize = 256;
   static constexpr uint64_t kAnnounceResendMillis = 1000;
 
-  PacketNub(Timer* timer, NodeId node) : timer_(timer), local_node_(node) {}
+  PacketNub(Router* router)
+      : timer_(router->timer()),
+        router_(router),
+        local_node_(router->node_id()) {}
   virtual ~PacketNub() {}
 
   virtual void SendTo(Address dest, Slice slice) = 0;
-  virtual Router* GetRouter() = 0;
   virtual void Publish(LinkPtr<> link) = 0;
 
   virtual void Process(TimeStamp received, Address src, Slice slice) {
@@ -540,15 +541,16 @@
   void BecomePublished(LinkDataPtr link) {
     assert(link->link == nullptr);
     assert(link->node_id);
-    link->link = new NubLink(this, link, *link->node_id, next_label_++);
+    link->link =
+        new NubLink(this, link, *link->node_id, router_->GenerateLinkLabel());
     Publish(LinkPtr<>(link->link));
   }
 
   Timer* const timer_;
+  Router* const router_;
   const NodeId local_node_;
   std::unordered_map<Address, LinkDataPtr, HashAddress, EqAddress> links_;
   std::mt19937_64 rng_;
-  uint64_t next_label_ = 1;
 };
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/packet_nub_connection_fuzzer.cc b/src/connectivity/overnet/lib/links/packet_nub_connection_fuzzer.cc
index c1f7b53..3a7ebc8 100644
--- a/src/connectivity/overnet/lib/links/packet_nub_connection_fuzzer.cc
+++ b/src/connectivity/overnet/lib/links/packet_nub_connection_fuzzer.cc
@@ -16,20 +16,28 @@
 
 class Fuzzer;
 
-class Nub final : public PacketNub<int, 256> {
+class NubStuff {
+ public:
+  NubStuff(Fuzzer* fuzzer, uint8_t index);
+
+  Router* router() { return &router_; }
+
+ private:
+  Router router_;
+};
+
+class Nub final : public NubStuff, public PacketNub<int, 256> {
  public:
   Nub(Fuzzer* fuzzer, uint8_t index);
 
   void SendTo(int dest, Slice slice) override;
-  Router* GetRouter() override;
   void Publish(LinkPtr<> link) override {
-    router_.RegisterLink(std::move(link));
+    router()->RegisterLink(std::move(link));
   }
 
  private:
   Fuzzer* const fuzzer_;
   const uint8_t index_;
-  Router router_;
 };
 
 class Fuzzer {
@@ -38,8 +46,8 @@
 
   Timer* timer() { return &timer_; }
   bool IsDone() {
-    return nub1_.GetRouter()->HasRouteTo(NodeId(2)) &&
-           nub2_.GetRouter()->HasRouteTo(NodeId(1));
+    return nub1_.router()->HasRouteTo(NodeId(2)) &&
+           nub2_.router()->HasRouteTo(NodeId(1));
   }
   bool StepTime() { return timer_.StepUntilNextEvent(); }
   bool StepTime(uint64_t us) { return timer_.Step(us); }
@@ -50,6 +58,16 @@
   void Initiate1();
   void Initiate2();
 
+  ~Fuzzer() {
+    bool done1 = false;
+    bool done2 = false;
+    nub1_.router()->Close([&done1] { done1 = true; });
+    nub2_.router()->Close([&done2] { done2 = true; });
+    while (!done1 || !done2) {
+      StepTime();
+    }
+  }
+
  private:
   TestTimer timer_;
   TraceCout cout_{&timer_};
@@ -71,18 +89,19 @@
   std::vector<Packet> packets_;
 };
 
+NubStuff::NubStuff(Fuzzer* fuzzer, uint8_t index)
+    : router_(fuzzer->timer(), NodeId(index), false) {}
+
 Nub::Nub(Fuzzer* fuzzer, uint8_t index)
-    : PacketNub(fuzzer->timer(), NodeId(index)),
+    : NubStuff(fuzzer, index),
+      PacketNub(router()),
       fuzzer_(fuzzer),
-      index_(index),
-      router_(fuzzer->timer(), NodeId(index), false) {}
+      index_(index) {}
 
 void Nub::SendTo(int dest, Slice slice) {
   fuzzer_->QueueSend(index_, dest, std::move(slice));
 }
 
-Router* Nub::GetRouter() { return &router_; }
-
 Fuzzer::Fuzzer() = default;
 
 void Fuzzer::Initiate1() { nub1_.Initiate({2}, NodeId(2)); }
diff --git a/src/connectivity/overnet/lib/links/packet_nub_fuzzer.cc b/src/connectivity/overnet/lib/links/packet_nub_fuzzer.cc
index 5962617..9609397da 100644
--- a/src/connectivity/overnet/lib/links/packet_nub_fuzzer.cc
+++ b/src/connectivity/overnet/lib/links/packet_nub_fuzzer.cc
@@ -35,7 +35,7 @@
 }
 
 PacketNubFuzzer::Nub::Nub(Timer* timer)
-    : BaseNub(timer, NodeId(1)), router_(timer, NodeId(1), false) {}
+    : Router(timer, NodeId(1), false), BaseNub(this) {}
 
 void PacketNubFuzzer::Nub::Process(TimeStamp received, uint64_t src,
                                    Slice slice) {
@@ -51,8 +51,6 @@
   }
 }
 
-Router* PacketNubFuzzer::Nub::GetRouter() { return &router_; }
-
 void PacketNubFuzzer::Nub::Publish(LinkPtr<> link) {
   auto node = link->GetLinkStatus().from;
   if (NodeId(node) != NodeId(1)) {
diff --git a/src/connectivity/overnet/lib/links/packet_nub_fuzzer.h b/src/connectivity/overnet/lib/links/packet_nub_fuzzer.h
index cc0b527..41ce7c5 100644
--- a/src/connectivity/overnet/lib/links/packet_nub_fuzzer.h
+++ b/src/connectivity/overnet/lib/links/packet_nub_fuzzer.h
@@ -42,18 +42,16 @@
   Budget budget_;
 
   using BaseNub = PacketNub<uint64_t, 256>;
-  class Nub : public BaseNub {
+  class Nub : private Router, public BaseNub {
    public:
     Nub(Timer* timer);
 
     void Process(TimeStamp received, uint64_t src, Slice slice) override;
 
     void SendTo(uint64_t dest, Slice slice) override;
-    Router* GetRouter() override;
     void Publish(LinkPtr<> link) override;
 
    private:
-    Router router_;
     Budget budget_;
   };
   Nub nub_{&timer_};
diff --git a/src/connectivity/overnet/lib/links/packet_nub_test.cc b/src/connectivity/overnet/lib/links/packet_nub_test.cc
index a139f35..3d88153 100644
--- a/src/connectivity/overnet/lib/links/packet_nub_test.cc
+++ b/src/connectivity/overnet/lib/links/packet_nub_test.cc
@@ -21,10 +21,22 @@
 
 using FakeAddress = uint32_t;
 
-class MockPacketNub : public PacketNub<FakeAddress, 1024> {
+class MockPacketNubBase {
+ public:
+  MockPacketNubBase(Timer* timer, NodeId node) : router_(timer, node, true) {}
+
+  Router* GetRouter() { return &router_; }
+
+ protected:
+  Router router_;
+};
+
+class MockPacketNub : public MockPacketNubBase,
+                      public PacketNub<FakeAddress, 1024> {
  public:
   MockPacketNub(Timer* timer, NodeId node)
-      : PacketNub<FakeAddress, 1024>(timer, node), router_(timer, node, true) {}
+      : MockPacketNubBase(timer, node),
+        PacketNub<FakeAddress, 1024>(GetRouter()) {}
 
   MOCK_METHOD2(SendTo, void(FakeAddress, Slice));
   MOCK_METHOD1(PublishMock, void(std::shared_ptr<LinkPtr<>>));
@@ -32,11 +44,6 @@
   void Publish(LinkPtr<> link) override final {
     PublishMock(std::make_shared<LinkPtr<>>(std::move(link)));
   }
-
-  Router* GetRouter() override final { return &router_; }
-
- private:
-  Router router_;
 };
 
 TEST(PacketNub, NoOp) {
diff --git a/src/connectivity/overnet/lib/links/packet_stuffer.cc b/src/connectivity/overnet/lib/links/packet_stuffer.cc
new file mode 100644
index 0000000..8f3468b
--- /dev/null
+++ b/src/connectivity/overnet/lib/links/packet_stuffer.cc
@@ -0,0 +1,153 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/links/packet_stuffer.h"
+
+namespace overnet {
+
+PacketStuffer::PacketStuffer(NodeId my_node_id, NodeId peer_node_id)
+    : my_node_id_(my_node_id), peer_node_id_(peer_node_id) {}
+
+bool PacketStuffer::Forward(Message message) {
+  // TODO(ctiller): do some real thinking about what this value should be
+  constexpr size_t kMaxBufferedMessages = 32;
+  if (outgoing_.size() >= kMaxBufferedMessages) {
+    auto drop = std::move(outgoing_.front());
+    outgoing_.pop();
+  }
+  const bool was_empty = !HasPendingMessages();
+  outgoing_.emplace(std::move(message));
+  return was_empty;
+}
+
+void PacketStuffer::DropPendingMessages() {
+  stashed_.Reset();
+  while (!outgoing_.empty()) {
+    outgoing_.pop();
+  }
+}
+
+bool PacketStuffer::HasPendingMessages() const {
+  return stashed_.has_value() || !outgoing_.empty();
+}
+
+Slice PacketStuffer::BuildPacket(LazySliceArgs args) {
+  OVERNET_TRACE(DEBUG)
+      << "Build outgoing=" << outgoing_.size() << " stashed="
+      << (stashed_ ? [&](){
+        std::ostringstream fmt;
+        fmt << stashed_->message << "+" << stashed_->payload.length() << "b";
+        return fmt.str();
+      }() : "nil");
+  auto remaining_length = args.max_length;
+  auto add_serialized_msg = [&remaining_length, this](
+                                const RoutableMessage& wire,
+                                Slice payload) -> bool {
+    auto serialized =
+        wire.Write(my_node_id_, peer_node_id_, std::move(payload));
+    const auto serialized_length = serialized.length();
+    const auto length_length = varint::WireSizeFor(serialized_length);
+    const auto segment_length = length_length + serialized_length;
+    OVERNET_TRACE(DEBUG) << "AddMsg segment_length=" << segment_length
+                         << " remaining_length=" << remaining_length
+                         << (segment_length > remaining_length ? "  => SKIP"
+                                                               : "")
+                         << "; serialized:" << serialized;
+    if (segment_length > remaining_length) {
+      return false;
+    }
+    send_slices_.push_back(serialized.WithPrefix(
+        length_length, [length_length, serialized_length](uint8_t* p) {
+          varint::Write(serialized_length, length_length, p);
+        }));
+    remaining_length -= segment_length;
+    return true;
+  };
+
+  static const uint32_t kMinMSS = 64;
+  if (stashed_.has_value()) {
+    if (add_serialized_msg(stashed_->message, stashed_->payload)) {
+      stashed_.Reset();
+    } else {
+      if (args.has_other_content) {
+        // Skip sending any other messages: we'll retry this message
+        // without an ack momentarily.
+        remaining_length = 0;
+      } else {
+        // There's no chance we'll ever send this message: drop it.
+        abort();
+        stashed_.Reset();
+        OVERNET_TRACE(DEBUG) << "drop stashed";
+      }
+    }
+  }
+  while (!outgoing_.empty() && remaining_length > kMinMSS) {
+    // Ensure there's space with the routing header included.
+    Optional<size_t> max_len_before_prefix =
+        outgoing_.front().header.MaxPayloadLength(my_node_id_, peer_node_id_,
+                                                  remaining_length);
+    if (!max_len_before_prefix.has_value() || *max_len_before_prefix <= 1) {
+      break;
+    }
+    // And ensure there's space with the segment length header.
+    auto max_len = varint::MaximumLengthWithPrefix(*max_len_before_prefix);
+    // Pull out the message.
+    Message msg = std::move(outgoing_.front());
+    outgoing_.pop();
+    // Serialize it.
+    auto payload = msg.make_payload(LazySliceArgs{
+        Border::None(), std::min(msg.mss, static_cast<uint32_t>(max_len)),
+        args.has_other_content || !send_slices_.empty()});
+    if (payload.length() == 0) {
+      continue;
+    }
+    // Add the serialized version to the outgoing queue.
+    if (!add_serialized_msg(msg.header, payload)) {
+      // If it fails, stash it, and retry the next loop around.
+      // This may happen if the sender is unable to trim to the maximum length.
+      OVERNET_TRACE(DEBUG) << "stash too long";
+      stashed_.Reset(std::move(msg.header), std::move(payload));
+      break;
+    }
+  }
+
+  Slice send =
+      Slice::Join(send_slices_.begin(), send_slices_.end(),
+                  args.desired_border.WithAddedPrefix(SeqNum::kMaxWireLength));
+  send_slices_.clear();
+
+  return send;
+}
+
+Status PacketStuffer::ParseAndForwardTo(TimeStamp received, Slice packet,
+                                        Router* router) const {
+  while (packet.length()) {
+    const uint8_t* const begin = packet.begin();
+    const uint8_t* p = begin;
+    const uint8_t* const end = packet.end();
+
+    uint64_t serialized_length;
+    if (!varint::Read(&p, end, &serialized_length)) {
+      return Status(StatusCode::INVALID_ARGUMENT,
+                    "Failed to parse segment length");
+    }
+    assert(end >= p);
+    if (static_cast<uint64_t>(end - p) < serialized_length) {
+      return Status(StatusCode::INVALID_ARGUMENT,
+                    "Message body extends past end of packet");
+    }
+    packet.TrimBegin(p - begin);
+    auto msg_status = RoutableMessage::Parse(
+        packet.TakeUntilOffset(serialized_length), my_node_id_, peer_node_id_);
+    if (msg_status.is_error()) {
+      return msg_status.AsStatus();
+    }
+    router->Forward(Message::SimpleForwarder(std::move(msg_status->message),
+                                             std::move(msg_status->payload),
+                                             received));
+  }
+  return Status::Ok();
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/packet_stuffer.h b/src/connectivity/overnet/lib/links/packet_stuffer.h
new file mode 100644
index 0000000..2cd98222
--- /dev/null
+++ b/src/connectivity/overnet/lib/links/packet_stuffer.h
@@ -0,0 +1,41 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <lib/fit/function.h>
+
+#include <queue>
+
+#include "src/connectivity/overnet/lib/routing/router.h"
+
+namespace overnet {
+
+// Manages a queue of outgoing messages, packing them into packets on request.
+class PacketStuffer {
+ public:
+  PacketStuffer(NodeId my_node_id, NodeId peer_node_id);
+
+  // Forward a message.
+  // Returns true if this is the first queued message.
+  [[nodiscard]] bool Forward(Message message);
+
+  void DropPendingMessages();
+  bool HasPendingMessages() const;
+
+  Slice BuildPacket(LazySliceArgs args);
+  Status ParseAndForwardTo(TimeStamp received, Slice packet,
+                           Router* router) const;
+
+ private:
+  const NodeId my_node_id_;
+  const NodeId peer_node_id_;
+  std::queue<Message> outgoing_;
+  Optional<MessageWithPayload> stashed_;
+
+  // data for a send
+  std::vector<Slice> send_slices_;
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/packet_stuffer_test.cc b/src/connectivity/overnet/lib/links/packet_stuffer_test.cc
new file mode 100644
index 0000000..575d0b7
--- /dev/null
+++ b/src/connectivity/overnet/lib/links/packet_stuffer_test.cc
@@ -0,0 +1,122 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/links/packet_stuffer.h"
+
+#include <gtest/gtest.h>
+
+#include "src/connectivity/overnet/lib/testing/test_timer.h"
+
+namespace overnet {
+namespace packet_stuffer_test {
+
+constexpr TimeStamp kDummyTimestamp123 =
+    TimeStamp::AfterEpoch(TimeDelta::FromMicroseconds(123));
+
+Message DummyMessage() {
+  return Message{std::move(RoutableMessage(NodeId(1)).AddDestination(
+                     NodeId(2), StreamId(1), SeqNum(1, 1))),
+                 ForwardingPayloadFactory(Slice::FromContainer({1, 2, 3})),
+                 kDummyTimestamp123};
+}
+
+template <class T>
+void IgnoreResult(T&& value) {}
+
+TEST(PacketStuffer, NoOp) { PacketStuffer stuffer(NodeId(1), NodeId(2)); }
+
+TEST(PacketStuffer, ForwardReturnValue) {
+  PacketStuffer stuffer(NodeId(1), NodeId(2));
+  EXPECT_TRUE(stuffer.Forward(DummyMessage()));
+  EXPECT_FALSE(stuffer.Forward(DummyMessage()));
+}
+
+TEST(PacketStuffer, CanDropMessages) {
+  PacketStuffer stuffer(NodeId(1), NodeId(2));
+  EXPECT_FALSE(stuffer.HasPendingMessages());
+  EXPECT_TRUE(stuffer.Forward(DummyMessage()));
+  EXPECT_TRUE(stuffer.HasPendingMessages());
+  stuffer.DropPendingMessages();
+  EXPECT_FALSE(stuffer.HasPendingMessages());
+}
+
+struct PacketVerificationArgs {
+  std::vector<Slice> messages;
+  size_t max_serialize_length;
+  Slice expected_bytes;
+};
+
+struct PacketStufferSerialization
+    : public ::testing::TestWithParam<PacketVerificationArgs> {};
+
+TEST_P(PacketStufferSerialization, Write) {
+  PacketStuffer stuffer(NodeId(1), NodeId(2));
+
+  int i = 0;
+  for (auto msg : GetParam().messages) {
+    IgnoreResult(stuffer.Forward(Message{
+        std::move(RoutableMessage(NodeId(1)).AddDestination(
+            NodeId(3), StreamId(1), SeqNum(i++, GetParam().messages.size()))),
+        ForwardingPayloadFactory(msg), kDummyTimestamp123}));
+  }
+
+  EXPECT_EQ(stuffer.BuildPacket(
+                LazySliceArgs{Border::None(), GetParam().max_serialize_length}),
+            GetParam().expected_bytes);
+}
+
+TEST_P(PacketStufferSerialization, Read) {
+  TestTimer timer;
+  Router router(&timer, NodeId(2), false);
+  std::vector<Slice> got_messages;
+
+  class MockLink final : public Link {
+   public:
+    MockLink(std::vector<Slice>* got_messages) : got_messages_(got_messages) {}
+
+    void Close(Callback<void> quiesced) override {}
+    fuchsia::overnet::protocol::LinkStatus GetLinkStatus() override {
+      return fuchsia::overnet::protocol::LinkStatus{NodeId(2).as_fidl(),
+                                                    NodeId(3).as_fidl(), 1, 1};
+    }
+    const LinkStats* GetStats() const override { return nullptr; }
+
+    void Forward(Message message) override {
+      EXPECT_EQ(message.header.src(), NodeId(1));
+      EXPECT_EQ(message.header.destinations().size(), size_t(1));
+      EXPECT_EQ(message.header.destinations()[0].dst(), NodeId(3));
+      got_messages_->emplace_back(message.make_payload(
+          LazySliceArgs{Border::None(), std::numeric_limits<size_t>::max()}));
+    }
+
+   private:
+    std::vector<Slice>* const got_messages_;
+  };
+  router.RegisterLink(MakeLink<MockLink>(&got_messages));
+
+  while (!router.HasRouteTo(NodeId(3))) {
+    timer.StepUntilNextEvent();
+  }
+
+  auto status = PacketStuffer(NodeId(1), NodeId(2))
+                    .ParseAndForwardTo(kDummyTimestamp123,
+                                       GetParam().expected_bytes, &router);
+
+  EXPECT_TRUE(status.is_ok()) << status;
+  EXPECT_EQ(got_messages, GetParam().messages);
+}
+
+INSTANTIATE_TEST_SUITE_P(PacketStufferSerializationSuite,
+                         PacketStufferSerialization,
+                         ::testing::Values(PacketVerificationArgs{
+                             {Slice::FromContainer({1, 2, 3})},
+                             256,
+                             Slice::FromContainer({
+                                 0x16, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03,
+                             })}));
+
+}  // namespace packet_stuffer_test
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/stream_link.cc b/src/connectivity/overnet/lib/links/stream_link.cc
index e3cfd08..89ef723 100644
--- a/src/connectivity/overnet/lib/links/stream_link.cc
+++ b/src/connectivity/overnet/lib/links/stream_link.cc
@@ -6,92 +6,97 @@
 
 namespace overnet {
 
-StreamLink::StreamLink(Router *router, NodeId peer, uint32_t mss,
-                       uint64_t label)
-    : mss_(mss), router_(router), peer_(peer), local_id_(label) {}
+StreamLink::StreamLink(Router *router, NodeId peer,
+                       std::unique_ptr<StreamFramer> framer, uint64_t label)
+    : router_(router),
+      framer_(std::move(framer)),
+      peer_(peer),
+      local_id_(label),
+      packet_stuffer_(router->node_id(), peer) {}
 
 void StreamLink::Forward(Message message) {
-  if (emitting_ || closed_) {
+  if (closed_) {
     return;
   }
-
-  // Ensure that we fit into the mss with the routing header
-  Optional<size_t> max_payload_length =
-      message.header.MaxPayloadLength(router_->node_id(), peer_, mss_);
-  if (!max_payload_length.has_value() || *max_payload_length <= 1) {
-    // Drop packet (higher layers can resend if needed).
-    return;
+  if (packet_stuffer_.Forward(std::move(message)) && !emitting_) {
+    EmitOne();
   }
+}
 
-  auto payload = message.make_payload(LazySliceArgs{
-      Border::Prefix(varint::WireSizeFor(mss_)), *max_payload_length, false});
+void StreamLink::SetClosed() {
+  closed_ = true;
+  packet_stuffer_.DropPendingMessages();
+}
 
-  auto packet =
-      message.header.Write(router_->node_id(), peer_, std::move(payload));
+void StreamLink::EmitOne() {
+  static uint64_t num_forwards = 0;
+  auto n = num_forwards++;
+  OVERNET_TRACE(TRACE) << "StreamLink::EmitOne[" << n
+                       << "]: forward with emitting=" << emitting_
+                       << " closed=" << closed_;
 
-  auto packet_length = packet.length();
-  auto prefix_length = varint::WireSizeFor(packet_length);
+  assert(!emitting_);
+  assert(!closed_);
+  assert(packet_stuffer_.HasPendingMessages());
+
+  auto packet = packet_stuffer_.BuildPacket(
+      LazySliceArgs{framer_->desired_border, framer_->maximum_segment_size});
+
+  OVERNET_TRACE(TRACE) << "StreamLink::EmitOne[" << n << "]: emit " << packet;
 
   emitting_ = true;
-  Emit(packet.WithPrefix(
-           prefix_length,
-           [=](uint8_t *p) { varint::Write(packet_length, prefix_length, p); }),
-       [this](const Status &status) {
+  stats_.outgoing_packet_count++;
+  Emit(framer_->Frame(std::move(packet)),
+       StatusCallback(ALLOCATED_CALLBACK, [this, n](const Status &status) {
          if (status.is_error()) {
-           closed_ = true;
+           OVERNET_TRACE(ERROR) << "Write failed: " << status;
+           SetClosed();
          }
          emitting_ = false;
-         MaybeQuiesce();
-       });
+         OVERNET_TRACE(TRACE) << "StreamLink::EmitOne[" << n << "]: emitted";
+         if (closed_) {
+           MaybeQuiesce();
+         } else if (packet_stuffer_.HasPendingMessages()) {
+           EmitOne();
+         }
+       }));
 }
 
 void StreamLink::Process(TimeStamp received, Slice bytes) {
+  OVERNET_TRACE(TRACE) << "StreamLink.Read: " << bytes;
+
   if (closed_) {
     return;
   }
 
-  buffered_input_.Append(std::move(bytes));
+  framer_->Push(std::move(bytes));
 
   for (;;) {
-    const uint8_t *begin = buffered_input_.begin();
-    const uint8_t *p = begin;
-    const uint8_t *end = buffered_input_.end();
-
-    uint64_t segment_length;
-    if (!varint::Read(&p, end, &segment_length)) {
-      if (end - p >= varint::WireSizeFor(mss_)) {
-        closed_ = true;
-      }
+    auto input = framer_->Pop();
+    if (input.is_error()) {
+      OVERNET_TRACE(ERROR) << input.AsStatus();
+      SetClosed();
+      return;
+    }
+    if (!input->has_value()) {
       return;
     }
 
-    if (segment_length > mss_) {
-      closed_ = true;
+    stats_.incoming_packet_count++;
+
+    if (auto status = packet_stuffer_.ParseAndForwardTo(
+            received, std::move(**input), router_);
+        status.is_error()) {
+      OVERNET_TRACE(ERROR) << input.AsStatus();
+      SetClosed();
       return;
     }
-
-    if (static_cast<uint64_t>(end - p) < segment_length) {
-      return;
-    }
-
-    buffered_input_.TrimBegin(p - begin);
-    auto message_with_payload =
-        RoutableMessage::Parse(buffered_input_.TakeUntilOffset(segment_length),
-                               router_->node_id(), peer_);
-
-    if (message_with_payload.is_error()) {
-      closed_ = true;
-      return;
-    }
-
-    router_->Forward(Message::SimpleForwarder(
-        std::move(message_with_payload->message),
-        std::move(message_with_payload->payload), received));
   }
 }
 
 void StreamLink::Close(Callback<void> quiesced) {
-  closed_ = true;
+  OVERNET_TRACE(DEBUG) << "Stream link closed by router";
+  SetClosed();
   on_quiesced_ = std::move(quiesced);
   MaybeQuiesce();
 }
@@ -104,9 +109,16 @@
 }
 
 fuchsia::overnet::protocol::LinkStatus StreamLink::GetLinkStatus() {
-  return fuchsia::overnet::protocol::LinkStatus{
-      router_->node_id().as_fidl(), peer_.as_fidl(), local_id_, 1,
-      fuchsia::overnet::protocol::LinkMetrics{}};
+  // Advertise MSS as smaller than it is to account for some bugs that exist
+  // right now.
+  // TODO(ctiller): eliminate this - we should be precise.
+  constexpr size_t kUnderadvertiseMaximumSendSize = 32;
+  fuchsia::overnet::protocol::LinkMetrics m;
+  m.set_mss(std::max(kUnderadvertiseMaximumSendSize, maximum_segment_size()) -
+            kUnderadvertiseMaximumSendSize);
+  return fuchsia::overnet::protocol::LinkStatus{router_->node_id().as_fidl(),
+                                                peer_.as_fidl(), local_id_, 1,
+                                                std::move(m)};
 }
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/stream_link.h b/src/connectivity/overnet/lib/links/stream_link.h
index 3436108..5ba2774 100644
--- a/src/connectivity/overnet/lib/links/stream_link.h
+++ b/src/connectivity/overnet/lib/links/stream_link.h
@@ -4,13 +4,16 @@
 
 #pragma once
 
+#include "src/connectivity/overnet/lib/links/packet_stuffer.h"
+#include "src/connectivity/overnet/lib/protocol/stream_framer.h"
 #include "src/connectivity/overnet/lib/routing/router.h"
 
 namespace overnet {
 
 class StreamLink : public Link {
  public:
-  StreamLink(Router* router, NodeId peer, uint32_t mss, uint64_t label);
+  StreamLink(Router* router, NodeId peer, std::unique_ptr<StreamFramer> framer,
+             uint64_t label);
 
   void Close(Callback<void> quiesced) override final;
   void Forward(Message message) override final;
@@ -20,17 +23,21 @@
   void Process(TimeStamp received, Slice bytes);
   virtual void Emit(Slice bytes, Callback<Status> done) = 0;
 
+  size_t maximum_segment_size() const { return framer_->maximum_segment_size; }
+
  private:
   void MaybeQuiesce();
+  void EmitOne();
+  void SetClosed();
 
-  const size_t mss_;
   Router* const router_;
+  std::unique_ptr<StreamFramer> framer_;
   const NodeId peer_;
   const uint64_t local_id_;
   bool emitting_ = false;
   bool closed_ = false;
-  Slice buffered_input_;
   Callback<void> on_quiesced_;
+  PacketStuffer packet_stuffer_;
   LinkStats stats_;
 };
 
diff --git a/src/connectivity/overnet/lib/links/stream_link_fuzzer.test.fidl b/src/connectivity/overnet/lib/links/stream_link_fuzzer.test.fidl
index 43ec029..1275fab 100644
--- a/src/connectivity/overnet/lib/links/stream_link_fuzzer.test.fidl
+++ b/src/connectivity/overnet/lib/links/stream_link_fuzzer.test.fidl
@@ -7,12 +7,24 @@
 /// A list of packets to process on a stream link
 /// For stream_link_untrusted_fuzzer
 struct UntrustedInputPlan {
+  UntrustedLinkDescription link_description;
   vector<vector<uint8>> input;
 };
 
+struct UntrustedReliableLink {};
+
+struct UntrustedUnreliableLink {};
+
+/// A description of a link between nodes
+xunion UntrustedLinkDescription {
+  UntrustedReliableLink reliable;
+  UntrustedUnreliableLink unreliable;
+};
+
 /// A list of actions to perform on a peer to peer fixture
 /// For stream_link_peer_to_peer_fuzzer
 struct PeerToPeerPlan {
+  PeerToPeerLinkDescription link_description;
   vector<PeerToPeerAction> actions;
 };
 
@@ -41,3 +53,30 @@
     /// Allow this many bytes to flow from node --> the other node
     uint64 allow_bytes;
 };
+
+/// A reliable link: uses the ReliableFramer class
+struct PeerToPeerReliableLink {};
+
+/// An unreliable link: uses the UnreliableFramer class
+/// and additionally has a mutation plan to modify bytes on the wire
+struct PeerToPeerUnreliableLink {
+  vector<StreamMutation> mutation_plan_1_to_2;
+  vector<StreamMutation> mutation_plan_2_to_1;
+};
+
+/// A description of a link between nodes
+xunion PeerToPeerLinkDescription {
+  PeerToPeerReliableLink reliable;
+  PeerToPeerUnreliableLink unreliable;
+};
+
+/// Describes a mutation to apply to a data stream
+xunion StreamMutation {
+  /// bit[flip_bit] ^= 1
+  uint64 flip_bit;
+};
+
+struct InsertByte {
+  uint64 position;
+  uint8 byte;
+};
diff --git a/src/connectivity/overnet/lib/links/stream_link_peer_to_peer_fuzzer.cc b/src/connectivity/overnet/lib/links/stream_link_peer_to_peer_fuzzer.cc
index 75b54a8..ba6d208 100644
--- a/src/connectivity/overnet/lib/links/stream_link_peer_to_peer_fuzzer.cc
+++ b/src/connectivity/overnet/lib/links/stream_link_peer_to_peer_fuzzer.cc
@@ -3,10 +3,14 @@
 // found in the LICENSE file.
 
 #include <fuchsia/overnet/streamlinkfuzzer/cpp/fidl.h>
+
 #include <queue>
+
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
 #include "src/connectivity/overnet/lib/links/stream_link.h"
 #include "src/connectivity/overnet/lib/protocol/fidl.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+#include "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
 #include "src/connectivity/overnet/lib/routing/router.h"
 #include "src/connectivity/overnet/lib/testing/test_timer.h"
 
@@ -14,10 +18,54 @@
 
 namespace {
 
+const auto kDummyMutation = [](auto, auto) -> Slice { return Slice(); };
+
+class StreamMutator {
+ public:
+  StreamMutator(std::vector<fuchsia::overnet::streamlinkfuzzer::StreamMutation>
+                    mutations) {
+    for (const auto& mut : mutations) {
+      switch (mut.Which()) {
+        case fuchsia::overnet::streamlinkfuzzer::StreamMutation::Tag::kFlipBit:
+          mops_.emplace_back(
+              mut.flip_bit() / 8,
+              [bit = 1 << (mut.flip_bit() % 8)](uint8_t offset, Slice slice) {
+                return slice.MutateUnique(
+                    [offset, bit](uint8_t* p) { p[offset] ^= bit; });
+              });
+          break;
+        case fuchsia::overnet::streamlinkfuzzer::StreamMutation::Tag::Empty:
+          break;
+      }
+    }
+    std::stable_sort(mops_.begin(), mops_.end(), CompareMOpPos);
+  }
+
+  Slice Mutate(uint64_t offset, Slice incoming) {
+    for (auto it = std::lower_bound(mops_.begin(), mops_.end(),
+                                    MOp(offset, kDummyMutation), CompareMOpPos);
+         it != mops_.end() && it->first < offset + incoming.length(); ++it) {
+      incoming = it->second(it->first - offset, incoming);
+    }
+    return incoming;
+  }
+
+ private:
+  using MOp = std::pair<uint64_t, std::function<Slice(uint64_t, Slice)>>;
+  std::vector<MOp> mops_;
+
+  static bool CompareMOpPos(const MOp& a, const MOp& b) {
+    return a.first < b.first;
+  }
+};
+
 class FuzzedStreamLink final : public StreamLink {
  public:
-  FuzzedStreamLink(Router* router, NodeId peer)
-      : StreamLink(router, peer, 64, 1), timer_(router->timer()) {}
+  FuzzedStreamLink(Router* router, NodeId peer,
+                   std::unique_ptr<StreamFramer> framer, StreamMutator mutator)
+      : StreamLink(router, peer, std::move(framer), 1),
+        timer_(router->timer()),
+        mutator_(std::move(mutator)) {}
 
   bool is_busy() const { return !done_.empty(); }
 
@@ -38,11 +86,13 @@
   void Flush(uint64_t bytes) {
     auto pending_length = pending_.length();
     if (bytes == 0) {
-    } else if (bytes >= pending_length) {
-      partner_->Process(timer_->Now(), std::move(pending_));
-    } else {
-      partner_->Process(timer_->Now(), pending_.TakeUntilOffset(bytes));
+      return;
     }
+    Slice process = mutator_.Mutate(
+        offset_, bytes >= pending_length ? std::move(pending_)
+                                         : pending_.TakeUntilOffset(bytes));
+    offset_ += process.length();
+    partner_->Process(timer_->Now(), std::move(process));
   }
 
   void set_partner(FuzzedStreamLink* partner) { partner_ = partner; }
@@ -52,6 +102,8 @@
   Slice pending_;
   Callback<Status> done_;
   FuzzedStreamLink* partner_ = nullptr;
+  uint64_t offset_ = 0;
+  StreamMutator mutator_;
 };
 
 class FuzzedHandler final : public Router::StreamHandler {
@@ -82,13 +134,16 @@
 
 class StreamLinkFuzzer {
  public:
-  StreamLinkFuzzer(bool log_stuff)
+  StreamLinkFuzzer(bool log_stuff, std::unique_ptr<StreamFramer> framer,
+                   StreamMutator mut_1_to_2, StreamMutator mut_2_to_1)
       : logging_(log_stuff ? new Logging(&timer_) : nullptr) {
-    auto link = MakeLink<FuzzedStreamLink>(&router_1_, NodeId(2));
+    auto link = MakeLink<FuzzedStreamLink>(
+        &router_1_, NodeId(2), std::move(framer), std::move(mut_1_to_2));
     link_12_ = link.get();
     router_1_.RegisterLink(std::move(link));
 
-    link = MakeLink<FuzzedStreamLink>(&router_2_, NodeId(1));
+    link = MakeLink<FuzzedStreamLink>(&router_2_, NodeId(1), std::move(framer),
+                                      std::move(mut_2_to_1));
     link_21_ = link.get();
     router_2_.RegisterLink(std::move(link));
 
@@ -218,13 +273,43 @@
   FuzzedHandler handler_2_;
 };
 
+struct Helpers {
+  std::unique_ptr<StreamFramer> framer;
+  StreamMutator mut_1_to_2;
+  StreamMutator mut_2_to_1;
+};
+
+Helpers MakeHelpers(
+    fuchsia::overnet::streamlinkfuzzer::PeerToPeerLinkDescription* desc) {
+  switch (desc->Which()) {
+    case fuchsia::overnet::streamlinkfuzzer::PeerToPeerLinkDescription::Tag::
+        Empty:
+      return Helpers{nullptr, StreamMutator({}), StreamMutator({})};
+    case fuchsia::overnet::streamlinkfuzzer::PeerToPeerLinkDescription::Tag::
+        kReliable:
+      return Helpers{std::make_unique<ReliableFramer>(), StreamMutator({}),
+                     StreamMutator({})};
+    case fuchsia::overnet::streamlinkfuzzer::PeerToPeerLinkDescription::Tag::
+        kUnreliable:
+      return Helpers{
+          std::make_unique<UnreliableFramer>(),
+          StreamMutator(std::move(desc->unreliable().mutation_plan_1_to_2)),
+          StreamMutator(std::move(desc->unreliable().mutation_plan_2_to_1))};
+  }
+}
+
 }  // namespace
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
   if (auto buffer = Decode<fuchsia::overnet::streamlinkfuzzer::PeerToPeerPlan>(
           Slice::FromCopiedBuffer(data, size));
       buffer.is_ok()) {
-    StreamLinkFuzzer(false).Run(std::move(*buffer));
+    if (auto helpers = MakeHelpers(&buffer->link_description); helpers.framer) {
+      StreamLinkFuzzer(false, std::move(helpers.framer),
+                       std::move(helpers.mut_1_to_2),
+                       std::move(helpers.mut_2_to_1))
+          .Run(std::move(*buffer));
+    }
   }
   return 0;
 }
diff --git a/src/connectivity/overnet/lib/links/stream_link_test.cc b/src/connectivity/overnet/lib/links/stream_link_test.cc
index bfde81f..a6d7067 100644
--- a/src/connectivity/overnet/lib/links/stream_link_test.cc
+++ b/src/connectivity/overnet/lib/links/stream_link_test.cc
@@ -3,9 +3,13 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/links/stream_link.h"
+
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
+
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+#include "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
 #include "src/connectivity/overnet/lib/testing/flags.h"
 #include "src/connectivity/overnet/lib/testing/test_timer.h"
 
@@ -18,8 +22,6 @@
 namespace overnet {
 namespace stream_link_test {
 
-constexpr auto kTestMSS = 2048;
-
 class MockStreamHandler : public Router::StreamHandler {
  public:
   MOCK_METHOD3(HandleMessage, void(SeqNum, TimeStamp, Slice));
@@ -30,13 +32,15 @@
  public:
   MOCK_METHOD2(Emit, void(Slice, std::function<void(Status)>));
 
-  LinkPtr<StreamLink> MakeLink(Router* router, NodeId peer, uint32_t mss) {
+  LinkPtr<StreamLink> MakeLink(Router* router, NodeId peer,
+                               std::unique_ptr<StreamFramer> framer) {
     static uint64_t next_label = 1;
     class PacketLinkImpl final : public StreamLink {
      public:
       PacketLinkImpl(MockStreamLink* mock, Router* router, NodeId peer,
-                     uint32_t mss)
-          : StreamLink(router, peer, mss, next_label++), mock_(mock) {}
+                     std::unique_ptr<StreamFramer> framer)
+          : StreamLink(router, peer, std::move(framer), next_label++),
+            mock_(mock) {}
       void Emit(Slice packet, Callback<Status> done) {
         auto cb = std::make_shared<Callback<Status>>(std::move(done));
         mock_->Emit(std::move(packet),
@@ -46,7 +50,8 @@
      private:
       MockStreamLink* mock_;
     };
-    return overnet::MakeLink<PacketLinkImpl>(this, router, peer, mss);
+    return overnet::MakeLink<PacketLinkImpl>(this, router, peer,
+                                             std::move(framer));
   }
 };
 
@@ -62,15 +67,15 @@
                                                 : Severity::INFO};
 };
 
-TEST(StreamLink, NoOp) {
-  TestEnvironment env;
+struct TestArg {
+  std::function<std::unique_ptr<StreamFramer>()> make_framer;
+  Slice payload;
+  Slice framed;
+};
 
-  StrictMock<MockStreamLink> mock_link;
-  Router router(env.timer(), NodeId(1), true);
-  router.RegisterLink(mock_link.MakeLink(&router, NodeId(2), kTestMSS));
-}
+struct StreamLinkTest : public ::testing::TestWithParam<TestArg> {};
 
-TEST(StreamLink, SendOne) {
+TEST_P(StreamLinkTest, SendOne) {
   TestEnvironment env;
   StrictMock<MockStreamLink> mock_link;
 
@@ -79,7 +84,8 @@
   };
 
   Router router(env.timer(), NodeId(1), true);
-  router.RegisterLink(mock_link.MakeLink(&router, NodeId(2), kTestMSS));
+  router.RegisterLink(
+      mock_link.MakeLink(&router, NodeId(2), GetParam().make_framer()));
   while (!router.HasRouteTo(NodeId(2))) {
     router.BlockUntilNoBackgroundUpdatesProcessing();
     env.timer()->StepUntilNextEvent();
@@ -92,23 +98,24 @@
   router.Forward(Message{
       std::move(RoutableMessage(NodeId(1)).AddDestination(
           NodeId(2), StreamId(1), SeqNum(1, 1))),
-      ForwardingPayloadFactory(Slice::FromContainer({7, 8, 9})),
+      ForwardingPayloadFactory(Slice::FromContainer(GetParam().payload)),
       env.timer()->Now(),
   });
   verify_all();
 
   done_emit(Status::Ok());
 
-  EXPECT_EQ(Slice::FromContainer({6, 0, 1, 1, 7, 8, 9}), emitted);
+  EXPECT_EQ(GetParam().framed, emitted);
 }
 
-TEST(StreamLink, RecvOne) {
+TEST_P(StreamLinkTest, RecvOne) {
   TestEnvironment env;
   StrictMock<MockStreamLink> mock_link;
   StrictMock<MockStreamHandler> mock_stream_handler;
 
   Router router(env.timer(), NodeId(2), true);
-  auto link_unique = mock_link.MakeLink(&router, NodeId(1), kTestMSS);
+  auto link_unique =
+      mock_link.MakeLink(&router, NodeId(1), GetParam().make_framer());
   auto* link = link_unique.get();
   router.RegisterLink(std::move(link_unique));
   while (!router.HasRouteTo(NodeId(2))) {
@@ -120,9 +127,19 @@
           .is_ok());
 
   EXPECT_CALL(mock_stream_handler, HandleMessage(_, _, _));
-  link->Process(env.timer()->Now(),
-                Slice::FromContainer({6, 0, 1, 1, 7, 8, 9}));
+  link->Process(env.timer()->Now(), GetParam().framed);
 }
 
+INSTANTIATE_TEST_SUITE_P(
+    StreamLinkSuite, StreamLinkTest,
+    ::testing::Values(
+        TestArg{[] { return std::make_unique<ReliableFramer>(); },
+                Slice::FromContainer({7, 8, 9}),
+                Slice::FromContainer({0x06, 0x00, 6, 0, 1, 1, 7, 8, 9})},
+        TestArg{[] { return std::make_unique<UnreliableFramer>(); },
+                Slice::FromContainer({7, 8, 9}),
+                Slice::FromContainer({0x0a, 6, 6, 0, 1, 1, 7, 8, 9, 0xb8, 0x80,
+                                      0x2a, 0xcf})}));
+
 }  // namespace stream_link_test
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/links/stream_link_untrusted_fuzzer.cc b/src/connectivity/overnet/lib/links/stream_link_untrusted_fuzzer.cc
index 75de983..2696717 100644
--- a/src/connectivity/overnet/lib/links/stream_link_untrusted_fuzzer.cc
+++ b/src/connectivity/overnet/lib/links/stream_link_untrusted_fuzzer.cc
@@ -3,9 +3,12 @@
 // found in the LICENSE file.
 
 #include <fuchsia/overnet/streamlinkfuzzer/cpp/fidl.h>
+
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
 #include "src/connectivity/overnet/lib/links/stream_link.h"
 #include "src/connectivity/overnet/lib/protocol/fidl.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+#include "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
 #include "src/connectivity/overnet/lib/routing/router.h"
 #include "src/connectivity/overnet/lib/testing/test_timer.h"
 
@@ -15,21 +18,22 @@
 
 class FuzzedStreamLink final : public StreamLink {
  public:
-  FuzzedStreamLink(Router* router) : StreamLink(router, NodeId(1), 64, 1) {}
+  FuzzedStreamLink(Router* router, std::unique_ptr<StreamFramer> framer)
+      : StreamLink(router, NodeId(1), std::move(framer), 1) {}
   void Emit(Slice slice, Callback<Status> done) override { abort(); }
 };
 
 class StreamLinkFuzzer {
  public:
-  StreamLinkFuzzer(bool log_stuff)
+  StreamLinkFuzzer(bool log_stuff, std::unique_ptr<StreamFramer> framer)
       : logging_(log_stuff ? new Logging(&timer_) : nullptr) {
-    auto link = MakeLink<FuzzedStreamLink>(&router_);
+    auto link = MakeLink<FuzzedStreamLink>(&router_, std::move(framer));
     link_ = link.get();
     router_.RegisterLink(std::move(link));
   }
 
-  void Run(fuchsia::overnet::streamlinkfuzzer::UntrustedInputPlan plan) {
-    for (const auto& action : plan.input) {
+  void Run(std::vector<std::vector<uint8_t>> plan) {
+    for (const auto& action : plan) {
       link_->Process(timer_.Now(), Slice::FromContainer(action));
       timer_.Step(TimeDelta::FromSeconds(1).as_us());
     }
@@ -47,6 +51,21 @@
   FuzzedStreamLink* link_;
 };
 
+std::unique_ptr<StreamFramer> MakeFramer(
+    const fuchsia::overnet::streamlinkfuzzer::UntrustedLinkDescription& desc) {
+  switch (desc.Which()) {
+    case fuchsia::overnet::streamlinkfuzzer::UntrustedLinkDescription::Tag::
+        Empty:
+      return nullptr;
+    case fuchsia::overnet::streamlinkfuzzer::UntrustedLinkDescription::Tag::
+        kReliable:
+      return std::make_unique<ReliableFramer>();
+    case fuchsia::overnet::streamlinkfuzzer::UntrustedLinkDescription::Tag::
+        kUnreliable:
+      return std::make_unique<UnreliableFramer>();
+  }
+}
+
 }  // namespace
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
@@ -54,7 +73,9 @@
           Decode<fuchsia::overnet::streamlinkfuzzer::UntrustedInputPlan>(
               Slice::FromCopiedBuffer(data, size));
       buffer.is_ok()) {
-    StreamLinkFuzzer(false).Run(std::move(*buffer));
+    if (auto framer = MakeFramer(buffer->link_description)) {
+      StreamLinkFuzzer(false, std::move(framer)).Run(std::move(buffer->input));
+    }
   }
   return 0;
 }
diff --git a/src/connectivity/overnet/lib/packet_protocol/BUILD.gn b/src/connectivity/overnet/lib/packet_protocol/BUILD.gn
index a74bb48..19edcfe 100644
--- a/src/connectivity/overnet/lib/packet_protocol/BUILD.gn
+++ b/src/connectivity/overnet/lib/packet_protocol/BUILD.gn
@@ -29,8 +29,6 @@
   ]
   public_deps = [
     "//third_party/boringssl:crypto",
-  ]
-  deps = [
     ":packet_protocol",
     "//src/connectivity/overnet/lib/protocol:serialization_helpers",
   ]
@@ -87,7 +85,7 @@
     "packet_protocol.cc",
     "packet_protocol.h",
   ]
-  deps = [
+  public_deps = [
     ":bbr",
     "//src/connectivity/overnet/lib/environment:timer",
     "//src/connectivity/overnet/lib/environment:trace",
@@ -100,6 +98,8 @@
     "//src/connectivity/overnet/lib/vocabulary:optional",
     "//src/connectivity/overnet/lib/vocabulary:slice",
     "//src/connectivity/overnet/lib/vocabulary:status",
+    "//src/connectivity/overnet/lib/stats:link",
+    "//zircon/public/lib/fit",
   ]
 }
 
diff --git a/src/connectivity/overnet/lib/packet_protocol/bbr.h b/src/connectivity/overnet/lib/packet_protocol/bbr.h
index 0f4b383..1218a182 100644
--- a/src/connectivity/overnet/lib/packet_protocol/bbr.h
+++ b/src/connectivity/overnet/lib/packet_protocol/bbr.h
@@ -78,6 +78,10 @@
 
   TimeDelta rtt() const { return rtprop_; }
 
+  uint64_t bandwidth_delay_product() const {
+    return bottleneck_bandwidth().BytesSentForTime(rtt());
+  }
+
   // Reporter should have a Put(name, value) method.
   // ... much like CsvWriter, but we don't include that here so that we can keep
   // that code testonly
diff --git a/src/connectivity/overnet/lib/packet_protocol/bdp_estimator.h b/src/connectivity/overnet/lib/packet_protocol/bdp_estimator.h
new file mode 100644
index 0000000..48daa0b
--- /dev/null
+++ b/src/connectivity/overnet/lib/packet_protocol/bdp_estimator.h
@@ -0,0 +1,39 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include <cstdint>
+
+#include "src/connectivity/overnet/lib/packet_protocol/windowed_filter.h"
+
+namespace overnet {
+
+class BdpEstimator {
+ public:
+  BdpEstimator() : filter_(1024, 0, 0) {}
+
+  struct PerPacketData {
+    uint64_t seq;
+    uint64_t bytes_received_at_send;
+  };
+
+  void ReceivedBytes(uint64_t count) { bytes_received_ += count; }
+
+  PerPacketData SentPacket(uint64_t seq) {
+    return PerPacketData{seq, bytes_received_};
+  }
+
+  void AckPacket(PerPacketData data) {
+    filter_.Update(data.seq, bytes_received_ - data.bytes_received_at_send);
+  }
+
+  uint64_t estimate() const { return filter_.best_estimate(); }
+
+ private:
+  uint64_t bytes_received_ = 0;
+  WindowedFilter<uint64_t, uint64_t, MaxFilter> filter_;
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/packet_protocol/packet_protocol.cc b/src/connectivity/overnet/lib/packet_protocol/packet_protocol.cc
index 0515834..80fc2c0 100644
--- a/src/connectivity/overnet/lib/packet_protocol/packet_protocol.cc
+++ b/src/connectivity/overnet/lib/packet_protocol/packet_protocol.cc
@@ -5,6 +5,7 @@
 #include "src/connectivity/overnet/lib/packet_protocol/packet_protocol.h"
 
 #include <iostream>
+#include <sstream>
 
 namespace overnet {
 
@@ -26,10 +27,11 @@
 
 PacketProtocol::PacketProtocol(Timer* timer, RandFunc rand,
                                PacketSender* packet_sender, const Codec* codec,
-                               uint64_t mss)
+                               uint64_t mss, bool probe_tails)
     : codec_(codec),
       timer_(timer),
       packet_sender_(packet_sender),
+      probe_tails_(probe_tails),
       maximum_send_size_(mss) {
   state_.Reset(this, std::move(rand));
 }
@@ -53,42 +55,45 @@
   ScopedModule<PacketProtocol> in_pp(this);
   OVERNET_TRACE(DEBUG) << "Quiesce";
   state_.Reset();
-  master_ref_.Drop();
+  primary_ref_.Drop();
 }
 
 PacketProtocol::Transaction::Transaction(PacketProtocol* protocol)
     : protocol_(protocol) {
+  ScopedModule<PacketProtocol> in_pp(protocol_);
+  OVERNET_TRACE(DEBUG) << "Transaction.Begin";
   assert(protocol_->active_transaction_ == nullptr);
   protocol_->active_transaction_ = this;
 }
 
 PacketProtocol::Transaction::~Transaction() {
+  OVERNET_TRACE(DEBUG) << "Transaction.Finalize";
+  ProtocolRef protocol(protocol_);
   for (;;) {
     if (quiesce_) {
-      assert(protocol_->active_transaction_ == this);
-      protocol_->active_transaction_ = nullptr;
-      protocol_->Quiesce();
-      return;
+      protocol->Quiesce();
+    } else if (protocol->state_.has_value()) {
+      if (bbr_ack_.has_value()) {
+        protocol->state_->bbr.OnAck(bbr_ack_.Take());
+        continue;
+      } else if (start_sending_) {
+        start_sending_ = false;
+        protocol->state_->outstanding_messages.StartSending();
+        continue;
+      } else if (increment_outstanding_tip_) {
+        increment_outstanding_tip_ = false;
+        protocol->state_->outstanding_messages.IncrementTip();
+        continue;
+      }
     }
 
-    if (schedule_send_queue_ && protocol_->state_.has_value()) {
-      schedule_send_queue_ = false;
-      protocol_->state_->send_queue->Schedule();
-      continue;
-    }
-
-    assert(protocol_->active_transaction_ == this);
-    protocol_->active_transaction_ = nullptr;
+    OVERNET_TRACE(DEBUG) << "Transaction.End";
+    assert(protocol->active_transaction_ == this);
+    protocol->active_transaction_ = nullptr;
     return;
   }
 }
 
-void PacketProtocol::Transaction::Send(SendRequestHdl hdl) {
-  if (auto* q = send_queue()) {
-    schedule_send_queue_ |= q->Add(std::move(hdl));
-  }
-}
-
 void PacketProtocol::Transaction::QuiesceOnCompletion(Callback<void> callback) {
   OVERNET_TRACE(DEBUG) << "Schedule Quiesce";
   assert(!quiesce_);
@@ -97,16 +102,6 @@
   quiesce_ = true;
 }
 
-PacketProtocol::SendQueue* PacketProtocol::Transaction::send_queue() {
-  if (quiesce_ || !protocol_->state_.has_value()) {
-    return nullptr;
-  }
-  if (!protocol_->state_->send_queue.has_value()) {
-    protocol_->state_->send_queue.Reset(protocol_);
-  }
-  return protocol_->state_->send_queue.get();
-}
-
 ///////////////////////////////////////////////////////////////////////////////
 // PacketProtocol::Send and friends.
 // Defines the send path.
@@ -115,132 +110,177 @@
   ScopedModule<PacketProtocol> in_pp(this);
   OVERNET_TRACE(DEBUG) << "Send";
   InTransaction([&](Transaction* transaction) {
-    transaction->Send(std::move(send_request));
+    if (!state_.has_value()) {
+      return;
+    }
+    state_->outstanding_messages.Schedule(transaction, std::move(send_request));
   });
 }
 
-bool PacketProtocol::SendQueue::Add(SendRequestHdl hdl) {
-  ScopedModule<PacketProtocol> in_pp(protocol_);
-  OVERNET_TRACE(DEBUG) << "SendQueue.Add";
-  scheduled_tail_loss_probe_.Reset();
-  requests_.push(std::move(hdl));
-  return !scheduled_;
+void PacketProtocol::OutstandingMessages::Schedule(Transaction* transaction,
+                                                   SendRequestHdl hdl) {
+  Schedule(transaction, OutstandingPacket::Pending{std::move(hdl)});
 }
 
-void PacketProtocol::SendQueue::Schedule() {
+std::string PacketProtocol::OutstandingMessages::OutstandingString() const {
+  std::ostringstream out;
+  out << "{tip=" << send_tip_ << ":" << unsent_tip_ << "|";
+  bool first = true;
+  for (const auto& pkt : outstanding_) {
+    if (!first) {
+      out << ",";
+    }
+    first = false;
+    out << pkt.state;
+  }
+  out << "}";
+  return out.str();
+}
+
+void PacketProtocol::OutstandingMessages::Schedule(
+    Transaction* transaction,
+    OutstandingPacket::State outstanding_packet_state) {
+  OVERNET_TRACE(DEBUG) << "OutstandingMessages.Schedule "
+                       << outstanding_packet_state
+                       << " outstanding=" << OutstandingString();
+  tail_probe_timeout_.Reset();
+  auto make_packet = [&] {
+    return OutstandingPacket{
+        protocol_->timer_->Now(),
+        protocol_->state_->received_queue.max_seen_sequence(),
+        std::move(outstanding_packet_state)};
+  };
+  if (!outstanding_.empty() &&
+      std::holds_alternative<OutstandingPacket::PendingTailProbe>(
+          outstanding_.back().state)) {
+    if (!std::holds_alternative<OutstandingPacket::PendingTailProbe>(
+            outstanding_packet_state)) {
+      outstanding_.back() = make_packet();
+    }
+  } else {
+    if (unsent_tip_ - send_tip_ == outstanding_.size()) {
+      transaction->StartSendingOnCompletion();
+    }
+    outstanding_.emplace_back(make_packet());
+  }
+  ScheduleRetransmit();
+  max_outstanding_size_ =
+      std::max(outstanding_.size(), size_t(max_outstanding_size_));
+}
+
+void PacketProtocol::OutstandingMessages::StartSending() {
   ScopedModule<PacketProtocol> in_pp(protocol_);
-  OVERNET_TRACE(DEBUG) << "SendQueue.Schedule";
-  assert(!scheduled_);
-  scheduled_ = true;
-  assert(!transmit_request_.has_value());
+  OVERNET_TRACE(DEBUG) << "OutstandingMessages.StartSending: "
+                       << OutstandingString();
+
+  assert(!outstanding_.empty());
+  assert(unsent_tip_ >= send_tip_);
+  assert(unsent_tip_ - send_tip_ < outstanding_.size());
+
   transmit_request_.Reset(
-      &protocol_->state_->bbr_, [this](const Status& status) {
-        ScopedModule<PacketProtocol> scoped_module(protocol_);
-        OVERNET_TRACE(DEBUG) << "SendQueue.Schedule --> " << status
-                             << " requests=" << requests_.size();
-        auto transmit_request = transmit_request_.Take();
+      &protocol_->state_->bbr, [this](const Status& status) {
+        OVERNET_TRACE(DEBUG) << "OutstandingMessages.Send status=" << status;
         if (status.is_error()) {
           return;
         }
-        SendRequestHdl request;
-        if (requests_.empty()) {
-          last_send_was_tail_loss_probe_ = true;
-          protocol_->state_->outstanding_messages.Send(
-              std::move(transmit_request), SendRequestHdl(&ack_send_request));
-        } else {
-          last_send_was_tail_loss_probe_ = false;
-          request = std::move(requests_.front());
-          requests_.pop();
-          protocol_->state_->outstanding_messages.Send(
-              std::move(transmit_request), std::move(request));
-        }
+        SeqNum seq_num(unsent_tip_, max_outstanding_size_ + 1);
+        protocol_->packet_sender_->SendPacket(
+            seq_num, PacketSend(protocol_, transmit_request_.Take()));
       });
 }
 
-void PacketProtocol::OutstandingMessages::Send(BBR::TransmitRequest bbr_request,
-                                               SendRequestHdl request) {
-  assert(protocol_->state_);
-
-  const uint64_t seq_idx = send_tip_ + outstanding_.size();
-  OVERNET_TRACE(DEBUG) << "OutstandingMessages.Send seq_idx=" << seq_idx;
-  SeqNum seq_num(seq_idx, max_outstanding_size_ + 1);
-
-  outstanding_.emplace_back(
-      OutstandingPacket{OutstandingPacketState::PENDING,
-                        protocol_->state_->received_queue.max_seen_sequence(),
-                        Nothing, std::move(request)});
-
-  max_outstanding_size_ =
-      std::max(outstanding_.size(), size_t(max_outstanding_size_));
-
-  protocol_->packet_sender_->SendPacket(
-      seq_num, PacketSend(protocol_, seq_idx, std::move(bbr_request)));
-}
-
 PacketProtocol::PacketSend::PacketSend(PacketProtocol* protocol,
-                                       uint64_t seq_idx,
                                        BBR::TransmitRequest bbr_request)
-    : protocol_(protocol),
-      seq_idx_(seq_idx),
-      bbr_request_(std::move(bbr_request)) {
+    : protocol_(protocol), bbr_request_(std::move(bbr_request)) {
   assert(protocol);
 }
 
 Slice PacketProtocol::PacketSend::operator()(LazySliceArgs args) {
   auto protocol = std::move(protocol_);
-  auto slice =
-      protocol->InTransaction([=, protocol = protocol.get()](Transaction* t) {
+  protocol->stats_.outgoing_packet_count++;
+  return protocol->InTransaction(
+      [=, protocol = protocol.get()](Transaction* transaction) {
         ScopedModule<PacketProtocol> in_pp(protocol);
+        Slice output;
         if (protocol->state_.has_value()) {
-          return protocol->state_->outstanding_messages.GeneratePacket(
-              std::move(bbr_request_), seq_idx_, args);
-        } else {
-          return Slice();
+          output = protocol->state_->outstanding_messages.GeneratePacket(
+              transaction, std::move(bbr_request_), args);
         }
+        if (protocol->state_.has_value()) {
+          protocol->state_->outstanding_messages.SentMessage(transaction);
+        }
+        return output;
       });
-  if (protocol->state_.has_value()) {
-    protocol->state_->send_queue->SentMessage();
-  }
-  return slice;
 }
 
 PacketProtocol::PacketSend::~PacketSend() {
   if (protocol_.has_value() && protocol_->state_.has_value()) {
-    protocol_->state_->outstanding_messages.CancelPacket(seq_idx_);
+    protocol_->InTransaction([this](Transaction* transaction) {
+      protocol_->state_->outstanding_messages.CancelledMessage(transaction);
+    });
   }
 }
 
-void PacketProtocol::OutstandingMessages::CancelPacket(uint64_t seq_idx) {
-  protocol_->state_->send_queue->SentMessage();
-  AckProcessor(this, TimeDelta::Zero()).Nack(seq_idx, Status::Cancelled());
+void PacketProtocol::OutstandingMessages::CancelledMessage(
+    Transaction* transaction) {
+  SentMessage(transaction);
 }
 
 Slice PacketProtocol::OutstandingMessages::GeneratePacket(
-    BBR::TransmitRequest bbr_request, uint64_t seq_idx, LazySliceArgs args) {
-  if (seq_idx < send_tip_) {
-    // Frame was nacked before sending (probably due to shutdown).
-    OVERNET_TRACE(DEBUG) << "Seq " << seq_idx << " nacked before sending";
+    Transaction* transaction, BBR::TransmitRequest bbr_request,
+    LazySliceArgs args) {
+  if (unsent_tip_ - send_tip_ == outstanding_.size()) {
+    // nack before send?
+    OVERNET_TRACE(DEBUG) << "Seq " << unsent_tip_ << " nacked before sending";
     return Slice();
   }
 
+  const uint64_t seq_idx = unsent_tip_;
   auto* outstanding_packet = &outstanding_[seq_idx - send_tip_];
-  auto send = protocol_->codec_->Encode(
-      seq_idx, protocol_->FormatPacket(
-                   seq_idx, outstanding_packet->request.borrow(), args));
-  if (send.is_error()) {
-    OVERNET_TRACE(ERROR) << "Failed to encode packet: " << send.AsStatus();
+
+  OVERNET_TRACE(DEBUG) << "GeneratePacket: " << outstanding_packet->state;
+
+  SendRequestHdl request;
+  Slice send;
+
+  if (std::holds_alternative<OutstandingPacket::Pending>(
+          outstanding_packet->state)) {
+    auto& pending_packet =
+        std::get<OutstandingPacket::Pending>(outstanding_packet->state);
+    request = std::move(pending_packet.request);
+    last_send_was_tail_probe_ = false;
+  } else if (std::holds_alternative<OutstandingPacket::PendingTailProbe>(
+                 outstanding_packet->state)) {
+    request = SendRequestHdl(&ack_send_request);
+    last_send_was_tail_probe_ = true;
+  } else {
+    abort();
   }
+
+  auto send_status = protocol_->codec_->Encode(
+      seq_idx,
+      protocol_->FormatPacket(transaction, seq_idx, request.borrow(), args));
+  if (send_status.is_error()) {
+    OVERNET_TRACE(ERROR) << "Failed to encode packet: "
+                         << send_status.AsStatus();
+  } else {
+    send = std::move(*send_status);
+  }
+
   // outstanding_ should not have changed
+  assert(unsent_tip_ == seq_idx);
   assert(outstanding_packet == &outstanding_[seq_idx - send_tip_]);
-  outstanding_packet->state = OutstandingPacketState::SENT;
-  outstanding_packet->bbr_sent_packet =
-      bbr_request.Sent(BBR::OutgoingPacket{seq_idx, send->length()});
+  outstanding_packet->state = OutstandingPacket::Sent{
+      std::move(request),
+      bbr_request.Sent(BBR::OutgoingPacket{seq_idx, send.length()}),
+      protocol_->state_->bdp_estimator.SentPacket(seq_idx)};
+  unsent_tip_++;
   ScheduleRetransmit();
-  return std::move(*send);
+  return send;
 }
 
-Slice PacketProtocol::FormatPacket(uint64_t seq_idx, SendRequest* request,
-                                   LazySliceArgs args) {
+Slice PacketProtocol::FormatPacket(Transaction* transaction, uint64_t seq_idx,
+                                   SendRequest* request, LazySliceArgs args) {
   assert(state_.has_value());
 
   const auto max_length = std::min(static_cast<uint64_t>(args.max_length),
@@ -255,21 +295,29 @@
                          maxlen, has_other_content || args.has_other_content};
   };
 
+  OVERNET_TRACE(DEBUG) << "FormatPacket: can_build_ack="
+                       << state_->received_queue.CanBuildAck()
+                       << " request.must_send_ack=" << request->must_send_ack()
+                       << " should_send_ack="
+                       << state_->ack_sender.ShouldSendAck();
+
   if (state_->received_queue.CanBuildAck() &&
       (request->must_send_ack() || state_->ack_sender.ShouldSendAck())) {
+    stats_.acks_sent++;
     auto ack = state_->received_queue.BuildAck(
-        seq_idx, timer_->Now(), varint::MaximumLengthWithPrefix(max_length),
-        &state_->ack_sender);
+        transaction, seq_idx, timer_->Now(),
+        varint::MaximumLengthWithPrefix(max_length), &state_->ack_sender);
     AckFrame::Writer ack_writer(&ack);
     const uint8_t ack_length_length =
         varint::WireSizeFor(ack_writer.wire_length());
     const uint64_t prefix_length = ack_length_length + ack_writer.wire_length();
-    return request->GenerateBytes(make_args(prefix_length, true))
-        .WithPrefix(prefix_length,
-                    [&ack_writer, ack_length_length](uint8_t* p) {
-                      ack_writer.Write(varint::Write(ack_writer.wire_length(),
-                                                     ack_length_length, p));
-                    });
+    auto payload = request->GenerateBytes(make_args(prefix_length, true));
+    stats_.pure_acks_sent += payload.length() == 0;
+    return std::move(payload).WithPrefix(
+        prefix_length, [&ack_writer, ack_length_length](uint8_t* p) {
+          ack_writer.Write(
+              varint::Write(ack_writer.wire_length(), ack_length_length, p));
+        });
   } else {
     return request->GenerateBytes(make_args(1, false))
         .WithPrefix(1, [](uint8_t* p) { *p = 0; });
@@ -287,7 +335,8 @@
   return received_tip_ + idx;
 }
 
-AckFrame PacketProtocol::ReceivedQueue::BuildAck(uint64_t seq_idx,
+AckFrame PacketProtocol::ReceivedQueue::BuildAck(Transaction* transaction,
+                                                 uint64_t seq_idx,
                                                  TimeStamp now,
                                                  uint32_t max_length,
                                                  AckSender* ack_sender) {
@@ -311,18 +360,21 @@
         OVERNET_TRACE(DEBUG)
             << "Mark unseen packet " << seq_idx << " as NOT_RECEIVED";
         received_packet = ReceivedPacket{ReceiveState::NOT_RECEIVED, now};
+        stats_->unseen_packets_marked_not_received++;
         [[fallthrough]];
       case ReceiveState::NOT_RECEIVED:
         ack.AddNack(seq_idx);
         break;
       case ReceiveState::RECEIVED:
+      case ReceiveState::RECEIVED_PURE_ACK:
+      case ReceiveState::RECEIVED_AND_ACKED_IMMEDIATELY:
         break;
     }
   }
 
   ack.AdjustForMSS(max_length, packet_delay);
 
-  ack_sender->AckSent(seq_idx, ack.partial());
+  ack_sender->AckSent(transaction, seq_idx, ack.partial());
 
   OVERNET_TRACE(DEBUG) << "BuildAck generates: " << ack << " bytes="
                        << Slice::FromWriters(AckFrame::Writer(&ack));
@@ -330,62 +382,53 @@
   return ack;
 }
 
-void PacketProtocol::SendQueue::SentMessage() {
+void PacketProtocol::OutstandingMessages::SentMessage(
+    Transaction* transaction) {
   ScopedModule<PacketProtocol> in_pp(protocol_);
-  OVERNET_TRACE(DEBUG) << "SendQueue.SentMessage: requests=" << requests_.size()
-                       << " scheduled_tail_loss_probe="
-                       << scheduled_tail_loss_probe_.Map(
-                              [](const ScheduledTailLossProbe& tlp) {
-                                return tlp.when;
-                              });
-  assert(scheduled_);
-  scheduled_ = false;
-  protocol_->InTransaction([this](Transaction* t) {
-    if (!requests_.empty()) {
-      Schedule();
-      return;
-    }
-    if (!last_send_was_tail_loss_probe_ && !t->Closing()) {
-      ScheduleTailLossProbe();
-      return;
-    }
-    protocol_->state_->send_queue.Reset();
-  });
+  OVERNET_TRACE(DEBUG) << "OutstandingMessages.SentMessage: "
+                       << OutstandingString();
+  if (unsent_tip_ - send_tip_ != outstanding_.size()) {
+    transaction->StartSendingOnCompletion();
+  }
+  if (protocol_->probe_tails_ && !last_send_was_tail_probe_) {
+    ScheduleAck();
+  }
 }
 
-void PacketProtocol::SendQueue::ScheduleTailLossProbe() {
+void PacketProtocol::OutstandingMessages::ScheduleAck() {
+  ScopedModule<PacketProtocol> in_pp(protocol_);
   const auto when = protocol_->timer_->Now() + protocol_->TailLossProbeDelay();
-  OVERNET_TRACE(DEBUG) << "SendQueue.ScheduleTailLossProbe: requests="
-                       << requests_.size() << " scheduled_tail_loss_probe="
-                       << scheduled_tail_loss_probe_.Map(
-                              [](const ScheduledTailLossProbe& tlp) {
-                                return tlp.when;
-                              })
-                       << " when=" << when;
-  if (!requests_.empty()) {
+  OVERNET_TRACE(DEBUG) << "OutstandingMessages.ScheduleTailLossProbe";
+  if (unsent_tip_ - send_tip_ != outstanding_.size()) {
+    protocol_->stats_
+        .tail_loss_probes_cancelled_because_requests_already_queued++;
     return;
   }
-  if (scheduled_tail_loss_probe_.has_value() &&
-      scheduled_tail_loss_probe_->when <= when) {
+  if (tail_probe_timeout_.has_value() && tail_probe_timeout_->when <= when) {
+    protocol_->stats_
+        .tail_loss_probes_cancelled_because_probe_already_scheduled++;
     return;
   }
-  scheduled_tail_loss_probe_.Reset(
+  tail_probe_timeout_.Reset(
       protocol_->timer_, when, [this](const Status& status) {
         ScopedModule<PacketProtocol> in_pp(protocol_);
-        OVERNET_TRACE(DEBUG) << "SendQueue.ScheduleTailLossProbe --> " << status
-                             << " requests=" << requests_.size();
+        OVERNET_TRACE(DEBUG)
+            << "OutstandingMessages.ScheduleTailLossProbe --> " << status;
         if (status.is_error()) {
+          protocol_->stats_.tail_loss_probes_cancelled_after_timer_created++;
           return;
         }
-        scheduled_tail_loss_probe_.Reset();
-        ForceSendAck();
+        tail_probe_timeout_.Reset();
+        protocol_->stats_.tail_loss_probes_scheduled++;
+        protocol_->InTransaction(
+            [this](Transaction* transaction) { ForceSendAck(transaction); });
       });
 }
 
-void PacketProtocol::SendQueue::ForceSendAck() {
-  if (!scheduled_) {
-    protocol_->InTransaction([](Transaction* t) { t->ScheduleForcedAck(); });
-  }
+void PacketProtocol::OutstandingMessages::ForceSendAck(
+    Transaction* transaction) {
+  OVERNET_TRACE(DEBUG) << "OutstandingMessages.ForceSendAck";
+  Schedule(transaction, OutstandingPacket::PendingTailProbe{});
 }
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -394,18 +437,23 @@
 
 void PacketProtocol::Process(TimeStamp received, SeqNum seq_num, Slice slice,
                              ProcessCallback handle_message) {
+  assert(refs_);
   ScopedModule<PacketProtocol> scoped_module(this);
   Transaction transaction(this);
 
   OVERNET_TRACE(DEBUG) << "Process: " << slice;
 
+  stats_.incoming_packet_count++;
+
   if (!state_.has_value()) {
     return;
   }
 
+  state_->bdp_estimator.ReceivedBytes(slice.length());
   state_->ack_sender.NeedAck(
+      &transaction,
       state_->received_queue.Received(seq_num, received, [&](uint64_t seq_idx) {
-        return ProcessMessage(seq_idx, std::move(slice), received,
+        return ProcessMessage(&transaction, seq_idx, std::move(slice), received,
                               std::move(handle_message));
       }));
   state_->outstanding_messages.ReceivedPacket();
@@ -420,6 +468,7 @@
   OVERNET_TRACE(DEBUG) << "Process seq:" << seq_idx;
 
   if (seq_idx < received_tip_) {
+    stats_->ack_not_required_historic_sequence++;
     return AckUrgency::NOT_REQUIRED;
   }
 
@@ -430,6 +479,7 @@
   auto* received_packet = &received_packets_[seq_idx - received_tip_];
   if (received_packet->state != ReceiveState::UNKNOWN) {
     OVERNET_TRACE(DEBUG) << "frozen as " << received_packet->state;
+    stats_->ack_not_required_frozen_sequence++;
     return AckUrgency::NOT_REQUIRED;
   }
 
@@ -449,43 +499,52 @@
   switch (pmr) {
     case ProcessMessageResult::NOT_PROCESSED:
       // Failed processing packets shouldn't generate traffic.
+      stats_->ack_not_required_invalid_packet++;
       return AckUrgency::NOT_REQUIRED;
     case ProcessMessageResult::NACK:
       optional_ack_run_length_ = 0;
       *received_packet = ReceivedPacket{ReceiveState::NOT_RECEIVED, received};
+      stats_->ack_required_immediately_due_to_nack++;
       // Always send a nack as soon as we realize one is necessary.
       return AckUrgency::SEND_IMMEDIATELY;
     case ProcessMessageResult::OPTIONAL_ACK:
       // If we get a single ack without a payload, we suppress sending a reply
       // ack.
       optional_ack_run_length_++;
+      *received_packet =
+          ReceivedPacket{ReceiveState::RECEIVED_PURE_ACK, received};
       if (optional_ack_run_length_ < 5) {
-        *received_packet = ReceivedPacket{ReceiveState::RECEIVED, received};
+        stats_->ack_not_required_short_optional_run++;
         return AckUrgency::NOT_REQUIRED;
       }
-      [[fallthrough]];
+      stats_->ack_required_soon_ack_received++;
+      optional_ack_run_length_ = 0;
+      return AckUrgency::SEND_SOON;
     case ProcessMessageResult::ACK: {
       optional_ack_run_length_ = 0;
       *received_packet = ReceivedPacket{ReceiveState::RECEIVED, received};
-      int num_received = 0;
-      for (const auto& pkt : received_packets_) {
-        switch (pkt.state) {
-          case ReceiveState::RECEIVED:
-            num_received++;
-            if (num_received >= 3) {
-              return AckUrgency::SEND_IMMEDIATELY;
-            }
-            break;
-          default:
-            break;
+      static const uint64_t kMaxIncomingBeforeForcedAck = 3;
+      if (seq_idx - received_tip_ >= kMaxIncomingBeforeForcedAck) {
+        for (auto idx = seq_idx - received_tip_ - kMaxIncomingBeforeForcedAck;
+             idx < seq_idx - received_tip_; idx++) {
+          if (received_packets_[idx].state != ReceiveState::RECEIVED) {
+            goto dont_send_immediately;
+          }
         }
+        *received_packet = ReceivedPacket{
+            ReceiveState::RECEIVED_AND_ACKED_IMMEDIATELY, received};
+        stats_->ack_required_immediately_due_to_multiple_receives++;
+        return AckUrgency::SEND_IMMEDIATELY;
       }
+    dont_send_immediately:
       // Got some data, make sure there's an ack scheduled soon.
+      stats_->ack_required_soon_data_received++;
       return AckUrgency::SEND_SOON;
     }
     case ProcessMessageResult::ACK_URGENTLY: {
       optional_ack_run_length_ = 0;
       *received_packet = ReceivedPacket{ReceiveState::RECEIVED, received};
+      stats_->ack_required_immediately_due_to_partial_ack++;
       return AckUrgency::SEND_IMMEDIATELY;
     }
   }
@@ -507,7 +566,7 @@
 }
 
 PacketProtocol::ProcessMessageResult PacketProtocol::ProcessMessage(
-    uint64_t seq_idx, Slice slice, TimeStamp received,
+    Transaction* transaction, uint64_t seq_idx, Slice slice, TimeStamp received,
     ProcessCallback handle_message) {
   using StatusType = StatusOr<IncomingMessage*>;
 
@@ -585,7 +644,8 @@
     handle_message(nullptr);
   }
   if (ack.has_value()) {
-    state_->outstanding_messages.ProcessValidAck(ack.Take(), received);
+    state_->outstanding_messages.ProcessValidAck(transaction, ack.Take(),
+                                                 received);
   }
   return ack_result;
 }
@@ -607,22 +667,23 @@
       continue;
     }
     const OutstandingPacket& pkt = outstanding_[nack_seq - send_tip_];
-    if (pkt.state == OutstandingPacketState::ACKED) {
+    if (std::holds_alternative<OutstandingPacket::Acked>(pkt.state)) {
       // Previously acked packet becomes nacked: this is an error.
       return Status(StatusCode::INVALID_ARGUMENT,
                     "Previously acked packet becomes nacked");
     }
   }
   for (size_t i = 0; i < ack.ack_to_seq() - send_tip_; i++) {
-    if (!outstanding_[i].bbr_sent_packet.has_value()) {
+    if (std::holds_alternative<OutstandingPacket::Pending>(
+            outstanding_[i].state)) {
       return Status(StatusCode::INVALID_ARGUMENT, "Ack/nack unsent sequence");
     }
   }
   return Status::Ok();
 }
 
-void PacketProtocol::OutstandingMessages::ProcessValidAck(AckFrame ack,
-                                                          TimeStamp received) {
+void PacketProtocol::OutstandingMessages::ProcessValidAck(
+    Transaction* transaction, AckFrame ack, TimeStamp received) {
   // Basic validation. Can assert anything that should be an error because
   // ValidateAck should have been called prior.
   if (ack.ack_to_seq() < send_tip_) {
@@ -638,98 +699,88 @@
                                              received);
   }
 
-  AckProcessor ack_processor(this,
-                             TimeDelta::FromMicroseconds(ack.ack_delay_us()));
+  const auto queue_delay = TimeDelta::FromMicroseconds(ack.ack_delay_us());
 
   // Fail any nacked packets.
   // Iteration is from oldest packet to newest, such that the OLDEST nacked
   // message is the most likely to be sent first. This has the important
   // consequence that if the packet was a fragment of a large message that was
-  // rejected due to buffering, the earlier pieces (that are more likely to fit)
-  // are retransmitted first.
+  // rejected due to buffering, the earlier pieces (that are more likely to
+  // fit) are retransmitted first.
   for (auto nack_seq : ack.nack_seqs()) {
-    ack_processor.Nack(nack_seq, Status::Unavailable());
+    Nack(transaction, nack_seq, queue_delay, Status::Unavailable());
   }
 
   // Clear out outstanding packet references, propagating acks.
   for (size_t i = send_tip_; i <= ack.ack_to_seq(); i++) {
     OutstandingPacket& pkt = outstanding_[i - send_tip_];
-    if (!pkt.request.empty()) {
-      ack_processor.Ack(i);
+    if (std::holds_alternative<OutstandingPacket::Sent>(pkt.state)) {
+      Ack(transaction, i, queue_delay);
     }
   }
 }
 
-void PacketProtocol::OutstandingMessages::AckProcessor::Ack(uint64_t ack_seq) {
-  OutstandingPacket& pkt =
-      outstanding_->outstanding_[ack_seq - outstanding_->send_tip_];
-  auto request = std::move(pkt.request);
-  if (!request.empty()) {
-    assert(pkt.state == OutstandingPacketState::SENT);
-    pkt.state = OutstandingPacketState::ACKED;
-    bbr_ack_.acked_packets.push_back(*pkt.bbr_sent_packet);
-    request.Ack(Status::Ok());
-  }
+void PacketProtocol::OutstandingMessages::Ack(Transaction* transaction,
+                                              uint64_t ack_seq,
+                                              TimeDelta queue_delay) {
+  OutstandingPacket& pkt = outstanding_[ack_seq - send_tip_];
+  auto& sent_packet = std::get<OutstandingPacket::Sent>(pkt.state);
+  auto request = std::move(sent_packet.request);
+  auto& bbr_sent_packet = sent_packet.bbr_sent_packet;
+  bbr_sent_packet.send_time += queue_delay;
+  transaction->QueueAck(bbr_sent_packet);
+  protocol_->state_->bdp_estimator.AckPacket(sent_packet.bdp_packet);
+  pkt.state = OutstandingPacket::Acked{};
+  transaction->IncrementOutstandingTipOnCompletion();
+  request.Ack(Status::Ok());
 }
 
-void PacketProtocol::OutstandingMessages::AckProcessor::Nack(
-    uint64_t nack_seq, const Status& status) {
+void PacketProtocol::OutstandingMessages::Nack(Transaction* transaction,
+                                               uint64_t nack_seq,
+                                               TimeDelta queue_delay,
+                                               const Status& status) {
   OVERNET_TRACE(DEBUG) << "AckProcessor.Nack: seq=" << nack_seq
-                       << " status=" << status
-                       << " send_tip=" << outstanding_->send_tip_;
+                       << " status=" << status << " send_tip=" << send_tip_;
   assert(status.is_error());
-  if (outstanding_->protocol_->state_.has_value()) {
-    outstanding_->protocol_->state_->ack_sender.OnNack(nack_seq);
+  if (protocol_->state_.has_value()) {
+    protocol_->state_->ack_sender.OnNack(
+        transaction, nack_seq, status.code() == StatusCode::CANCELLED);
   }
-  if (nack_seq < outstanding_->send_tip_) {
+  if (nack_seq < send_tip_) {
     return;
   }
-  OutstandingPacket& pkt =
-      outstanding_->outstanding_[nack_seq - outstanding_->send_tip_];
-  auto request = std::move(pkt.request);
+  OutstandingPacket& pkt = outstanding_[nack_seq - send_tip_];
   OVERNET_TRACE(DEBUG) << "AckProcessor.Nack: seq=" << nack_seq
-                       << " has_request=" << !request.empty()
                        << " state=" << pkt.state;
-  if (request.empty()) {
-    return;
+  if (std::holds_alternative<OutstandingPacket::Pending>(pkt.state) ||
+      std::holds_alternative<OutstandingPacket::PendingTailProbe>(pkt.state)) {
+    pkt.state = OutstandingPacket::Nacked{};
+    transaction->IncrementOutstandingTipOnCompletion();
+  } else if (auto* sent_packet =
+                 std::get_if<OutstandingPacket::Sent>(&pkt.state)) {
+    assert(sent_packet->bbr_sent_packet.outgoing.sequence == nack_seq);
+    auto& bbr_sent_packet = sent_packet->bbr_sent_packet;
+    bbr_sent_packet.send_time += queue_delay;
+    transaction->QueueNack(bbr_sent_packet);
+    auto request = std::move(sent_packet->request);
+    pkt.state = OutstandingPacket::Nacked{};
+    transaction->IncrementOutstandingTipOnCompletion();
+    request.Ack(status);
+  } else if (std::holds_alternative<OutstandingPacket::Nacked>(pkt.state)) {
+  } else {
+    // Previously acked packet becomes nacked: this is an error that should be
+    // diagnosed during validation.
+    abort();
   }
-  switch (pkt.state) {
-    case OutstandingPacketState::PENDING:
-      pkt.state = OutstandingPacketState::NACKED;
-      break;
-    case OutstandingPacketState::SENT:
-      assert(pkt.bbr_sent_packet.has_value());
-      assert(pkt.bbr_sent_packet->outgoing.sequence == nack_seq);
-      bbr_ack_.nacked_packets.push_back(*pkt.bbr_sent_packet);
-      pkt.state = OutstandingPacketState::NACKED;
-      break;
-    case OutstandingPacketState::NACKED:
-      break;
-    default:
-      // Previously acked packet becomes nacked: this is an error.
-      abort();
-  }
-  request.Ack(status);
 }
 
-PacketProtocol::OutstandingMessages::AckProcessor::~AckProcessor() {
-  bool empty = true;
-  // Offset send_time to account for queuing delay on peer.
-  for (auto& pkt : bbr_ack_.acked_packets) {
-    empty = false;
-    pkt.send_time = pkt.send_time + queue_delay_;
+void PacketProtocol::OutstandingMessages::IncrementTip() {
+  while (!outstanding_.empty() && outstanding_.front().is_finalized()) {
+    send_tip_++;
+    outstanding_.pop_front();
   }
-  for (auto& pkt : bbr_ack_.nacked_packets) {
-    empty = false;
-    pkt.send_time = pkt.send_time + queue_delay_;
-  }
-  if (!empty && outstanding_->protocol_->state_.has_value()) {
-    outstanding_->protocol_->state_->bbr_.OnAck(bbr_ack_);
-  }
-  while (!outstanding_->outstanding_.empty() &&
-         outstanding_->outstanding_.front().request.empty()) {
-    outstanding_->send_tip_++;
-    outstanding_->outstanding_.pop_front();
+  if (unsent_tip_ < send_tip_) {
+    unsent_tip_ = send_tip_;
   }
 }
 
@@ -765,40 +816,57 @@
   return "{" + out.str() + "}";
 }
 
-void PacketProtocol::AckSender::NeedAck(AckUrgency urgency) {
+void PacketProtocol::AckSender::NeedAck(Transaction* transaction,
+                                        AckUrgency urgency) {
+  OVERNET_TRACE(DEBUG) << "AckSender.NeedAck"
+                       << " urgency=" << urgency << " (from " << urgency_ << ")"
+                       << " all_acks_acknowledged=" << all_acks_acknowledged_
+                       << " sent_full_acks=" << SentFullAcksString();
+
   if (urgency <= urgency_) {
     return;
   }
 
-  OVERNET_TRACE(DEBUG) << "AckSender.NeedAck"
-                       << " urgency=" << urgency
-                       << " all_acks_acknowledged=" << all_acks_acknowledged_
-                       << " sent_full_acks=" << SentFullAcksString();
   urgency_ = urgency;
+  assert(protocol_->state_.has_value());
   sent_full_acks_.clear();
   all_acks_acknowledged_ = false;
-  assert(protocol_->state_.has_value());
-  if (!protocol_->state_->send_queue.has_value()) {
-    protocol_->state_->send_queue.Reset(protocol_);
-  }
   switch (urgency_) {
     case AckUrgency::NOT_REQUIRED:
       abort();
-    case AckUrgency::SEND_SOON:
-      protocol_->state_->send_queue->ScheduleTailLossProbe();
-      break;
+    case AckUrgency::SEND_SOON: {
+      const auto when =
+          protocol_->timer_->Now() + protocol_->TailLossProbeDelay();
+      OVERNET_TRACE(DEBUG) << "AckSender.NeedAck: schedule ack start for "
+                           << when;
+      suppress_need_acks_ = true;
+      send_ack_timer_.Reset(protocol_->timer_, when, [this](const Status& status) {
+        OVERNET_TRACE(DEBUG) << "AckSender.NeedAck: ack start --> " << status;
+        if (status.is_error()) {
+          return;
+        }
+        suppress_need_acks_ = false;
+        protocol_->stats_
+            .tail_loss_probe_scheduled_because_ack_required_soon_timer_expired++;
+        protocol_->state_->outstanding_messages.ScheduleAck();
+      });
+    } break;
     case AckUrgency::SEND_IMMEDIATELY:
-      protocol_->state_->send_queue->ForceSendAck();
+      suppress_need_acks_ = false;
+      send_ack_timer_.Reset();
+      protocol_->state_->outstanding_messages.ForceSendAck(transaction);
       break;
   }
 }
 
-void PacketProtocol::AckSender::AckSent(uint64_t seq_idx, bool partial) {
+void PacketProtocol::AckSender::AckSent(Transaction* transaction,
+                                        uint64_t seq_idx, bool partial) {
   OVERNET_TRACE(DEBUG) << "AckSender.AckSent seq_idx=" << seq_idx
                        << " partial=" << partial
                        << " all_acks_acknowledged=" << all_acks_acknowledged_
                        << " sent_full_acks=" << SentFullAcksString();
 
+  send_ack_timer_.Reset();
   if (!sent_full_acks_.empty()) {
     assert(seq_idx > sent_full_acks_.back());
   }
@@ -806,11 +874,13 @@
   if (!partial) {
     sent_full_acks_.push_back(seq_idx);
   } else if (sent_full_acks_.empty()) {
-    NeedAck(AckUrgency::SEND_SOON);
+    protocol_->stats_.ack_required_soon_continue_partial_after_ack++;
+    NeedAck(transaction, AckUrgency::SEND_SOON);
   }
 }
 
-void PacketProtocol::AckSender::OnNack(uint64_t seq) {
+void PacketProtocol::AckSender::OnNack(Transaction* transaction, uint64_t seq,
+                                       bool shutting_down) {
   OVERNET_TRACE(DEBUG) << "AckSender.OnNack"
                        << " sent_full_acks=" << SentFullAcksString()
                        << " seq=" << seq
@@ -821,8 +891,9 @@
     return;
   }
   sent_full_acks_.erase(it);
-  if (sent_full_acks_.empty()) {
-    NeedAck(AckUrgency::SEND_SOON);
+  if (sent_full_acks_.empty() && !shutting_down) {
+    protocol_->stats_.ack_required_soon_all_acks_nacked++;
+    NeedAck(transaction, AckUrgency::SEND_SOON);
   }
 }
 
@@ -845,13 +916,13 @@
 
 void PacketProtocol::OutstandingMessages::ScheduleRetransmit() {
   OVERNET_TRACE(DEBUG) << "OutstandingMessages.ScheduleRetransmit: rto_timer="
-                       << rto_timer_.has_value()
+                       << retransmit_timeout_.has_value()
                        << " deadline=" << RetransmitDeadline();
-  if (rto_timer_.has_value()) {
+  if (retransmit_timeout_.has_value()) {
     return;
   }
   if (auto timeout = RetransmitDeadline(); timeout.has_value()) {
-    rto_timer_.Reset(
+    retransmit_timeout_.Reset(
         protocol_->timer_, *timeout,
         [protocol = protocol_](const Status& status) {
           ScopedModule in_pp(protocol);
@@ -863,68 +934,70 @@
               return;
             }
             if (status.is_error()) {
-              protocol->state_->outstanding_messages.NackAll();
+              protocol->state_->outstanding_messages.NackAll(transaction);
               return;
             }
-            protocol->state_->outstanding_messages.CheckRetransmit();
+            protocol->state_->outstanding_messages.CheckRetransmit(transaction);
           });
         });
   }
 }
 
 Optional<TimeStamp> PacketProtocol::OutstandingMessages::RetransmitDeadline() {
+  OVERNET_TRACE(DEBUG) << "OutstandingMessages.RetransmitDeadline: "
+                       << OutstandingString();
   for (const auto& outstanding : outstanding_) {
-    if (outstanding.bbr_sent_packet.has_value() &&
-        outstanding.state == OutstandingPacketState::SENT) {
-      return outstanding.bbr_sent_packet->send_time +
-             protocol_->RetransmitDelay();
+    if (!outstanding.is_finalized()) {
+      return outstanding.sent + protocol_->RetransmitDelay();
     }
   }
   return Nothing;
 }
 
-void PacketProtocol::OutstandingMessages::CheckRetransmit() {
+void PacketProtocol::OutstandingMessages::CheckRetransmit(
+    Transaction* transaction) {
   if (!protocol_->state_.has_value()) {
     return;
   }
-  rto_timer_.Reset();
+  retransmit_timeout_.Reset();
   const auto nack_before =
       protocol_->timer_->Now() - protocol_->RetransmitDelay();
   OVERNET_TRACE(DEBUG) << "OutstandingMessages.CheckRetransmit: nack_before="
                        << nack_before
                        << " (current_rtt=" << protocol_->CurrentRTT() << ")";
-  AckProcessor ack_processor(this, TimeDelta::Zero());
   for (size_t i = 0; i < outstanding_.size(); i++) {
-    if (!outstanding_[i].bbr_sent_packet.has_value()) {
+    if (outstanding_[i].is_finalized()) {
       OVERNET_TRACE(DEBUG) << "OutstandingMessages.CheckRetransmit: seq "
-                           << (send_tip_ + i) << " not sent: STOP";
+                           << (send_tip_ + i) << " finalized: STOP";
       break;
     }
-    if (outstanding_[i].bbr_sent_packet->send_time > nack_before) {
+    const auto sent = outstanding_[i].sent;
+    if (sent > nack_before) {
       OVERNET_TRACE(DEBUG) << "OutstandingMessages.CheckRetransmit: seq "
-                           << (send_tip_ + i) << " sent at "
-                           << outstanding_[i].bbr_sent_packet->send_time
+                           << (send_tip_ + i) << " sent at " << sent
                            << ": STOP";
       break;
     }
     OVERNET_TRACE(DEBUG) << "OutstandingMessages.CheckRetransmit: seq "
-                         << (send_tip_ + i) << " sent at "
-                         << outstanding_[i].bbr_sent_packet->send_time
-                         << ": NACK";
-    ack_processor.Nack(send_tip_ + i, Status::Unavailable());
+                         << (send_tip_ + i) << " sent at " << sent << ": NACK";
+    Nack(transaction, send_tip_ + i, TimeDelta::Zero(), Status::Unavailable());
   }
   ScheduleRetransmit();
 }
 
-void PacketProtocol::OutstandingMessages::NackAll() {
+PacketProtocol::OutstandingMessages::~OutstandingMessages() {
+  protocol_->InTransaction(
+      [this](Transaction* transaction) { NackAll(transaction); });
+}
+
+void PacketProtocol::OutstandingMessages::NackAll(Transaction* transaction) {
   OVERNET_TRACE(DEBUG) << "OutstandingMessages.NackAll";
-  AckProcessor ack_processor(this, TimeDelta::Zero());
   for (uint64_t i = send_tip_, end = send_tip_ + outstanding_.size(); i < end;
        i++) {
-    if (outstanding_[i - send_tip_].request.empty()) {
+    if (outstanding_[i - send_tip_].is_finalized()) {
       continue;
     }
-    ack_processor.Nack(i, Status::Cancelled());
+    Nack(transaction, i, TimeDelta::Zero(), Status::Cancelled());
   }
 }
 
@@ -932,7 +1005,7 @@
 // Utilities
 
 TimeDelta PacketProtocol::CurrentRTT() const {
-  return std::max(TimeDelta::FromMilliseconds(1), state_->bbr_.rtt());
+  return std::max(TimeDelta::FromMilliseconds(1), state_->bbr.rtt());
 }
 
 TimeDelta PacketProtocol::RetransmitDelay() const {
diff --git a/src/connectivity/overnet/lib/packet_protocol/packet_protocol.h b/src/connectivity/overnet/lib/packet_protocol/packet_protocol.h
index 23c859d..58d0e3a 100644
--- a/src/connectivity/overnet/lib/packet_protocol/packet_protocol.h
+++ b/src/connectivity/overnet/lib/packet_protocol/packet_protocol.h
@@ -7,13 +7,16 @@
 #include <deque>
 #include <map>
 #include <queue>
+#include <variant>
 
 #include "src/connectivity/overnet/lib/environment/timer.h"
 #include "src/connectivity/overnet/lib/environment/trace.h"
 #include "src/connectivity/overnet/lib/labels/seq_num.h"
 #include "src/connectivity/overnet/lib/packet_protocol/bbr.h"
+#include "src/connectivity/overnet/lib/packet_protocol/bdp_estimator.h"
 #include "src/connectivity/overnet/lib/protocol/ack_frame.h"
 #include "src/connectivity/overnet/lib/protocol/varint.h"
+#include "src/connectivity/overnet/lib/stats/link.h"
 #include "src/connectivity/overnet/lib/vocabulary/callback.h"
 #include "src/connectivity/overnet/lib/vocabulary/lazy_slice.h"
 #include "src/connectivity/overnet/lib/vocabulary/once_fn.h"
@@ -121,7 +124,11 @@
   // either explicitly dropped or destroyed.
   class ProtocolRef {
    public:
-    ProtocolRef(PacketProtocol* protocol) : protocol_(protocol) {
+    ProtocolRef(PacketProtocol* protocol, bool primary_ref = false)
+        : protocol_(protocol) {
+      if (!primary_ref) {
+        assert(protocol_->refs_ != 0);
+      }
       protocol_->refs_++;
     }
     ~ProtocolRef() {
@@ -190,118 +197,150 @@
 
   class PacketSend;
 
+  // A Transaction is created to describe a set of changes to a PacketProtocol.
+  // One is created in response to every Process() call, and in response to
+  // sends that are outside of incoming messages.
+  // Only one Transaction can be active at a time.
+  // A Transaction can process only one incoming message.
+  // A Transaction may process any number (including zero) sends.
+  class Transaction {
+   public:
+    Transaction(PacketProtocol* protocol);
+    ~Transaction();
+
+    void QuiesceOnCompletion(Callback<void> callback);
+    bool Closing() const { return quiesce_ || !protocol_->state_.has_value(); }
+
+    void StartSendingOnCompletion() { start_sending_ = true; }
+    void IncrementOutstandingTipOnCompletion() {
+      increment_outstanding_tip_ = true;
+    }
+
+    void QueueAck(BBR::SentPacket packet) {
+      bbr_ack_.Force()->acked_packets.push_back(packet);
+    }
+    void QueueNack(BBR::SentPacket packet) {
+      bbr_ack_.Force()->nacked_packets.push_back(packet);
+    }
+
+   private:
+    PacketProtocol* const protocol_;
+    bool quiesce_ = false;
+    bool start_sending_ = false;
+    bool increment_outstanding_tip_ = false;
+    Optional<BBR::Ack> bbr_ack_;
+  };
+
   // OutstandingMessages tracks messages that are sent but not yet acknowledged.
   class OutstandingMessages {
    public:
     OutstandingMessages(PacketProtocol* protocol);
-    ~OutstandingMessages() { NackAll(); }
-    void Send(BBR::TransmitRequest bbr_request, SendRequestHdl request);
+    ~OutstandingMessages();
     Status ValidateAck(const AckFrame& ack) const;
-    void ProcessValidAck(AckFrame ack, TimeStamp received);
+    void ProcessValidAck(Transaction* transaction, AckFrame ack,
+                         TimeStamp received);
     void ReceivedPacket() { ScheduleRetransmit(); }
+    void Schedule(Transaction* transaction, SendRequestHdl message);
+    void StartSending();
+
+    void ScheduleAck();
+    void ForceSendAck(Transaction* t);
+
+    void IncrementTip();
 
    private:
     friend class PacketSend;
 
-    Slice GeneratePacket(BBR::TransmitRequest bbr_request, uint64_t seq_idx,
-                         LazySliceArgs args);
-    void CancelPacket(uint64_t seq_idx);
+    Slice GeneratePacket(Transaction* transaction,
+                         BBR::TransmitRequest bbr_request, LazySliceArgs args);
+    void SentMessage(Transaction* transaction);
+    void CancelledMessage(Transaction* transaction);
+
     void FinishedSending();
     void ScheduleRetransmit();
     Optional<TimeStamp> RetransmitDeadline();
-    void CheckRetransmit();
-    void NackAll();
+    void CheckRetransmit(Transaction* transaction);
+    void NackAll(Transaction* transaction);
+    void Send(BBR::TransmitRequest bbr_request, SendRequestHdl request);
 
-    enum class OutstandingPacketState : uint8_t {
-      PENDING,
-      SENT,
-      ACKED,
-      NACKED,
-    };
-
-    friend std::ostream& operator<<(std::ostream& out,
-                                    OutstandingPacketState state) {
-      switch (state) {
-        case OutstandingPacketState::PENDING:
-          return out << "PENDING";
-        case OutstandingPacketState::SENT:
-          return out << "SENT";
-        case OutstandingPacketState::ACKED:
-          return out << "ACKED";
-        case OutstandingPacketState::NACKED:
-          return out << "NACKED";
-      }
-    }
+    void Ack(Transaction* transaction, uint64_t seq, TimeDelta queue_delay);
+    void Nack(Transaction* transaction, uint64_t seq, TimeDelta queue_delay,
+              const Status& status);
 
     struct OutstandingPacket {
-      OutstandingPacketState state;
+      struct PendingTailProbe {};
+
+      struct Pending {
+        SendRequestHdl request;
+      };
+
+      struct Sent {
+        SendRequestHdl request;
+        BBR::SentPacket bbr_sent_packet;
+        BdpEstimator::PerPacketData bdp_packet;
+      };
+
+      struct Acked {};
+      struct Nacked {};
+
+      using State =
+          std::variant<PendingTailProbe, Pending, Sent, Acked, Nacked>;
+
+      TimeStamp sent;
       uint64_t max_seen_sequence_at_send;
-      Optional<BBR::SentPacket> bbr_sent_packet;
-      SendRequestHdl request;
+      State state;
+
+      bool has_request() const {
+        return std::holds_alternative<Pending>(state) ||
+               std::holds_alternative<Sent>(state);
+      }
+
+      bool is_finalized() const {
+        return std::holds_alternative<Acked>(state) ||
+               std::holds_alternative<Nacked>(state);
+      }
+
+      friend std::ostream& operator<<(std::ostream& out, const State& state) {
+        if (std::holds_alternative<PendingTailProbe>(state)) {
+          return out << "PENDING_TAIL_PROBE";
+        } else if (std::holds_alternative<Pending>(state)) {
+          return out << "PENDING";
+        } else if (std::holds_alternative<Sent>(state)) {
+          return out << "SENT";
+        } else if (std::holds_alternative<Acked>(state)) {
+          return out << "ACKED";
+        } else if (std::holds_alternative<Nacked>(state)) {
+          return out << "NACKED";
+        } else {
+          abort();
+        }
+      }
     };
 
-    // Assists processing a set of acks/nacks
-    class AckProcessor {
-     public:
-      AckProcessor(OutstandingMessages* outstanding, TimeDelta queue_delay)
-          : outstanding_(outstanding), queue_delay_(queue_delay) {}
-      ~AckProcessor();
-
-      void Ack(uint64_t seq);
-      void Nack(uint64_t seq, const Status& status);
-
-     private:
-      OutstandingMessages* const outstanding_;
-      const TimeDelta queue_delay_;
-      BBR::Ack bbr_ack_;
-    };
+    void Schedule(Transaction* transaction,
+                  OutstandingPacket::State outstanding_packet_state);
 
     PacketProtocol* const protocol_;
     uint64_t send_tip_ = 1;
+    uint64_t unsent_tip_ = 1;
     uint64_t max_outstanding_size_ = 1;
     uint64_t last_sent_ack_ = 0;
+    bool last_send_was_tail_probe_ = false;
     std::deque<OutstandingPacket> outstanding_;
 
-    Optional<Timeout> rto_timer_;
-  };
+    std::string OutstandingString() const;
 
-  // SendQueue groups together pending sends in the protocol and manages writing
-  // them out.
-  class SendQueue {
-   public:
-    SendQueue(PacketProtocol* protocol) : protocol_(protocol) {}
-
-    // Schedule this queue to be sent. Should only be used as directed.
-    void Schedule();
-
-    // A message was just sent: possibly schedule the next one.
-    void SentMessage();
-
-    // Ensure a tail loss probe is scheduled: used to force acks to be sent on
-    // idle connections.
-    void ScheduleTailLossProbe();
-
-    // Force a packet to be sent with an ack.
-    void ForceSendAck();
-
-    // Returns true if send request addition triggers the need for
-    // Schedule to be called.
-    [[nodiscard]] bool Add(SendRequestHdl hdl);
-
-   private:
-    PacketProtocol* const protocol_;
-    bool scheduled_ = false;
-    bool last_send_was_tail_loss_probe_ = false;
-    Optional<BBR::TransmitRequest> transmit_request_;
-    std::queue<SendRequestHdl> requests_;
-    struct ScheduledTailLossProbe {
+    struct TailProbeTimeout {
       template <class F>
-      ScheduledTailLossProbe(Timer* timer, TimeStamp when, F f)
+      TailProbeTimeout(Timer* timer, TimeStamp when, F f)
           : when(when), timeout(timer, when, std::move(f)) {}
       TimeStamp when;
       Timeout timeout;
     };
-    Optional<ScheduledTailLossProbe> scheduled_tail_loss_probe_;
+
+    Optional<Timeout> retransmit_timeout_;
+    Optional<TailProbeTimeout> tail_probe_timeout_;
+    Optional<BBR::TransmitRequest> transmit_request_;
   };
 
   enum class AckUrgency { NOT_REQUIRED, SEND_SOON, SEND_IMMEDIATELY };
@@ -321,19 +360,22 @@
    public:
     AckSender(PacketProtocol* protocol);
 
-    void NeedAck(AckUrgency urgency);
+    void NeedAck(Transaction* transaction, AckUrgency urgency);
     bool ShouldSendAck() const {
-      return !all_acks_acknowledged_ && sent_full_acks_.empty();
+      return !suppress_need_acks_ && !all_acks_acknowledged_ &&
+             sent_full_acks_.empty();
     }
-    void AckSent(uint64_t seq_idx, bool partial);
-    void OnNack(uint64_t seq_idx);
+    void AckSent(Transaction* transaction, uint64_t seq_idx, bool partial);
+    void OnNack(Transaction* transaction, uint64_t seq_idx, bool shutting_down);
     void OnAck(uint64_t seq_idx);
 
    private:
     PacketProtocol* const protocol_;
     std::vector<uint64_t> sent_full_acks_;
     bool all_acks_acknowledged_ = true;
+    bool suppress_need_acks_ = false;
     AckUrgency urgency_ = AckUrgency::NOT_REQUIRED;
+    Optional<Timeout> send_ack_timer_;
 
     std::string SentFullAcksString() const;
   };
@@ -342,6 +384,8 @@
   // need to acknowledge them.
   class ReceivedQueue {
    public:
+    explicit ReceivedQueue(LinkStats* stats) : stats_(stats) {}
+
     // Return true if an ack should be sent now.
     template <class F>
     [[nodiscard]] AckUrgency Received(SeqNum seq_num, TimeStamp received,
@@ -349,8 +393,8 @@
 
     uint64_t max_seen_sequence() const;
     bool CanBuildAck() const { return max_seen_sequence() > 0; }
-    AckFrame BuildAck(uint64_t seq_idx, TimeStamp now, uint32_t max_length,
-                      AckSender* ack_sender);
+    AckFrame BuildAck(Transaction* transaction, uint64_t seq_idx, TimeStamp now,
+                      uint32_t max_length, AckSender* ack_sender);
     void SetTip(uint64_t seq_idx, TimeStamp received);
 
    private:
@@ -374,7 +418,9 @@
     enum class ReceiveState {
       UNKNOWN,
       NOT_RECEIVED,
+      RECEIVED_PURE_ACK,
       RECEIVED,
+      RECEIVED_AND_ACKED_IMMEDIATELY,
     };
 
     friend std::ostream& operator<<(std::ostream& out, ReceiveState state) {
@@ -385,6 +431,10 @@
           return out << "NOT_RECEIVED";
         case ReceiveState::RECEIVED:
           return out << "RECEIVED";
+        case ReceiveState::RECEIVED_PURE_ACK:
+          return out << "RECEIVED_PURE_ACK";
+        case ReceiveState::RECEIVED_AND_ACKED_IMMEDIATELY:
+          return out << "RECEIVED_AND_ACKED_IMMEDIATELY";
       }
     }
 
@@ -394,37 +444,12 @@
       TimeStamp when;
     };
     std::deque<ReceivedPacket> received_packets_;
-  };
-
-  // A Transaction is created to describe a set of changes to a PacketProtocol.
-  // One is created in response to every Process() call, and in response to
-  // sends that are outside of incoming messages.
-  // Only one Transaction can be active at a time.
-  // A Transaction can process only one incoming message.
-  // A Transaction may process any number (including zero) sends.
-  class Transaction {
-   public:
-    Transaction(PacketProtocol* protocol);
-    ~Transaction();
-    void Send(SendRequestHdl hdl);
-    void ScheduleForcedAck() { schedule_send_queue_ = true; }
-
-    void QuiesceOnCompletion(Callback<void> callback);
-
-    bool Closing() const { return quiesce_ || !protocol_->state_.has_value(); }
-
-   private:
-    SendQueue* send_queue();
-
-    PacketProtocol* const protocol_;
-    bool schedule_send_queue_ = false;
-    bool quiesce_ = false;
+    LinkStats* const stats_;
   };
 
   class PacketSend final {
    public:
-    PacketSend(PacketProtocol* protocol, uint64_t seq_idx,
-               BBR::TransmitRequest bbr_request);
+    PacketSend(PacketProtocol* protocol, BBR::TransmitRequest bbr_request);
     ~PacketSend();
     PacketSend(const PacketSend&) = delete;
     PacketSend(PacketSend&&) = default;
@@ -433,7 +458,6 @@
 
    private:
     ProtocolRef protocol_;
-    uint64_t seq_idx_;
     BBR::TransmitRequest bbr_request_;
   };
 
@@ -444,7 +468,7 @@
   using RandFunc = BBR::RandFunc;
 
   PacketProtocol(Timer* timer, RandFunc rand, PacketSender* packet_sender,
-                 const Codec* codec, uint64_t mss);
+                 const Codec* codec, uint64_t mss, bool probe_tails);
 
   // Request that a single message be sent.
   void Send(SendRequestHdl send_request);
@@ -479,24 +503,30 @@
 
   uint32_t maximum_send_size() const { return maximum_send_size_; }
   TimeDelta round_trip_time() const {
-    return state_.has_value() ? state_->bbr_.rtt() : TimeDelta::PositiveInf();
+    return state_.has_value() ? state_->bbr.rtt() : TimeDelta::PositiveInf();
   }
   Bandwidth bottleneck_bandwidth() const {
-    return state_.has_value() ? state_->bbr_.bottleneck_bandwidth()
+    return state_.has_value() ? state_->bbr.bottleneck_bandwidth()
                               : Bandwidth::Zero();
   }
+  uint64_t bdp_estimate() const {
+    return state_.has_value() ? state_->bdp_estimator.estimate() : 0;
+  }
 
   static Codec* NullCodec();
 
+  const LinkStats* stats() const { return &stats_; }
+
   /////////////////////////////////////////////////////////////////////////////
   // Internal methods.
  private:
   TimeDelta CurrentRTT() const;
   TimeDelta RetransmitDelay() const;
   TimeDelta TailLossProbeDelay() const;
-  Slice FormatPacket(uint64_t seq_idx, SendRequest* request,
-                     LazySliceArgs args);
-  ProcessMessageResult ProcessMessage(uint64_t seq_idx, Slice slice,
+  Slice FormatPacket(Transaction* transaction, uint64_t seq_idx,
+                     SendRequest* request, LazySliceArgs args);
+  ProcessMessageResult ProcessMessage(Transaction* transaction,
+                                      uint64_t seq_idx, Slice slice,
                                       TimeStamp received,
                                       ProcessCallback handle_message);
   // Run closure f in a transaction (creating one if necessary)
@@ -520,22 +550,25 @@
   PacketSender* const packet_sender_;
   Transaction* active_transaction_ = nullptr;
   Callback<void> quiesce_;
+  const bool probe_tails_;
   const uint32_t maximum_send_size_;
   uint32_t refs_ = 0;
-  ProtocolRef master_ref_{this};
+  ProtocolRef primary_ref_{this, true};
   struct OpenState {
     OpenState(PacketProtocol* protocol, RandFunc rand)
         : ack_sender(protocol),
+          received_queue(&protocol->stats_),
           outstanding_messages(protocol),
-          bbr_(protocol->timer_, std::move(rand), protocol->maximum_send_size_,
-               Nothing) {}
+          bbr(protocol->timer_, std::move(rand), protocol->maximum_send_size_,
+              Nothing) {}
     AckSender ack_sender;
-    Optional<SendQueue> send_queue;
     ReceivedQueue received_queue;
     OutstandingMessages outstanding_messages;
-    BBR bbr_;
+    BBR bbr;
+    BdpEstimator bdp_estimator;
   };
   Optional<OpenState> state_;
+  LinkStats stats_;
 };
 
 template <class GB, class A>
diff --git a/src/connectivity/overnet/lib/packet_protocol/packet_protocol_fuzzer.h b/src/connectivity/overnet/lib/packet_protocol/packet_protocol_fuzzer.h
index 3de9854..0e70b92 100644
--- a/src/connectivity/overnet/lib/packet_protocol/packet_protocol_fuzzer.h
+++ b/src/connectivity/overnet/lib/packet_protocol/packet_protocol_fuzzer.h
@@ -7,6 +7,7 @@
 #include <iostream>
 #include <map>
 #include <random>
+
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
 #include "src/connectivity/overnet/lib/packet_protocol/packet_protocol.h"
 #include "src/connectivity/overnet/lib/testing/test_timer.h"
@@ -76,9 +77,11 @@
   std::mt19937 rng_{12345};
   const PacketProtocol::Codec* const codec_;
   ClosedPtr<PacketProtocol> pp1_ = MakeClosedPtr<PacketProtocol>(
-      &timer_, [this] { return rng_(); }, &sender1_, codec_, kMaxSegmentSize);
+      &timer_, [this] { return rng_(); }, &sender1_, codec_, kMaxSegmentSize,
+      true);
   ClosedPtr<PacketProtocol> pp2_ = MakeClosedPtr<PacketProtocol>(
-      &timer_, [this] { return rng_(); }, &sender2_, codec_, kMaxSegmentSize);
+      &timer_, [this] { return rng_(); }, &sender2_, codec_, kMaxSegmentSize,
+      true);
 };
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/packet_protocol/packet_protocol_test.cc b/src/connectivity/overnet/lib/packet_protocol/packet_protocol_test.cc
index df93bda..b4cedb18 100644
--- a/src/connectivity/overnet/lib/packet_protocol/packet_protocol_test.cc
+++ b/src/connectivity/overnet/lib/packet_protocol/packet_protocol_test.cc
@@ -3,7 +3,9 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/packet_protocol/packet_protocol.h"
+
 #include <memory>
+
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "src/connectivity/overnet/lib/environment/trace_cout.h"
@@ -101,7 +103,7 @@
   StrictMock<MockPacketSender> ps;
   std::mt19937 rng{123};
   MakeClosedPtr<PacketProtocol>(
-      &timer, [&rng] { return rng(); }, &ps, GetParam(), kMSS);
+      &timer, [&rng] { return rng(); }, &ps, GetParam(), kMSS, true);
 }
 
 TEST_P(PacketProtocolTest, SendOnePacket) {
@@ -114,7 +116,7 @@
   StrictMock<MockPacketSender> ps;
   std::mt19937 rng{123};
   auto packet_protocol = MakeClosedPtr<PacketProtocol>(
-      &timer, [&rng] { return rng(); }, &ps, GetParam(), kMSS);
+      &timer, [&rng] { return rng(); }, &ps, GetParam(), kMSS, true);
 
   // Send some dummy data: we expect to see a packet emitted immediately
   Slice got_slice;
diff --git a/src/connectivity/overnet/lib/protocol/BUILD.gn b/src/connectivity/overnet/lib/protocol/BUILD.gn
index a4af4e6..83e8b83 100644
--- a/src/connectivity/overnet/lib/protocol/BUILD.gn
+++ b/src/connectivity/overnet/lib/protocol/BUILD.gn
@@ -19,6 +19,8 @@
   deps = [
     ":ack_frame_test",
     ":routable_message_test",
+    ":stream_framer_test",
+    ":unreliable_framer_test",
     ":varint_test",
   ]
 }
@@ -114,6 +116,20 @@
   ]
 }
 
+# reliable_framer
+source_set("reliable_framer") {
+  sources = [
+    "reliable_framer.cc",
+    "reliable_framer.h",
+  ]
+  public_deps = [
+    ":stream_framer",
+  ]
+  deps = [
+    "//src/connectivity/overnet/lib/environment:trace",
+  ]
+}
+
 # routable_message
 source_set("routable_message") {
   sources = [
@@ -160,6 +176,60 @@
   ]
 }
 
+# stream_framer
+source_set("stream_framer") {
+  sources = [
+    "stream_framer.h",
+  ]
+  public_deps = [
+    "//src/connectivity/overnet/lib/vocabulary:optional",
+    "//src/connectivity/overnet/lib/vocabulary:slice",
+    "//src/connectivity/overnet/lib/vocabulary:status",
+    "//src/connectivity/overnet/lib/environment:trace",
+  ]
+}
+
+source_set("stream_framer_test") {
+  testonly = true
+  sources = [
+    "stream_framer_test.cc",
+  ]
+  public_deps = [
+    ":reliable_framer",
+    ":unreliable_framer",
+  ]
+  deps = [
+    "//third_party/googletest:gtest",
+  ]
+}
+
+# unreliable_framer
+source_set("unreliable_framer") {
+  sources = [
+    "unreliable_framer.h",
+    "unreliable_framer.cc",
+  ]
+  public_deps = [
+    ":stream_framer",
+  ]
+  deps = [
+    "//third_party/zlib:zlib_static",
+  ]
+}
+
+source_set("unreliable_framer_test") {
+  testonly = true
+  sources = [
+    "unreliable_framer_test.cc",
+  ]
+  public_deps = [
+    ":unreliable_framer",
+  ]
+  deps = [
+    "//third_party/googletest:gtest",
+  ]
+}
+
 # varint
 source_set("varint") {
   sources = [
diff --git a/src/connectivity/overnet/lib/protocol/reliable_framer.cc b/src/connectivity/overnet/lib/protocol/reliable_framer.cc
new file mode 100644
index 0000000..2e3da85
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/reliable_framer.cc
@@ -0,0 +1,70 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+
+#include "src/connectivity/overnet/lib/environment/trace.h"
+
+namespace overnet {
+
+ReliableFramer::ReliableFramer() : StreamFramer(Border::Prefix(2), 65536) {}
+ReliableFramer::~ReliableFramer() = default;
+
+void ReliableFramer::Push(Slice data) {
+  OVERNET_TRACE(DEBUG) << "ReliableFramer.Push: " << data;
+  buffered_input_.Append(std::move(data));
+}
+
+StatusOr<Optional<Slice>> ReliableFramer::Pop() {
+  OVERNET_TRACE(DEBUG) << "ReliableFramer.Pop: q=" << buffered_input_;
+  using Sts = StatusOr<Optional<Slice>>;
+
+  const uint8_t *begin = buffered_input_.begin();
+  const uint8_t *p = begin;
+  const uint8_t *end = buffered_input_.end();
+
+  OVERNET_TRACE(DEBUG) << "ReliableFramer.Pop: have " << (end - p) << " bytes";
+
+  if (end - p < 2) {
+    OVERNET_TRACE(DEBUG)
+        << "ReliableFramer.Pop: insufficient bytes to see header";
+    return Nothing;
+  }
+  uint16_t hdr;
+  memcpy(&hdr, p, 2);
+  p += 2;
+
+  const ssize_t segment_length = ssize_t(hdr) + 1;
+  OVERNET_TRACE(DEBUG) << "ReliableFramer.Pop: hdr=" << hdr
+                       << " => segment_length " << segment_length;
+
+  if (end - p < segment_length) {
+    return Sts(Nothing);
+  }
+
+  buffered_input_.TrimBegin(p - begin);
+  return Sts(buffered_input_.TakeUntilOffset(segment_length));
+}
+
+bool ReliableFramer::InputEmpty() const {
+  return buffered_input_.length() == 0;
+}
+
+Optional<Slice> ReliableFramer::SkipNoise() { return Nothing; }
+
+Slice ReliableFramer::Frame(Slice data) {
+  OVERNET_TRACE(DEBUG) << "ReliableFramer.Frame: " << data;
+  auto length = data.length();
+
+  if (length == 0) {
+    return data;
+  }
+
+  return data.WithPrefix(2, [length](uint8_t *p) {
+    uint16_t hdr = length - 1;
+    memcpy(p, &hdr, 2);
+  });
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/protocol/reliable_framer.h b/src/connectivity/overnet/lib/protocol/reliable_framer.h
new file mode 100644
index 0000000..e81e16b
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/reliable_framer.h
@@ -0,0 +1,28 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include "src/connectivity/overnet/lib/protocol/stream_framer.h"
+
+namespace overnet {
+
+// Framer that transports packets on a reliable stream of bytes
+class ReliableFramer final : public StreamFramer {
+ public:
+  ReliableFramer();
+  ~ReliableFramer();
+
+  void Push(Slice data) override;
+  StatusOr<Optional<Slice>> Pop() override;
+  bool InputEmpty() const override;
+  Optional<Slice> SkipNoise() override;
+
+  Slice Frame(Slice data) override;
+
+ private:
+  Slice buffered_input_;
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/protocol/stream_framer.h b/src/connectivity/overnet/lib/protocol/stream_framer.h
new file mode 100644
index 0000000..79842ee
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/stream_framer.h
@@ -0,0 +1,48 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include "src/connectivity/overnet/lib/vocabulary/optional.h"
+#include "src/connectivity/overnet/lib/vocabulary/slice.h"
+#include "src/connectivity/overnet/lib/vocabulary/status.h"
+
+namespace overnet {
+
+// Manages the framing and unframing of packets in a stream
+class StreamFramer {
+ public:
+  StreamFramer(Border desired_border, uint32_t maximum_segment_size)
+      : desired_border(desired_border),
+        maximum_segment_size(maximum_segment_size) {}
+  virtual ~StreamFramer() = default;
+
+  const Border desired_border;
+  const uint32_t maximum_segment_size;
+
+  // Input loop:
+  //   incoming_data = read_from_stream();
+  //   Push(incoming_data);
+  //   while (auto frame = Pop()) {
+  //     process_frame(*frame);
+  //   }
+  virtual void Push(Slice data) = 0;
+  virtual StatusOr<Optional<Slice>> Pop() = 0;
+  // Returns true if nothing is buffered.
+  virtual bool InputEmpty() const = 0;
+  // Skip content if stuck.
+  // Should be called after some appropriate timeout (some small multiple of the
+  // time required to transmit MaximumSegmentSize()).
+  // Allows the framer to skip noise reliably.
+  // Returns what was skipped (or Nothing if a skip is unavailable).
+  virtual Optional<Slice> SkipNoise() = 0;
+
+  // Output loop:
+  //   frame = construct_frame(MaximumSegmentSize());
+  //   outgoing_data = Frame(frame);
+  //   write(*outgoing_data);
+  virtual Slice Frame(Slice data) = 0;
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/protocol/stream_framer_test.cc b/src/connectivity/overnet/lib/protocol/stream_framer_test.cc
new file mode 100644
index 0000000..f6494cc
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/stream_framer_test.cc
@@ -0,0 +1,155 @@
+// Copyright 2019 The Fuchsia 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 <gtest/gtest.h>
+
+#include "src/connectivity/overnet/lib/environment/trace.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+#include "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
+
+namespace overnet {
+namespace stream_framer_test {
+
+struct TestArg {
+  std::function<std::unique_ptr<StreamFramer>()> make_framer;
+  Slice enframe;
+};
+
+struct StreamFramerTest : public ::testing::TestWithParam<TestArg> {};
+
+TEST_P(StreamFramerTest, DeframesFramed) {
+  ScopedSeverity severity(Severity::ERROR);
+  auto framer = GetParam().make_framer();
+  const auto enframe = GetParam().enframe;
+  const auto framed = framer->Frame(enframe);
+  framer->Push(framed);
+  auto deframed = framer->Pop();
+  ASSERT_TRUE(deframed.is_ok()) << deframed << " framed=" << framed;
+  ASSERT_TRUE(deframed->has_value()) << deframed << " framed=" << framed;
+  EXPECT_EQ(enframe, **deframed) << " framed=" << framed;
+  EXPECT_TRUE(framer->InputEmpty());
+}
+
+TEST_P(StreamFramerTest, DeframesOneByteAtATime) {
+  ScopedSeverity severity(Severity::ERROR);
+  auto framer = GetParam().make_framer();
+  const auto enframe = GetParam().enframe;
+  const auto framed = framer->Frame(enframe);
+  for (auto c : framed) {
+    auto early_pop = framer->Pop();
+    EXPECT_TRUE(early_pop.is_ok()) << early_pop << " framed=" << framed;
+    EXPECT_FALSE(early_pop->has_value()) << early_pop << " framed=" << framed;
+
+    framer->Push(Slice::RepeatedChar(1, c));
+  }
+  auto deframed = framer->Pop();
+  ASSERT_TRUE(deframed.is_ok()) << deframed << " framed=" << framed;
+  ASSERT_TRUE(deframed->has_value()) << deframed << " framed=" << framed;
+  EXPECT_EQ(enframe, **deframed) << " framed=" << framed;
+  EXPECT_TRUE(framer->InputEmpty());
+}
+
+template <class T>
+TestArg Test(Slice enframe) {
+  return TestArg{[] { return std::make_unique<T>(); }, enframe};
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    StreamFramerSuite, StreamFramerTest,
+    ::testing::Values(Test<ReliableFramer>(Slice::FromContainer({1, 2, 3})),
+                      Test<UnreliableFramer>(Slice::FromContainer({1, 2, 3})),
+                      Test<ReliableFramer>(Slice::RepeatedChar(256, 'a')),
+                      Test<UnreliableFramer>(Slice::RepeatedChar(256, 'a')),
+                      Test<ReliableFramer>(Slice::RepeatedChar(65536, 'a'))));
+
+struct MultiArg {
+  std::function<std::unique_ptr<StreamFramer>()> make_framer;
+  std::vector<Slice> enframe;
+};
+
+struct StreamFramerMulti : public ::testing::TestWithParam<MultiArg> {};
+
+template <class T>
+MultiArg Multi(std::initializer_list<Slice> enframe) {
+  return MultiArg{[] { return std::make_unique<T>(); }, enframe};
+}
+
+TEST_P(StreamFramerMulti, DeframesFramedOneFrameAtATime) {
+  ScopedSeverity severity(Severity::ERROR);
+  auto framer = GetParam().make_framer();
+  for (auto enframe : GetParam().enframe) {
+    const auto framed = framer->Frame(enframe);
+    framer->Push(framed);
+    auto deframed = framer->Pop();
+    ASSERT_TRUE(deframed.is_ok()) << deframed << " framed=" << framed;
+    ASSERT_TRUE(deframed->has_value()) << deframed << " framed=" << framed;
+    EXPECT_EQ(enframe, **deframed) << " framed=" << framed;
+    EXPECT_TRUE(framer->InputEmpty());
+  }
+}
+
+TEST_P(StreamFramerMulti, DeframesOneByteAtATimeOneFrameAtATime) {
+  ScopedSeverity severity(Severity::ERROR);
+  auto framer = GetParam().make_framer();
+  for (auto enframe : GetParam().enframe) {
+    const auto framed = framer->Frame(enframe);
+    for (auto c : framed) {
+      auto early_pop = framer->Pop();
+      EXPECT_TRUE(early_pop.is_ok()) << early_pop << " framed=" << framed;
+      EXPECT_FALSE(early_pop->has_value()) << early_pop << " framed=" << framed;
+
+      framer->Push(Slice::RepeatedChar(1, c));
+    }
+    auto deframed = framer->Pop();
+    ASSERT_TRUE(deframed.is_ok()) << deframed << " framed=" << framed;
+    ASSERT_TRUE(deframed->has_value()) << deframed << " framed=" << framed;
+    EXPECT_EQ(enframe, **deframed) << " framed=" << framed;
+    EXPECT_TRUE(framer->InputEmpty());
+  }
+}
+
+TEST_P(StreamFramerMulti, DeframesFramedAllFramesAtOnce) {
+  ScopedSeverity severity(Severity::ERROR);
+  auto framer = GetParam().make_framer();
+  for (auto enframe : GetParam().enframe) {
+    const auto framed = framer->Frame(enframe);
+    framer->Push(framed);
+  }
+  for (auto enframe : GetParam().enframe) {
+    auto deframed = framer->Pop();
+    ASSERT_TRUE(deframed.is_ok()) << deframed;
+    ASSERT_TRUE(deframed->has_value()) << deframed;
+    EXPECT_EQ(enframe, **deframed);
+  }
+  EXPECT_TRUE(framer->InputEmpty());
+}
+
+TEST_P(StreamFramerMulti, DeframesOneByteAtATimeAllFramesAtOnce) {
+  ScopedSeverity severity(Severity::ERROR);
+  auto framer = GetParam().make_framer();
+  for (auto enframe : GetParam().enframe) {
+    const auto framed = framer->Frame(enframe);
+    for (auto c : framed) {
+      framer->Push(Slice::RepeatedChar(1, c));
+    }
+  }
+  for (auto enframe : GetParam().enframe) {
+    auto deframed = framer->Pop();
+    ASSERT_TRUE(deframed.is_ok()) << deframed;
+    ASSERT_TRUE(deframed->has_value()) << deframed;
+    EXPECT_EQ(enframe, **deframed);
+  }
+  EXPECT_TRUE(framer->InputEmpty());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    StreamFramerMultiSuite, StreamFramerMulti,
+    ::testing::Values(Multi<ReliableFramer>({Slice::FromContainer({1, 2, 3}),
+                                             Slice::FromContainer({1, 2, 3})}),
+                      Multi<UnreliableFramer>({Slice::FromContainer({1, 2, 3}),
+                                               Slice::FromContainer({1, 2,
+                                                                     3})})));
+
+}  // namespace stream_framer_test
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/protocol/unreliable_framer.cc b/src/connectivity/overnet/lib/protocol/unreliable_framer.cc
new file mode 100644
index 0000000..9701eec
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/unreliable_framer.cc
@@ -0,0 +1,121 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
+
+#include "third_party/zlib/zlib.h"
+
+namespace overnet {
+
+UnreliableFramer::UnreliableFramer() : StreamFramer(Border{2, 4}, 256) {}
+
+UnreliableFramer::~UnreliableFramer() = default;
+
+void UnreliableFramer::Push(Slice data) {
+  buffered_input_.Append(std::move(data));
+}
+
+StatusOr<Optional<Slice>> UnreliableFramer::Pop() {
+  using Sts = StatusOr<Optional<Slice>>;
+
+  const uint8_t *const begin = buffered_input_.begin();
+  const uint8_t *p = begin;
+  const uint8_t *const end = buffered_input_.end();
+
+  while (p != end) {
+    if (*p != kStartOfFrameMarker) {
+      ++p;
+      continue;
+    }
+
+    if (end - p < 7) {
+      break;
+    }
+
+    uint32_t length = uint32_t(p[1]) + 1;
+    if (end - p < 2 + length + 4) {
+      break;
+    }
+
+    uint32_t sent_crc;
+    memcpy(&sent_crc, p + 2 + length, sizeof(sent_crc));
+    const uint32_t calc_crc = crc32(0, p + 2, length);
+
+    if (sent_crc != calc_crc) {
+      ++p;
+      continue;
+    }
+
+    auto out = buffered_input_;
+    out.Trim(p + 2 - begin, end - (p + 2) - length);
+    buffered_input_.TrimBegin(p + 2 + length + 4 - begin);
+    return Sts(out);
+  }
+
+  buffered_input_.TrimBegin(p - begin);
+  return Sts(Nothing);
+}
+
+bool UnreliableFramer::InputEmpty() const {
+  return buffered_input_.length() == 0;
+}
+
+Optional<Slice> UnreliableFramer::SkipNoise() {
+  const uint8_t *const begin = buffered_input_.begin();
+  const uint8_t *p = begin;
+  const uint8_t *const end = buffered_input_.end();
+
+  if (p == end) {
+    return Nothing;
+  }
+  if (*p != kStartOfFrameMarker) {
+    return Nothing;
+  }
+  ++p;
+  while (p != end) {
+    if (*p != kStartOfFrameMarker) {
+      ++p;
+      continue;
+    }
+
+    if (end - p < 7) {
+      break;
+    }
+
+    uint32_t length = uint32_t(p[1]) + 1;
+    if (end - p < 2 + length + 4) {
+      break;
+    }
+
+    uint32_t sent_crc;
+    memcpy(&sent_crc, p + 2 + length, sizeof(sent_crc));
+    const uint32_t calc_crc = crc32(0, p + 2, length);
+
+    if (sent_crc != calc_crc) {
+      ++p;
+      continue;
+    }
+
+    break;
+  }
+
+  return buffered_input_.TakeUntilPointer(p);
+}
+
+Slice UnreliableFramer::Frame(Slice data) {
+  const auto length = data.length();
+  if (length == 0) {
+    return data;
+  }
+  assert(length <= 256);
+  return data.WithBorders(Border{2, 4}, [length](uint8_t *p) {
+    p[0] = kStartOfFrameMarker;
+    p[1] = length - 1;
+    auto *s = p + 2 + length;
+    const uint32_t crc = crc32(0, p + 2, length);
+    memcpy(s, &crc, sizeof(crc));
+  });
+}
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/protocol/unreliable_framer.h b/src/connectivity/overnet/lib/protocol/unreliable_framer.h
new file mode 100644
index 0000000..5f52fc4
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/unreliable_framer.h
@@ -0,0 +1,31 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#pragma once
+
+#include "src/connectivity/overnet/lib/protocol/stream_framer.h"
+
+namespace overnet {
+
+// Framer that transports packets on an unreliable stream of bytes (assumes
+// bytes may be dropped, replicated, and/or mutated)
+class UnreliableFramer final : public StreamFramer {
+ public:
+  static constexpr uint8_t kStartOfFrameMarker = '\n';
+
+  UnreliableFramer();
+  ~UnreliableFramer();
+
+  void Push(Slice data) override;
+  StatusOr<Optional<Slice>> Pop() override;
+  bool InputEmpty() const override;
+  Optional<Slice> SkipNoise() override;
+
+  Slice Frame(Slice data) override;
+
+ private:
+  Slice buffered_input_;
+};
+
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/protocol/unreliable_framer_test.cc b/src/connectivity/overnet/lib/protocol/unreliable_framer_test.cc
new file mode 100644
index 0000000..3ecf349
--- /dev/null
+++ b/src/connectivity/overnet/lib/protocol/unreliable_framer_test.cc
@@ -0,0 +1,114 @@
+// Copyright 2019 The Fuchsia 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 "src/connectivity/overnet/lib/protocol/unreliable_framer.h"
+
+#include <gtest/gtest.h>
+
+namespace overnet {
+namespace unreliable_framer_test {
+
+struct Param {
+  Slice input;
+  std::vector<Slice> output;
+};
+
+std::ostream& operator<<(std::ostream& out, const Param& param) {
+  out << param.input << " --> {";
+  bool first = true;
+  for (const auto& output : param.output) {
+    if (!first) {
+      out << ", ";
+    }
+    first = false;
+    out << output;
+  }
+  out << "}";
+  return out;
+}
+
+struct UnreliableFramerTest : public ::testing::TestWithParam<Param> {};
+
+TEST_P(UnreliableFramerTest, UnframesCorrectly_AtOnce) {
+  UnreliableFramer framer;
+  framer.Push(GetParam().input);
+  for (const auto& expect : GetParam().output) {
+    while (true) {
+      auto frame = framer.Pop();
+      ASSERT_TRUE(frame.is_ok()) << frame;
+      if (frame->has_value()) {
+        EXPECT_EQ(expect, **frame);
+        break;  // from while loop
+      } else {
+        // No frame ready: skip any noise (simulates timeout), try again.
+        EXPECT_TRUE(framer.SkipNoise().has_value());
+      }
+    }
+  }
+  auto frame = framer.Pop();
+  ASSERT_TRUE(frame.is_ok());
+  ASSERT_FALSE(frame->has_value());
+}
+
+TEST_P(UnreliableFramerTest, UnframesCorrectly_OneByteAtATime) {
+  UnreliableFramer framer;
+  auto expect_it = GetParam().output.begin();
+  auto expect_end = GetParam().output.end();
+  for (auto c : GetParam().input) {
+    framer.Push(Slice::RepeatedChar(1, c));
+    auto frame = framer.Pop();
+    ASSERT_TRUE(frame.is_ok());
+    if (expect_it == expect_end) {
+      EXPECT_FALSE(frame->has_value());
+    } else if (frame->has_value()) {
+      EXPECT_EQ(*expect_it, **frame);
+      ++expect_it;
+    } else {
+      // nothing to do
+    }
+  }
+  while (expect_it != expect_end) {
+    EXPECT_TRUE(framer.SkipNoise().has_value());
+    while (expect_it != expect_end) {
+      auto frame = framer.Pop();
+      ASSERT_TRUE(frame.is_ok()) << frame;
+      if (frame->has_value()) {
+        EXPECT_EQ(*expect_it, **frame);
+        ++expect_it;
+      } else {
+        // No frame ready: skip any noise (simulates timeout), try again.
+        break;  // from while loop
+      }
+    }
+  }
+  EXPECT_EQ(size_t(expect_it - GetParam().output.begin()),
+            GetParam().output.size());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    UnreliableFramerSuite, UnreliableFramerTest,
+    ::testing::Values(
+        // Simple correct frame
+        Param{Slice::FromContainer({'\n', 2, 'a', 'b', 'c', 0xc2, 0x41, 0x24,
+                                    0x35}),
+              {Slice::FromContainer({'a', 'b', 'c'})}},
+        // Correct frame prefixed with noise, and suffixed with noise
+        Param{Slice::FromContainer({'h', 'e', 'l', 'l', 'o', '\n', 2, 'a', 'b',
+                                    'c', 0xc2, 0x41, 0x24, 0x35, '\n'}),
+              {Slice::FromContainer({'a', 'b', 'c'})}},
+        // Badly formed frame (incorrect CRC)
+        Param{Slice::FromContainer({'\n', 2, 'a', 'b', 'c', 0xc2, 0x41, 0x00,
+                                    0x35}),
+              {}},
+        // Correct frame prefixed with noise, and suffixed with noise, then a
+        // new frame
+        Param{Slice::FromContainer({'h',  'e', 'l',  'l',  'o',  '\n', 2,
+                                    'a',  'b', 'c',  0xc2, 0x41, 0x24, 0x35,
+                                    '\n', 'b', 'o',  'b',  '\n', 2,    'a',
+                                    'b',  'c', 0xc2, 0x41, 0x24, 0x35}),
+              {Slice::FromContainer({'a', 'b', 'c'}),
+               Slice::FromContainer({'a', 'b', 'c'})}}));
+
+}  // namespace unreliable_framer_test
+}  // namespace overnet
diff --git a/src/connectivity/overnet/lib/routing/router.cc b/src/connectivity/overnet/lib/routing/router.cc
index efbfbd7..4235cc9 100644
--- a/src/connectivity/overnet/lib/routing/router.cc
+++ b/src/connectivity/overnet/lib/routing/router.cc
@@ -215,7 +215,8 @@
                 // Set routing information for other links.
                 for (const auto& sl : selected_links) {
                   OVERNET_TRACE(DEBUG)
-                      << "Select: " << sl.first << " " << sl.second.link_id
+                      << node_id() << " Select: dest=" << sl.first << " link"
+                      << sl.second.target_node << "#" << sl.second.link_id
                       << " (route_mss=" << sl.second.route_mss << ")";
                   auto it = owned_links_.find(
                       OwnedLabel{sl.second.target_node, sl.second.link_id});
@@ -312,6 +313,7 @@
 void Router::RegisterLink(LinkPtr<> link) {
   ScopedModule<Router> scoped_module(this);
   auto status = link->GetLinkStatus();
+  OVERNET_TRACE(DEBUG) << node_id() << " RegisterLink: " << status;
   assert(status.from == node_id());
   owned_links_.emplace(OwnedLabel{status.to, status.local_id}, std::move(link));
   auto target = status.to;
diff --git a/src/connectivity/overnet/lib/routing/router.h b/src/connectivity/overnet/lib/routing/router.h
index c719ba9..c4e20b3 100644
--- a/src/connectivity/overnet/lib/routing/router.h
+++ b/src/connectivity/overnet/lib/routing/router.h
@@ -33,14 +33,18 @@
 namespace std {
 template <>
 struct hash<overnet::router_impl::LocalStreamId> {
-  size_t operator()(const overnet::router_impl::LocalStreamId& id) const { return id.Hash(); }
+  size_t operator()(const overnet::router_impl::LocalStreamId& id) const {
+    return id.Hash();
+  }
 };
 }  // namespace std
 
 namespace overnet {
 
 inline auto ForwardingPayloadFactory(Slice payload) {
-  return [payload = std::move(payload)](auto args) mutable { return std::move(payload); };
+  return [payload = std::move(payload)](auto args) mutable {
+    return std::move(payload);
+  };
 }
 
 struct Message final {
@@ -49,7 +53,8 @@
   TimeStamp received;
   uint32_t mss = std::numeric_limits<uint32_t>::max();
 
-  static Message SimpleForwarder(RoutableMessage msg, Slice payload, TimeStamp received) {
+  static Message SimpleForwarder(RoutableMessage msg, Slice payload,
+                                 TimeStamp received) {
     return Message{std::move(msg), ForwardingPayloadFactory(payload), received};
   }
 };
@@ -82,7 +87,6 @@
   };
 
   Router(Timer* timer, NodeId node_id, bool allow_non_determinism);
-
   virtual ~Router();
 
   virtual void Close(Callback<void> quiesced);
@@ -90,8 +94,10 @@
   // Forward a message to either ourselves or a link
   void Forward(Message message);
   // Register a (locally handled) stream into this Router
-  Status RegisterStream(NodeId peer, StreamId stream_id, StreamHandler* stream_handler);
-  Status UnregisterStream(NodeId peer, StreamId stream_id, StreamHandler* stream_handler);
+  Status RegisterStream(NodeId peer, StreamId stream_id,
+                        StreamHandler* stream_handler);
+  Status UnregisterStream(NodeId peer, StreamId stream_id,
+                          StreamHandler* stream_handler);
   // Register a link to another router (usually on a different machine)
   void RegisterLink(LinkPtr<> link);
 
@@ -101,7 +107,8 @@
 
   void UpdateRoutingTable(
       std::initializer_list<fuchsia::overnet::protocol::NodeStatus> node_status,
-      std::initializer_list<fuchsia::overnet::protocol::LinkStatus> link_status) {
+      std::initializer_list<fuchsia::overnet::protocol::LinkStatus>
+          link_status) {
     UpdateRoutingTable(std::move(node_status), std::move(link_status), false);
   }
 
@@ -115,7 +122,8 @@
   }
 
   Optional<NodeId> SelectGossipPeer();
-  void SendGossipUpdate(fuchsia::overnet::protocol::Peer_Proxy* peer, NodeId target);
+  void SendGossipUpdate(fuchsia::overnet::protocol::Peer_Proxy* peer,
+                        NodeId target);
 
   void ApplyGossipUpdate(fuchsia::overnet::protocol::NodeStatus node_status) {
     UpdateRoutingTable({std::move(node_status)}, {}, false);
@@ -143,13 +151,16 @@
     }
   }
 
+  uint64_t GenerateLinkLabel() { return next_link_label_++; }
+
  private:
   Timer* const timer_;
   const NodeId node_id_;
 
-  void UpdateRoutingTable(std::initializer_list<fuchsia::overnet::protocol::NodeStatus> node_status,
-                          std::initializer_list<fuchsia::overnet::protocol::LinkStatus> link_status,
-                          bool flush_old_nodes);
+  void UpdateRoutingTable(
+      std::initializer_list<fuchsia::overnet::protocol::NodeStatus> node_status,
+      std::initializer_list<fuchsia::overnet::protocol::LinkStatus> link_status,
+      bool flush_old_nodes);
   virtual void OnUnknownStream(NodeId peer, StreamId stream_id) {}
 
   void MaybeStartPollingLinkChanges();
@@ -162,7 +173,8 @@
    public:
     StreamHolder(NodeId peer, StreamId id) : peer_(peer), stream_(id) {}
     Status SetHandler(StreamHandler* handler);
-    [[nodiscard]] bool HandleMessage(SeqNum seq, TimeStamp received, Slice payload);
+    [[nodiscard]] bool HandleMessage(SeqNum seq, TimeStamp received,
+                                     Slice payload);
     Status ClearHandler(StreamHandler* handler);
     void Close(Callback<void> quiesced) {
       if (handler_ != nullptr)
@@ -209,7 +221,8 @@
     if (it != links_.end())
       return &it->second;
     return &links_
-                .emplace(std::piecewise_construct, std::forward_as_tuple(node_id),
+                .emplace(std::piecewise_construct,
+                         std::forward_as_tuple(node_id),
                          std::forward_as_tuple(node_id))
                 .first->second;
   }
@@ -219,9 +232,10 @@
     if (it != streams_.end())
       return &it->second;
     return &streams_
-                .emplace(std::piecewise_construct,
-                         std::forward_as_tuple(LocalStreamId{node_id, stream_id}),
-                         std::forward_as_tuple(node_id, stream_id))
+                .emplace(
+                    std::piecewise_construct,
+                    std::forward_as_tuple(LocalStreamId{node_id, stream_id}),
+                    std::forward_as_tuple(node_id, stream_id))
                 .first->second;
   }
 
@@ -232,7 +246,8 @@
     uint64_t target_nodes_label;
 
     bool operator==(const OwnedLabel& other) const {
-      return target_node == other.target_node && target_nodes_label == other.target_nodes_label;
+      return target_node == other.target_node &&
+             target_nodes_label == other.target_nodes_label;
     }
   };
 
@@ -253,6 +268,8 @@
   Optional<Timeout> poll_link_changes_timeout_;
   Optional<Timeout> flush_old_nodes_timeout_;
   fuchsia::overnet::protocol::NodeStatus own_node_status_;
+
+  uint64_t next_link_label_ = 1;
 };
 
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/routing/routing_table.cc b/src/connectivity/overnet/lib/routing/routing_table.cc
index ce62af1..22786f1 100644
--- a/src/connectivity/overnet/lib/routing/routing_table.cc
+++ b/src/connectivity/overnet/lib/routing/routing_table.cc
@@ -133,9 +133,9 @@
   }
   for (const auto& m : changes.links) {
     auto report_drop = [&m](const char* why) {
-      OVERNET_TRACE(INFO) << "Drop link info: from=" << m.from << " to=" << m.to
-                          << " label=" << m.local_id << " version=" << m.version
-                          << ": " << why;
+      OVERNET_TRACE(DEBUG) << "Drop link info: from=" << m.from
+                           << " to=" << m.to << " label=" << m.local_id
+                           << " version=" << m.version << ": " << why;
     };
     // Cannot add a link if the relevant nodes are unknown.
     auto from_node = nodes_.find(NodeId(m.from));
@@ -239,6 +239,13 @@
     return SelectedLinks();  // Root node as yet unknown.
   }
 
+  for (const auto& n : nodes_) {
+    OVERNET_TRACE(DEBUG) << n.first << " metrics\n" << n.second.status.metrics;
+  }
+  for (const auto& l : links_) {
+    OVERNET_TRACE(DEBUG) << l.first << " metrics\n" << l.second.status.metrics;
+  }
+
   ++path_finding_run_;
   node_it->second.last_path_finding_run = path_finding_run_;
   node_it->second.best_rtt = TimeDelta::Zero();
@@ -252,6 +259,15 @@
     todo.PushBack(node);
   };
 
+  auto node_id_of = [this](const Node* n) {
+    for (const auto& np : nodes_) {
+      if (&np.second == n) {
+        return np.first;
+      }
+    }
+    return NodeId(0);
+  };
+
   enqueue(&node_it->second);
 
   while (!todo.Empty()) {
@@ -261,17 +277,31 @@
       if (link->status.version ==
           fuchsia::overnet::protocol::METRIC_VERSION_TOMBSTONE)
         continue;
-      TimeDelta rtt =
+      TimeDelta rtt = std::min(
+          TimeDelta::FromHours(1),
           src->best_rtt +
-          (src->status.metrics.has_forwarding_time()
-               ? TimeDelta::FromMicroseconds(
-                     src->status.metrics.forwarding_time())
-               : TimeDelta::PositiveInf()) +
-          (link->status.metrics.has_rtt()
-               ? TimeDelta::FromMicroseconds(link->status.metrics.rtt())
-               : TimeDelta::PositiveInf());
+              (src->status.metrics.has_forwarding_time()
+                   ? TimeDelta::FromMicroseconds(
+                         src->status.metrics.forwarding_time())
+                   : TimeDelta::PositiveInf()) +
+              (link->status.metrics.has_rtt()
+                   ? TimeDelta::FromMicroseconds(link->status.metrics.rtt())
+                   : TimeDelta::PositiveInf()));
       Node* dst = link->to_node;
       // For now we order by RTT.
+      OVERNET_TRACE(DEBUG) << "RB[" << root_node_
+                           << "]: src=" << node_id_of(src)
+                           << " dst=" << node_id_of(dst)
+                           << " dst->last_path_finding_run="
+                           << dst->last_path_finding_run
+                           << " path_finding_run=" << path_finding_run_
+                           << " src->best_rtt=" << src->best_rtt
+                           << " dst->best_rtt=" << dst->best_rtt
+                           << " rtt=" << rtt << " src->mss=" << src->mss
+                           << " link->mss="
+                           << (link->status.metrics.has_mss()
+                                   ? link->status.metrics.mss()
+                                   : std::numeric_limits<uint32_t>::max());
       if (dst->last_path_finding_run != path_finding_run_ ||
           dst->best_rtt > rtt) {
         dst->last_path_finding_run = path_finding_run_;
@@ -290,21 +320,23 @@
   SelectedLinks selected_links;
 
   for (node_it = nodes_.begin(); node_it != nodes_.end(); ++node_it) {
-    if (node_it->second.last_path_finding_run != path_finding_run_) {
-      continue;  // Unreachable
-    }
     if (node_it->first == root_node_) {
       continue;
     }
+    if (node_it->second.last_path_finding_run != path_finding_run_) {
+      OVERNET_TRACE(DEBUG) << "RB[" << root_node_ << "]: " << node_it->first
+                           << " unreachable";
+      continue;  // Unreachable
+    }
     Node* n = &node_it->second;
     while (n->best_from->status.id != root_node_) {
       n = n->best_from;
     }
     Link* link = n->best_link;
     assert(link->status.from == root_node_);
-    selected_links.emplace(
-        node_it->first,
-        SelectedLink{link->status.to, link->status.local_id, n->mss});
+    selected_links.emplace(node_it->first,
+                           SelectedLink{link->status.to, link->status.local_id,
+                                        node_it->second.mss});
   }
 
   return selected_links;
@@ -312,12 +344,13 @@
 
 uint64_t RoutingTable::SendUpdate(fuchsia::overnet::protocol::Peer_Proxy* peer,
                                   Optional<NodeId> exclude_node) const {
-  OVERNET_TRACE(DEBUG) << "SendUpdate";
+  OVERNET_TRACE(DEBUG) << root_node_ << " SendUpdate exclude=" << exclude_node;
 
   std::lock_guard<std::mutex> mutex(shared_table_mu_);
   std::unordered_set<NodeId> version_zero_nodes;
 
   for (const auto& m : shared_node_status_) {
+    OVERNET_TRACE(DEBUG) << "Consider node: " << m;
     if (m.version == 0) {
       version_zero_nodes.insert(NodeId(m.id));
       continue;
@@ -330,6 +363,7 @@
   }
 
   for (const auto& m : shared_link_status_) {
+    OVERNET_TRACE(DEBUG) << "Consider link: " << m;
     if (NodeId(m.from) == exclude_node ||
         version_zero_nodes.count(NodeId(m.from)) > 0 ||
         version_zero_nodes.count(NodeId(m.to)) > 0) {
diff --git a/src/connectivity/overnet/lib/routing/routing_table.h b/src/connectivity/overnet/lib/routing/routing_table.h
index 505212a..4b18651 100644
--- a/src/connectivity/overnet/lib/routing/routing_table.h
+++ b/src/connectivity/overnet/lib/routing/routing_table.h
@@ -10,6 +10,7 @@
 #include <condition_variable>
 #include <iostream>
 #include <mutex>
+#include <ostream>
 #include <thread>
 #include <tuple>
 #include <unordered_map>
@@ -37,6 +38,10 @@
   return a.from == b.from && a.to == b.to && a.link_label == b.link_label;
 }
 
+inline std::ostream& operator<<(std::ostream& out, FullLinkLabel lbl) {
+  return out << lbl.from << "->" << lbl.to << "#" << lbl.link_label;
+}
+
 }  // namespace routing_table_impl
 }  // namespace overnet
 
diff --git a/src/connectivity/overnet/lib/stats/BUILD.gn b/src/connectivity/overnet/lib/stats/BUILD.gn
index 8935cf7..097494f 100644
--- a/src/connectivity/overnet/lib/stats/BUILD.gn
+++ b/src/connectivity/overnet/lib/stats/BUILD.gn
@@ -31,12 +31,7 @@
 }
 
 stats("link") {}
-
-group("lib") {
-    public_deps = [
-        ":link",
-    ]
-}
+stats("stream") {}
 
 source_set("visitor") {
     sources = [
diff --git a/src/connectivity/overnet/lib/stats/link.py b/src/connectivity/overnet/lib/stats/link.py
index 152f9fc..8d7c620 100755
--- a/src/connectivity/overnet/lib/stats/link.py
+++ b/src/connectivity/overnet/lib/stats/link.py
@@ -12,5 +12,28 @@
     stats = [
         statsc.Counter(name='incoming_packet_count'),
         statsc.Counter(name='outgoing_packet_count'),
+
+        statsc.Counter(name='unseen_packets_marked_not_received'),
+        statsc.Counter(name='acks_sent'),
+        statsc.Counter(name='pure_acks_sent'),
+        statsc.Counter(name='tail_loss_probes_scheduled'),
+        statsc.Counter(name='tail_loss_probes_cancelled_because_requests_already_queued'),
+        statsc.Counter(name='tail_loss_probes_cancelled_because_probe_already_scheduled'),
+        statsc.Counter(name='tail_loss_probes_cancelled_after_timer_created'),
+
+        statsc.Counter(name='tail_loss_probe_scheduled_because_ack_required_soon_timer_expired'),
+        statsc.Counter(name='tail_loss_probe_scheduled_because_send_queue_is_empty'),
+
+        statsc.Counter(name='ack_not_required_historic_sequence'),
+        statsc.Counter(name='ack_not_required_frozen_sequence'),
+        statsc.Counter(name='ack_not_required_invalid_packet'),
+        statsc.Counter(name='ack_not_required_short_optional_run'),
+        statsc.Counter(name='ack_required_soon_ack_received'),
+        statsc.Counter(name='ack_required_soon_data_received'),
+        statsc.Counter(name='ack_required_soon_continue_partial_after_ack'),
+        statsc.Counter(name='ack_required_soon_all_acks_nacked'),
+        statsc.Counter(name='ack_required_immediately_due_to_nack'),
+        statsc.Counter(name='ack_required_immediately_due_to_partial_ack'),
+        statsc.Counter(name='ack_required_immediately_due_to_multiple_receives'),
     ]
 )
diff --git a/src/connectivity/overnet/lib/stats/stream.py b/src/connectivity/overnet/lib/stats/stream.py
new file mode 100755
index 0000000..827e087
--- /dev/null
+++ b/src/connectivity/overnet/lib/stats/stream.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import statsc
+
+statsc.compile(
+    name='Stream',
+    include='src/connectivity/overnet/lib/stats/stream.h',
+    stats = [
+        statsc.Counter(name='linearizer_reject_past_end_of_buffering'),
+        statsc.Counter(name='linearizer_empty_chunk'),
+        statsc.Counter(name='linearizer_fast_path_taken'),
+        statsc.Counter(name='linearizer_ignore_all_prior'),
+        statsc.Counter(name='linearizer_partial_ignore_begin'),
+        statsc.Counter(name='linearizer_new_pending_queue'),
+        statsc.Counter(name='linearizer_integrations'),
+        statsc.Counter(name='linearizer_integration_inserts'),
+        statsc.Counter(name='linearizer_integration_errors'),
+        statsc.Counter(name='linearizer_integration_coincident_shorter'),
+        statsc.Counter(name='linearizer_integration_coincident_longer'),
+        statsc.Counter(name='linearizer_integration_prior_longer'),
+        statsc.Counter(name='linearizer_integration_prior_partial'),
+        statsc.Counter(name='linearizer_integration_subsequent_splits'),
+        statsc.Counter(name='linearizer_integration_subsequent_covers'),
+        statsc.Counter(name='send_chunk_cancel_packet_too_small'),
+        statsc.Counter(name='send_chunk_split_packet_too_small'),
+        statsc.Counter(name='send_chunk_take_entire_chunk'),
+        statsc.Counter(name='send_chunk_nacked'),
+        statsc.Counter(name='send_chunk_push'),
+    ]
+)
diff --git a/src/connectivity/overnet/lib/vocabulary/ip_addr.cc b/src/connectivity/overnet/lib/vocabulary/ip_addr.cc
index a821c9b..3e4d70d 100644
--- a/src/connectivity/overnet/lib/vocabulary/ip_addr.cc
+++ b/src/connectivity/overnet/lib/vocabulary/ip_addr.cc
@@ -3,12 +3,27 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/vocabulary/ip_addr.h"
+
 #include <arpa/inet.h>
 #include <string.h>
+
 #include <ostream>
 
 namespace overnet {
 
+#ifndef __Fuchsia__
+Optional<IpAddr> IpAddr::Unix(const std::string& name) {
+  IpAddr out;
+  if (name.length() >= sizeof(out.unix.sun_path)) {
+    return Nothing;
+  }
+  memset(&out, 0, sizeof(out));
+  out.unix.sun_family = AF_UNIX;
+  memcpy(out.unix.sun_path, name.data(), name.length());
+  return out;
+}
+#endif
+
 IpAddr IpAddr::AnyIpv4() {
   IpAddr out;
   memset(&out, 0, sizeof(out));
@@ -23,6 +38,21 @@
   return out;
 }
 
+socklen_t IpAddr::length() const {
+  switch (addr.sa_family) {
+    case AF_INET:
+      return sizeof(ipv4);
+    case AF_INET6:
+      return sizeof(ipv6);
+#ifndef __Fuchsia__
+    case AF_UNIX:
+      return SUN_LEN(&unix);
+#endif
+    default:
+      return sizeof(*this);
+  }
+}
+
 Optional<IpAddr> IpAddr::WithPort(uint16_t port) const {
   IpAddr out = *this;
   switch (out.addr.sa_family) {
@@ -64,6 +94,10 @@
     case AF_INET6:
       inet_ntop(AF_INET6, &addr.ipv6.sin6_addr, dst, sizeof(dst));
       return out << dst << "." << ntohs(addr.ipv6.sin6_port);
+#ifndef __Fuchsia__
+    case AF_UNIX:
+      return out << addr.unix.sun_path;
+#endif
     default:
       return out << "<<unknown address family " << addr.addr.sa_family << ">>";
   }
@@ -87,6 +121,11 @@
       add_value(addr.ipv6.sin6_addr);
       add_value(addr.ipv6.sin6_port);
       break;
+#ifndef __Fuchsia__
+    case AF_UNIX:
+      add_value(addr.unix.sun_path);
+      break;
+#endif
   }
   return out;
 }
@@ -102,6 +141,10 @@
         return a.ipv6.sin6_port == b.ipv6.sin6_port &&
                0 == memcmp(&a.ipv6.sin6_addr, &b.ipv6.sin6_addr,
                            sizeof(a.ipv6.sin6_addr));
+#ifndef __Fuchsia__
+      case AF_UNIX:
+        return 0 == strcmp(a.unix.sun_path, b.unix.sun_path);
+#endif
     }
   }
   if (auto a6 = a.AsIpv6(); a6.has_value()) {
diff --git a/src/connectivity/overnet/lib/vocabulary/ip_addr.h b/src/connectivity/overnet/lib/vocabulary/ip_addr.h
index 5eee649..3f1b83e 100644
--- a/src/connectivity/overnet/lib/vocabulary/ip_addr.h
+++ b/src/connectivity/overnet/lib/vocabulary/ip_addr.h
@@ -5,7 +5,11 @@
 #pragma once
 
 #include <netinet/in.h>
+#ifndef __Fuchsia__
+#include <sys/un.h>
+#endif
 #include <iosfwd>
+
 #include "src/connectivity/overnet/lib/vocabulary/optional.h"
 
 namespace overnet {
@@ -14,12 +18,19 @@
   sockaddr_in ipv4;
   sockaddr_in6 ipv6;
   sockaddr addr;
+#ifndef __Fuchsia__
+  sockaddr_un unix;
+  static Optional<IpAddr> Unix(const std::string& name);
+#endif
 
   Optional<IpAddr> WithPort(uint16_t port) const;
 
   static IpAddr AnyIpv4();
   static IpAddr AnyIpv6();
 
+  const sockaddr* get() const { return &addr; }
+  socklen_t length() const;
+
   Optional<IpAddr> AsIpv6() const;
   uint16_t port() const {
     switch (addr.sa_family) {
diff --git a/src/connectivity/overnet/lib/vocabulary/optional.h b/src/connectivity/overnet/lib/vocabulary/optional.h
index 6d86ce74..fd1208f 100644
--- a/src/connectivity/overnet/lib/vocabulary/optional.h
+++ b/src/connectivity/overnet/lib/vocabulary/optional.h
@@ -5,7 +5,9 @@
 #pragma once
 
 #include <assert.h>
+
 #include <ostream>
+
 #include "src/connectivity/overnet/lib/vocabulary/manual_constructor.h"
 
 namespace overnet {
@@ -107,7 +109,8 @@
     assert(set_);
     return *storage_.get();
   }
-  operator bool() const { return set_; }
+  typedef bool Optional::*FakeBool;
+  operator FakeBool() const { return set_ ? &Optional::set_ : nullptr; }
   bool has_value() const { return set_; }
   T& value() {
     assert(set_);
@@ -120,6 +123,14 @@
   T* get() { return set_ ? storage_.get() : nullptr; }
   const T* get() const { return set_ ? storage_.get() : nullptr; }
 
+  T* Force() {
+    if (!set_) {
+      set_ = true;
+      storage_.Init();
+    }
+    return get();
+  }
+
   T Take() {
     assert(set_);
     set_ = false;
diff --git a/src/connectivity/overnet/lib/vocabulary/socket.cc b/src/connectivity/overnet/lib/vocabulary/socket.cc
index dbf03c8..7508b1d 100644
--- a/src/connectivity/overnet/lib/vocabulary/socket.cc
+++ b/src/connectivity/overnet/lib/vocabulary/socket.cc
@@ -3,7 +3,10 @@
 // found in the LICENSE file.
 
 #include "src/connectivity/overnet/lib/vocabulary/socket.h"
+
+#include <fcntl.h>
 #include <unistd.h>
+
 #include <cstring>
 #include <sstream>
 
@@ -16,6 +19,18 @@
   }
 }
 
+Status Socket::Create(int family, int type, int option) {
+  *this = Socket(socket(family, type, option));
+  if (socket_ == -1) {
+    std::ostringstream msg;
+    msg << "Creating socket family=" << family << " type=" << type
+        << " option=" << option;
+    return Status(StatusCode::UNKNOWN, strerror(errno))
+        .WithContext(msg.str().c_str());
+  }
+  return Status::Ok();
+}
+
 Status Socket::SetOptReusePort(bool reuse) {
   return SetOpt(SOL_SOCKET, SO_REUSEPORT, reuse ? 1 : 0)
       .WithContext(reuse ? "Enable SO_REUSEPORT" : "Disable SO_REUSEPORT");
@@ -26,27 +41,74 @@
     return Status(StatusCode::INVALID_ARGUMENT, "Invalid socket");
   }
   if (setsockopt(socket_, level, opt, value, value_size) < 0) {
-    return Status(StatusCode::INVALID_ARGUMENT, strerror(errno));
+    return Status(StatusCode::UNKNOWN, strerror(errno));
   }
   return Status::Ok();
 }
 
+Status Socket::MutateFlags(std::function<int(int)> mut) {
+  if (!IsValid()) {
+    return Status(StatusCode::INVALID_ARGUMENT, "Invalid socket");
+  }
+  int flags = fcntl(socket_, F_GETFL);
+  if (flags == -1) {
+    return Status(StatusCode::UNKNOWN, strerror(errno))
+        .WithContext("fcntl(F_GETFL)");
+  }
+  flags = mut(flags);
+  if (0 != fcntl(socket_, F_SETFL, flags)) {
+    return Status(StatusCode::UNKNOWN, strerror(errno))
+        .WithContext("fcntl(F_SETFL)");
+  }
+  return Status::Ok();
+}
+
+Status Socket::SetNonBlocking(bool non_blocking) {
+  if (non_blocking) {
+    return MutateFlags([](int flags) { return flags | O_NONBLOCK; });
+  } else {
+    return MutateFlags([](int flags) { return flags ^ ~O_NONBLOCK; });
+  }
+}
+
 Status Socket::Bind(IpAddr addr) {
   if (!IsValid()) {
     return Status(StatusCode::INVALID_ARGUMENT, "Invalid socket");
   }
-  if (bind(socket_, &addr.addr, sizeof(addr)) < 0) {
-    auto err = errno;
-    std::ostringstream msg;
-    msg << "Bind to " << addr << " failed: " << strerror(err);
-    return Status(StatusCode::INVALID_ARGUMENT, msg.str());
+#ifndef __Fuchsia__
+  // If we're using unix domain sockets, newest process always gets to handle
+  // the socket.
+  if (addr.addr.sa_family == AF_UNIX) {
+    unlink(addr.unix.sun_path);
+  }
+#endif
+  if (bind(socket_, addr.get(), addr.length()) < 0) {
+    return Status(StatusCode::UNKNOWN, strerror(errno)).WithLazyContext([&] {
+      std::ostringstream out;
+      out << "bind(" << addr << ")";
+      return out.str();
+    });
+  }
+  return Status::Ok();
+}
+
+Status Socket::Connect(IpAddr addr) {
+  if (!IsValid()) {
+    return Status(StatusCode::INVALID_ARGUMENT, "Invalid socket");
+  }
+  if (0 != connect(socket_, addr.get(), addr.length())) {
+    return Status(StatusCode::UNKNOWN, strerror(errno)).WithLazyContext([&] {
+      std::ostringstream out;
+      out << "connect(" << addr << ")";
+      return out.str();
+    });
   }
   return Status::Ok();
 }
 
 Status Socket::SendTo(Slice data, int flags, IpAddr dest) {
   const auto result = sendto(socket_, data.begin(), data.length(), flags,
-                             &dest.addr, sizeof(dest));
+                             dest.get(), dest.length());
   if (result < 0) {
     return Status(StatusCode::UNKNOWN, strerror(errno))
         .WithContext("sendto failed");
@@ -75,4 +137,59 @@
   return DataAndAddr{std::move(msg), std::move(source_address)};
 }
 
+Status Socket::Listen() {
+  if (listen(socket_, 0) == 0) {
+    return Status::Ok();
+  } else if (errno == EINTR) {
+    return Listen();
+  } else {
+    return Status(StatusCode::UNKNOWN, strerror(errno)).WithContext("listen");
+  }
+}
+
+StatusOr<Socket> Socket::Accept() {
+  int socket = accept(socket_, nullptr, nullptr);
+  if (socket >= 0) {
+    return Socket(socket);
+  } else if (errno == EAGAIN) {
+    return Status::Unavailable();
+  } else if (errno == EINTR) {
+    return Accept();
+  } else {
+    return Status(StatusCode::UNKNOWN, strerror(errno)).WithContext("accept");
+  }
+}
+
+StatusOr<Slice> Socket::Write(Slice slice) {
+  if (auto n = write(socket_, slice.begin(), slice.length()); n >= 0) {
+    return slice.FromOffset(n);
+  } else if (errno == EAGAIN) {
+    return slice;
+  } else if (errno == EINTR) {
+    return Write(std::move(slice));
+  } else {
+    return Status(StatusCode::UNKNOWN, strerror(errno)).WithContext("write");
+  }
+}
+
+StatusOr<Optional<Slice>> Socket::Read(size_t maximum_read_size) {
+  auto msg = Slice::WithInitializer(maximum_read_size, [](uint8_t*) {});
+  ssize_t n;
+  for (;;) {
+    n = read(socket_, const_cast<uint8_t*>(msg.begin()), msg.length());
+    if (n == 0) {
+      return Nothing;
+    } else if (n > 0) {
+      return msg.ToOffset(n);
+    } else if (errno == EAGAIN) {
+      return Slice();
+    } else if (errno == EINTR) {
+      continue;
+    } else {
+      return Status(StatusCode::UNKNOWN, strerror(errno)).WithContext("read");
+    }
+    abort();
+  }
+}
+
 }  // namespace overnet
diff --git a/src/connectivity/overnet/lib/vocabulary/socket.h b/src/connectivity/overnet/lib/vocabulary/socket.h
index 3b4091c..15feb60 100644
--- a/src/connectivity/overnet/lib/vocabulary/socket.h
+++ b/src/connectivity/overnet/lib/vocabulary/socket.h
@@ -5,7 +5,9 @@
 #pragma once
 
 #include <cstring>
+#include <functional>
 #include <sstream>
+
 #include "src/connectivity/overnet/lib/vocabulary/ip_addr.h"
 #include "src/connectivity/overnet/lib/vocabulary/slice.h"
 #include "src/connectivity/overnet/lib/vocabulary/status.h"
@@ -32,8 +34,12 @@
   bool IsValid() const { return socket_ != -1; }
   int get() const { return socket_; }
 
+  Status Create(int family, int type, int option);
   Status SetOptReusePort(bool reuse);
 
+  Status MutateFlags(std::function<int(int)> mutator);
+  Status SetNonBlocking(bool non_blocking);
+
   template <class T>
   Status SetOpt(int level, int opt, T value) {
     return SetOpt(level, opt, &value, sizeof(value));
@@ -42,6 +48,12 @@
   Status SetOpt(int level, int opt, void* value, size_t value_size);
   Status Bind(IpAddr addr);
   Status SendTo(Slice data, int flags, IpAddr dest);
+  Status Listen();
+  Status Connect(IpAddr dest);
+  StatusOr<Socket> Accept();
+  // Returns the data that was not written, or error.
+  StatusOr<Slice> Write(Slice data);
+  StatusOr<Optional<Slice>> Read(size_t maximum_read_size);
 
   struct DataAndAddr {
     Slice data;
diff --git a/src/connectivity/overnet/overnetstack/omdp_nub.cc b/src/connectivity/overnet/overnetstack/omdp_nub.cc
index 4ac4c1b..7387591 100644
--- a/src/connectivity/overnet/overnetstack/omdp_nub.cc
+++ b/src/connectivity/overnet/overnetstack/omdp_nub.cc
@@ -29,9 +29,9 @@
   if (incoming_.IsValid()) {
     return;
   }
-  incoming_ = overnet::Socket(socket(AF_INET6, SOCK_DGRAM, 0));
   auto status =
-      incoming_.SetOptReusePort(true)
+      incoming_.Create(AF_INET6, SOCK_DGRAM, 0)
+          .Then([&] { return incoming_.SetOptReusePort(true); })
           .Then([&] {
             return incoming_.Bind(*overnet::IpAddr::AnyIpv6().WithPort(
                 kMulticastGroupAddr.port()));
diff --git a/src/connectivity/overnet/overnetstack/service.cc b/src/connectivity/overnet/overnetstack/service.cc
index 9bcffbb..a046161 100644
--- a/src/connectivity/overnet/overnetstack/service.cc
+++ b/src/connectivity/overnet/overnetstack/service.cc
@@ -21,8 +21,15 @@
   using Peer = fuchsia::overnet::Peer;
   app_->endpoint()->OnNodeDescriptionTableChange(
       last_seen_version,
-      overnet::Callback<void>(
-          overnet::ALLOCATED_CALLBACK, [this, callback = std::move(callback)] {
+      overnet::StatusCallback(
+          overnet::ALLOCATED_CALLBACK, [this, callback = std::move(callback)](
+                                           const overnet::Status& status) {
+            if (status.is_error()) {
+              // Note: callback not called, but this case should imply that
+              // we're shutting down Overnet, so the associated channel should
+              // disappear also.
+              return;
+            }
             std::vector<Peer> response;
             auto new_version = app_->endpoint()->ForEachNodeDescription(
                 [&response, self_node = app_->endpoint()->node_id()](
diff --git a/src/connectivity/overnet/overnetstack/udp_nub.h b/src/connectivity/overnet/overnetstack/udp_nub.h
index ce7c7f2..db97b18 100644
--- a/src/connectivity/overnet/overnetstack/udp_nub.h
+++ b/src/connectivity/overnet/overnetstack/udp_nub.h
@@ -27,7 +27,7 @@
 class UdpNub final : public UdpNubBase, public OvernetApp::Actor {
  public:
   explicit UdpNub(OvernetApp* app)
-      : UdpNubBase(app->timer(), app->node_id()),
+      : UdpNubBase(app->endpoint()),
         endpoint_(app->endpoint()),
         timer_(app->timer()) {}
 
@@ -54,8 +54,6 @@
     }
   }
 
-  overnet::Router* GetRouter() override { return endpoint_; }
-
   void Publish(overnet::LinkPtr<> link) override {
     overnet::NodeId node = overnet::NodeId(link->GetLinkStatus().to);
     OVERNET_TRACE(DEBUG) << "NewLink: " << node << "\n";
diff --git a/src/connectivity/overnet/tools/BUILD.gn b/src/connectivity/overnet/tools/BUILD.gn
index 6cd74ed..0df3387 100644
--- a/src/connectivity/overnet/tools/BUILD.gn
+++ b/src/connectivity/overnet/tools/BUILD.gn
@@ -5,6 +5,7 @@
 group("tools") {
   testonly = true
   deps = [
+    "ascendd",
     "onet",
   ]
 }
diff --git a/src/connectivity/overnet/tools/ascendd/BUILD.gn b/src/connectivity/overnet/tools/ascendd/BUILD.gn
new file mode 100644
index 0000000..f49490ef
--- /dev/null
+++ b/src/connectivity/overnet/tools/ascendd/BUILD.gn
@@ -0,0 +1,27 @@
+# Copyright 2019 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/fidl/fidl.gni")
+import("//build/package.gni")
+
+executable("bin") {
+  output_name = "ascendd"
+
+  sources = [ "ascendd.cc" ]
+  deps = [
+    "//src/connectivity/overnet/lib/embedded:basic_embedded",
+    "//src/connectivity/overnet/lib/embedded:stream_server",
+    "//src/connectivity/overnet/lib/embedded:udp_nub",
+    "//src/connectivity/overnet/lib/embedded:omdp_nub",
+    "//src/connectivity/overnet/lib/protocol:reliable_framer",
+    "//src/connectivity/overnet/lib/protocol:unreliable_framer",
+    "//third_party/gflags",
+  ]
+}
+
+group("ascendd") {
+  deps = [
+    ":bin(${host_toolchain})",
+  ]
+}
diff --git a/src/connectivity/overnet/tools/ascendd/ascendd.cc b/src/connectivity/overnet/tools/ascendd/ascendd.cc
new file mode 100644
index 0000000..721a172
--- /dev/null
+++ b/src/connectivity/overnet/tools/ascendd/ascendd.cc
@@ -0,0 +1,46 @@
+// Copyright 2019 The Fuchsia 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 <gflags/gflags.h>
+
+#include "src/connectivity/overnet/lib/embedded/basic_overnet_embedded.h"
+#include "src/connectivity/overnet/lib/embedded/omdp_nub.h"
+#include "src/connectivity/overnet/lib/embedded/stream_server.h"
+#include "src/connectivity/overnet/lib/embedded/udp_nub.h"
+#include "src/connectivity/overnet/lib/protocol/reliable_framer.h"
+
+DEFINE_bool(udp, false, "Support Overnet over UDP");
+DEFINE_bool(omdp, true,
+            "Support OMDP discovery protocol for UDP communications");
+DEFINE_string(unix_socket, "/tmp/ascendd.socket",
+              "UNIX domain socket path for ascendd Overnet server");
+DEFINE_string(verbosity, "INFO", "Verbosity level");
+DEFINE_validator(
+    verbosity, +[](const char* flag_name, const std::string& value) {
+      return overnet::SeverityFromString(value).has_value();
+    });
+
+int main(int argc, char** argv) {
+  gflags::ParseCommandLineFlags(&argc, &argv, true);
+
+  overnet::ScopedSeverity trace_severity(
+      *overnet::SeverityFromString(FLAGS_verbosity));
+
+  overnet::BasicOvernetEmbedded overnet_embedded;
+  overnet::StreamServer<overnet::ReliableFramer> stream_server(
+      &overnet_embedded, *overnet::IpAddr::Unix(FLAGS_unix_socket));
+
+  // Optional services
+  overnet::Optional<overnet::UdpNub> udp_nub;
+  overnet::Optional<overnet::OmdpNub> omdp_nub;
+
+  if (FLAGS_udp) {
+    udp_nub.Reset(&overnet_embedded);
+  }
+  if (udp_nub.has_value() && FLAGS_omdp) {
+    omdp_nub.Reset(&overnet_embedded, udp_nub.get());
+  }
+
+  return overnet_embedded.Run();
+}