ipcz: Introduce RouterLinkState

Introduces the RouterLinkState structure shared between both sides of a
RouterLink. Also extends the RouterLink interface to include methods for
cooperation via RouterLinkState, e.g. for locking the link and
authenticating bypass requests.

The main point of this CL is just to establish the RouterLinkState type
and implement its core set of atomic operations that will support
orderly route mutations.

Only initial RouterLinkStates (for ConnectNode() portals) are
established here.

Bug: 1299283
Change-Id: I71e9957d3594d785c6676ea4871b2e3f770f94e5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3751248
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Commit-Queue: Ken Rockot <rockot@google.com>
Cr-Commit-Position: refs/heads/main@{#1022922}
NOKEYCHECK=True
GitOrigin-RevId: 8bdf7d2c448c12106bcd4938dec342469358abf5
diff --git a/src/BUILD.gn b/src/BUILD.gn
index 85a4b98..461339b 100644
--- a/src/BUILD.gn
+++ b/src/BUILD.gn
@@ -221,18 +221,22 @@
     "ipcz/fragment_ref.h",
     "ipcz/link_side.h",
     "ipcz/link_type.h",
+    "ipcz/local_router_link.h",
     "ipcz/message.h",
     "ipcz/node.h",
     "ipcz/node_connector.h",
     "ipcz/node_link.h",
     "ipcz/node_link_memory.h",
     "ipcz/node_messages.h",
+    "ipcz/node_name.h",
     "ipcz/parcel.h",
     "ipcz/parcel_queue.h",
     "ipcz/portal.h",
     "ipcz/ref_counted_fragment.h",
     "ipcz/remote_router_link.h",
     "ipcz/router.h",
+    "ipcz/router_link.h",
+    "ipcz/router_link_state.h",
     "ipcz/sequence_number.h",
     "ipcz/sequenced_queue.h",
     "ipcz/sublink_id.h",
@@ -257,7 +261,6 @@
     "ipcz/link_side.cc",
     "ipcz/link_type.cc",
     "ipcz/local_router_link.cc",
-    "ipcz/local_router_link.h",
     "ipcz/message.cc",
     "ipcz/message_macros/message_declaration_macros.h",
     "ipcz/message_macros/message_definition_macros.h",
@@ -274,7 +277,6 @@
     "ipcz/node_messages.cc",
     "ipcz/node_messages_generator.h",
     "ipcz/node_name.cc",
-    "ipcz/node_name.h",
     "ipcz/parcel.cc",
     "ipcz/parcel_queue.cc",
     "ipcz/portal.cc",
@@ -283,7 +285,7 @@
     "ipcz/router.cc",
     "ipcz/router_descriptor.cc",
     "ipcz/router_descriptor.h",
-    "ipcz/router_link.h",
+    "ipcz/router_link_state.cc",
     "ipcz/test_messages.cc",
     "ipcz/test_messages_generator.h",
     "ipcz/trap_event_dispatcher.cc",
@@ -335,6 +337,7 @@
     "ipcz/node_link_test.cc",
     "ipcz/parcel_queue_test.cc",
     "ipcz/ref_counted_fragment_test.cc",
+    "ipcz/router_link_test.cc",
     "ipcz/sequenced_queue_test.cc",
     "reference_drivers/sync_reference_driver_test.cc",
     "remote_portal_test.cc",
diff --git a/src/ipcz/driver_transport.cc b/src/ipcz/driver_transport.cc
index d5865d4..f78971c 100644
--- a/src/ipcz/driver_transport.cc
+++ b/src/ipcz/driver_transport.cc
@@ -58,6 +58,31 @@
 
 DriverTransport::~DriverTransport() = default;
 
+// static
+DriverTransport::Pair DriverTransport::CreatePair(
+    const IpczDriver& driver,
+    const DriverTransport* transport0,
+    const DriverTransport* transport1) {
+  IpczDriverHandle new_transport0;
+  IpczDriverHandle new_transport1;
+  IpczDriverHandle target_transport0 = IPCZ_INVALID_DRIVER_HANDLE;
+  IpczDriverHandle target_transport1 = IPCZ_INVALID_DRIVER_HANDLE;
+  if (transport0) {
+    ABSL_ASSERT(transport1);
+    target_transport0 = transport0->driver_object().handle();
+    target_transport1 = transport1->driver_object().handle();
+  }
+  IpczResult result = driver.CreateTransports(
+      target_transport0, target_transport1, IPCZ_NO_FLAGS, nullptr,
+      &new_transport0, &new_transport1);
+  ABSL_ASSERT(result == IPCZ_RESULT_OK);
+  auto first =
+      MakeRefCounted<DriverTransport>(DriverObject(driver, new_transport0));
+  auto second =
+      MakeRefCounted<DriverTransport>(DriverObject(driver, new_transport1));
+  return {std::move(first), std::move(second)};
+}
+
 IpczDriverHandle DriverTransport::Release() {
   return transport_.release();
 }
diff --git a/src/ipcz/driver_transport.h b/src/ipcz/driver_transport.h
index 1dfadc4..a9f79f5 100644
--- a/src/ipcz/driver_transport.h
+++ b/src/ipcz/driver_transport.h
@@ -58,6 +58,15 @@
   // handle in `transport`.
   explicit DriverTransport(DriverObject transport);
 
+  // Creates a new pair of connected DriverTransports, one to send over
+  // `transport0`, and one to send over `transport1`, in order to establish a
+  // direct link between their respective remote nodes. Both `transport0` and
+  // `transport1` may be null if the new transport pair won't be sent anywhere.
+  static DriverTransport::Pair CreatePair(
+      const IpczDriver& driver,
+      const DriverTransport* transport0 = nullptr,
+      const DriverTransport* transport1 = nullptr);
+
   // Set the object handling any incoming message or error notifications. This
   // is only safe to set before Activate() is called, or from within one of the
   // Listener methods when invoked by this DriverTransport (because invocations
diff --git a/src/ipcz/local_router_link.cc b/src/ipcz/local_router_link.cc
index 3d0ce11..5b8f590 100644
--- a/src/ipcz/local_router_link.cc
+++ b/src/ipcz/local_router_link.cc
@@ -10,6 +10,7 @@
 #include "ipcz/link_side.h"
 #include "ipcz/link_type.h"
 #include "ipcz/router.h"
+#include "ipcz/router_link_state.h"
 #include "third_party/abseil-cpp/absl/synchronization/mutex.h"
 #include "util/ref_counted.h"
 
@@ -24,6 +25,8 @@
 
   LinkType type() const { return type_; }
 
+  RouterLinkState& link_state() { return link_state_; }
+
   Ref<Router> GetRouter(LinkSide side) {
     absl::MutexLock lock(&mutex_);
     switch (side.value()) {
@@ -54,19 +57,21 @@
   const LinkType type_;
 
   absl::Mutex mutex_;
+  RouterLinkState link_state_;
   Ref<Router> router_a_ ABSL_GUARDED_BY(mutex_);
   Ref<Router> router_b_ ABSL_GUARDED_BY(mutex_);
 };
 
 // static
-void LocalRouterLink::ConnectRouters(LinkType type,
-                                     const Router::Pair& routers) {
+RouterLink::Pair LocalRouterLink::ConnectRouters(LinkType type,
+                                                 const Router::Pair& routers) {
   ABSL_ASSERT(type == LinkType::kCentral || type == LinkType::kBridge);
   auto state = MakeRefCounted<SharedState>(type, routers.first, routers.second);
-  routers.first->SetOutwardLink(
-      AdoptRef(new LocalRouterLink(LinkSide::kA, state)));
-  routers.second->SetOutwardLink(
-      AdoptRef(new LocalRouterLink(LinkSide::kB, state)));
+  auto a = AdoptRef(new LocalRouterLink(LinkSide::kA, state));
+  auto b = AdoptRef(new LocalRouterLink(LinkSide::kB, state));
+  routers.first->SetOutwardLink(a);
+  routers.second->SetOutwardLink(b);
+  return {a, b};
 }
 
 LocalRouterLink::LocalRouterLink(LinkSide side, Ref<SharedState> state)
@@ -78,6 +83,10 @@
   return state_->type();
 }
 
+RouterLinkState* LocalRouterLink::GetLinkState() const {
+  return &state_->link_state();
+}
+
 bool LocalRouterLink::HasLocalPeer(const Router& router) {
   return state_->GetRouter(side_.opposite()).get() == &router;
 }
@@ -99,6 +108,48 @@
   }
 }
 
+void LocalRouterLink::MarkSideStable() {
+  state_->link_state().SetSideStable(side_);
+}
+
+bool LocalRouterLink::TryLockForBypass(const NodeName& bypass_request_source) {
+  if (!state_->link_state().TryLock(side_)) {
+    return false;
+  }
+
+  state_->link_state().allowed_bypass_request_source = bypass_request_source;
+
+  // Balanced by an acquire in CanNodeRequestBypass().
+  std::atomic_thread_fence(std::memory_order_release);
+  return true;
+}
+
+bool LocalRouterLink::TryLockForClosure() {
+  return state_->link_state().TryLock(side_);
+}
+
+void LocalRouterLink::Unlock() {
+  state_->link_state().Unlock(side_);
+}
+
+bool LocalRouterLink::FlushOtherSideIfWaiting() {
+  const LinkSide other_side = side_.opposite();
+  if (state_->link_state().ResetWaitingBit(other_side)) {
+    state_->GetRouter(other_side)->Flush();
+    return true;
+  }
+  return false;
+}
+
+bool LocalRouterLink::CanNodeRequestBypass(
+    const NodeName& bypass_request_source) {
+  // Balanced by a release in TryLockForBypass().
+  std::atomic_thread_fence(std::memory_order_acquire);
+  return state_->link_state().is_locked_by(side_.opposite()) &&
+         state_->link_state().allowed_bypass_request_source ==
+             bypass_request_source;
+}
+
 void LocalRouterLink::Deactivate() {
   state_->Deactivate(side_);
 }
diff --git a/src/ipcz/local_router_link.h b/src/ipcz/local_router_link.h
index 2137609..bfb58d4 100644
--- a/src/ipcz/local_router_link.h
+++ b/src/ipcz/local_router_link.h
@@ -12,6 +12,8 @@
 
 namespace ipcz {
 
+struct RouterLinkState;
+
 // Local link between two Routers on the same node. This class is thread-safe.
 //
 // NOTE: This implementation must take caution when calling into any Router. See
@@ -21,14 +23,22 @@
   // Creates a new pair of LocalRouterLinks linking the given pair of Routers
   // together. The Routers must not currently have outward links. `type` must
   // be either kCentral or kBridge, as local links may never be peripheral.
-  static void ConnectRouters(LinkType type, const Router::Pair& routers);
+  static RouterLink::Pair ConnectRouters(LinkType type,
+                                         const Router::Pair& routers);
 
   // RouterLink:
   LinkType GetType() const override;
+  RouterLinkState* GetLinkState() const override;
   bool HasLocalPeer(const Router& router) override;
   bool IsRemoteLinkTo(const NodeLink& node_link, SublinkId sublink) override;
   void AcceptParcel(Parcel& parcel) override;
   void AcceptRouteClosure(SequenceNumber sequence_length) override;
+  void MarkSideStable() override;
+  bool TryLockForBypass(const NodeName& bypass_request_source) override;
+  bool TryLockForClosure() override;
+  void Unlock() override;
+  bool FlushOtherSideIfWaiting() override;
+  bool CanNodeRequestBypass(const NodeName& bypass_request_source) override;
   void Deactivate() override;
   std::string Describe() const override;
 
diff --git a/src/ipcz/node_connector.cc b/src/ipcz/node_connector.cc
index 1013577..7186359 100644
--- a/src/ipcz/node_connector.cc
+++ b/src/ipcz/node_connector.cc
@@ -263,7 +263,8 @@
   for (size_t i = 0; i < num_valid_portals; ++i) {
     const Ref<Router> router = waiting_portals_[i]->router();
     router->SetOutwardLink(to_link->AddRemoteRouterLink(
-        SublinkId(i), LinkType::kCentral, link_side, router));
+        SublinkId(i), to_link->memory().GetInitialRouterLinkState(i),
+        LinkType::kCentral, link_side, router));
   }
 
   // Elicit immediate peer closure on any surplus portals that were established
diff --git a/src/ipcz/node_link.cc b/src/ipcz/node_link.cc
index 6fb7f58..2edd013 100644
--- a/src/ipcz/node_link.cc
+++ b/src/ipcz/node_link.cc
@@ -65,12 +65,14 @@
   ABSL_HARDENING_ASSERT(!active_);
 }
 
-Ref<RemoteRouterLink> NodeLink::AddRemoteRouterLink(SublinkId sublink,
-                                                    LinkType type,
-                                                    LinkSide side,
-                                                    Ref<Router> router) {
-  auto link =
-      RemoteRouterLink::Create(WrapRefCounted(this), sublink, type, side);
+Ref<RemoteRouterLink> NodeLink::AddRemoteRouterLink(
+    SublinkId sublink,
+    FragmentRef<RouterLinkState> link_state,
+    LinkType type,
+    LinkSide side,
+    Ref<Router> router) {
+  auto link = RemoteRouterLink::Create(WrapRefCounted(this), sublink,
+                                       std::move(link_state), type, side);
 
   absl::MutexLock lock(&mutex_);
   if (!active_) {
@@ -243,6 +245,13 @@
       sublink->router_link->GetType(), route_closed.params().sequence_length);
 }
 
+bool NodeLink::OnFlushRouter(msg::FlushRouter& flush) {
+  if (Ref<Router> router = GetRouter(flush.params().sublink)) {
+    router->Flush();
+  }
+  return true;
+}
+
 void NodeLink::OnTransportError() {
   SublinkMap sublinks;
   {
diff --git a/src/ipcz/node_link.h b/src/ipcz/node_link.h
index 47c20ee..2922add 100644
--- a/src/ipcz/node_link.h
+++ b/src/ipcz/node_link.h
@@ -77,10 +77,16 @@
   // specifies which side of the link this end identifies as (A or B), and
   // `type` specifies the type of link this is, from the perspective of
   // `router`.
-  Ref<RemoteRouterLink> AddRemoteRouterLink(SublinkId sublink,
-                                            LinkType type,
-                                            LinkSide side,
-                                            Ref<Router> router);
+  //
+  // If `link_state_fragment` is non-null, the given fragment contains the
+  // shared RouterLinkState structure for the new link. Only central links
+  // require a RouterLinkState.
+  Ref<RemoteRouterLink> AddRemoteRouterLink(
+      SublinkId sublink,
+      FragmentRef<RouterLinkState> link_state,
+      LinkType type,
+      LinkSide side,
+      Ref<Router> router);
 
   // Removes the route specified by `sublink`. Once removed, any messages
   // received for that sublink are ignored.
@@ -121,6 +127,7 @@
   // NodeMessageListener overrides:
   bool OnAcceptParcel(msg::AcceptParcel& accept) override;
   bool OnRouteClosed(msg::RouteClosed& route_closed) override;
+  bool OnFlushRouter(msg::FlushRouter& flush) override;
   void OnTransportError() override;
 
   const Ref<Node> node_;
diff --git a/src/ipcz/node_link_memory.cc b/src/ipcz/node_link_memory.cc
index 012f26c..1711739 100644
--- a/src/ipcz/node_link_memory.cc
+++ b/src/ipcz/node_link_memory.cc
@@ -11,9 +11,11 @@
 
 #include "ipcz/buffer_id.h"
 #include "ipcz/driver_memory.h"
+#include "ipcz/fragment_descriptor.h"
 #include "ipcz/ipcz.h"
 #include "ipcz/node.h"
 #include "ipcz/node_link.h"
+#include "third_party/abseil-cpp/absl/base/macros.h"
 #include "util/ref_counted.h"
 
 namespace ipcz {
@@ -29,6 +31,15 @@
 // uses which require synchronous availability throughout a link's lifetime.
 constexpr size_t kPrimaryBufferReservedHeaderSize = 256;
 
+// The number of fixed RouterLinkState locations in the primary buffer. This
+// limits the maximum number of initial portals supported by the ConnectNode()
+// API. Note that these states reside in a fixed location at the end of the
+// reserved block.
+using InitialRouterLinkStateArray =
+    std::array<RouterLinkState, NodeLinkMemory::kMaxInitialPortals>;
+static_assert(sizeof(InitialRouterLinkStateArray) == 768,
+              "Invalid InitialRouterLinkStateArray size");
+
 struct IPCZ_ALIGN(8) PrimaryBufferHeader {
   // Atomic generator for new unique BufferIds to use across the associated
   // NodeLink. This allows each side of a NodeLink to generate new BufferIds
@@ -46,6 +57,12 @@
 constexpr size_t kPrimaryBufferHeaderPaddingSize =
     kPrimaryBufferReservedHeaderSize - sizeof(PrimaryBufferHeader);
 
+// Computes the byte offset of one address from another.
+uint32_t ToOffset(void* ptr, void* base) {
+  return static_cast<uint32_t>(static_cast<uint8_t*>(ptr) -
+                               static_cast<uint8_t*>(base));
+}
+
 }  // namespace
 
 // This structure always sits at offset 0 in the primary buffer and has a fixed
@@ -56,6 +73,10 @@
   PrimaryBufferHeader header;
   uint8_t reserved_header_padding[kPrimaryBufferHeaderPaddingSize];
 
+  // Reserved RouterLinkState instances for use only by the NodeLink's initial
+  // portals.
+  InitialRouterLinkStateArray initial_link_states;
+
   // Reserved memory for a series of fixed block allocators. Additional
   // allocators may be adopted by a NodeLinkMemory over its lifetime, but these
   // ones remain fixed within the primary buffer.
@@ -163,4 +184,17 @@
       count, std::memory_order_relaxed)};
 }
 
+FragmentRef<RouterLinkState> NodeLinkMemory::GetInitialRouterLinkState(
+    size_t i) {
+  auto& states = primary_buffer_.initial_link_states;
+  ABSL_ASSERT(i < states.size());
+  RouterLinkState* state = &states[i];
+
+  FragmentDescriptor descriptor(kPrimaryBufferId,
+                                ToOffset(state, primary_buffer_memory_.data()),
+                                sizeof(RouterLinkState));
+  return FragmentRef<RouterLinkState>(RefCountedFragment::kUnmanagedRef,
+                                      Fragment(descriptor, state));
+}
+
 }  // namespace ipcz
diff --git a/src/ipcz/node_link_memory.h b/src/ipcz/node_link_memory.h
index 014a78e..b335ec9 100644
--- a/src/ipcz/node_link_memory.h
+++ b/src/ipcz/node_link_memory.h
@@ -5,13 +5,16 @@
 #ifndef IPCZ_SRC_IPCZ_NODE_LINK_MEMORY_H_
 #define IPCZ_SRC_IPCZ_NODE_LINK_MEMORY_H_
 
+#include <cstddef>
 #include <cstdint>
 
 #include "ipcz/buffer_id.h"
 #include "ipcz/buffer_pool.h"
 #include "ipcz/driver_memory.h"
 #include "ipcz/driver_memory_mapping.h"
+#include "ipcz/fragment_ref.h"
 #include "ipcz/ipcz.h"
+#include "ipcz/router_link_state.h"
 #include "ipcz/sublink_id.h"
 #include "third_party/abseil-cpp/absl/types/span.h"
 #include "util/ref_counted.h"
@@ -75,6 +78,13 @@
   // use on the corresponding NodeLink.
   SublinkId AllocateSublinkIds(size_t count);
 
+  // Returns a ref to the RouterLinkState for the `i`th initial portal on the
+  // NodeLink, established by the Connect() call which created this link. Unlike
+  // other RouterLinkStates which are allocated dynamically, these have a fixed
+  // location within the NodeLinkMemory's primary buffer. The returned
+  // FragmentRef is unmanaged and will never free its underlying fragment.
+  FragmentRef<RouterLinkState> GetInitialRouterLinkState(size_t i);
+
  private:
   struct PrimaryBuffer;
 
diff --git a/src/ipcz/node_link_test.cc b/src/ipcz/node_link_test.cc
index 8f26dd5..0eff379 100644
--- a/src/ipcz/node_link_test.cc
+++ b/src/ipcz/node_link_test.cc
@@ -66,9 +66,11 @@
   auto router0 = MakeRefCounted<Router>();
   auto router1 = MakeRefCounted<Router>();
   router0->SetOutwardLink(link0->AddRemoteRouterLink(
-      SublinkId(0), LinkType::kCentral, LinkSide::kA, router0));
+      SublinkId(0), link0->memory().GetInitialRouterLinkState(0),
+      LinkType::kCentral, LinkSide::kA, router0));
   router1->SetOutwardLink(link1->AddRemoteRouterLink(
-      SublinkId(0), LinkType::kCentral, LinkSide::kB, router1));
+      SublinkId(0), link0->memory().GetInitialRouterLinkState(0),
+      LinkType::kCentral, LinkSide::kB, router1));
 
   EXPECT_FALSE(router1->IsPeerClosed());
   router0->CloseRoute();
diff --git a/src/ipcz/node_messages_generator.h b/src/ipcz/node_messages_generator.h
index 17dc8f1..d8f3c9f 100644
--- a/src/ipcz/node_messages_generator.h
+++ b/src/ipcz/node_messages_generator.h
@@ -102,4 +102,11 @@
   IPCZ_MSG_PARAM(SequenceNumber, sequence_length)
 IPCZ_MSG_END()
 
+// Hints to the target router that it should flush its state. Generally sent to
+// catalyze route reduction or elicit some other state change which was blocked
+// on some other work being done first by the sender of this message.
+IPCZ_MSG_BEGIN(FlushRouter, IPCZ_MSG_ID(36), IPCZ_MSG_VERSION(0))
+  IPCZ_MSG_PARAM(SublinkId, sublink)
+IPCZ_MSG_END()
+
 IPCZ_MSG_END_INTERFACE()
diff --git a/src/ipcz/node_name.cc b/src/ipcz/node_name.cc
index 1f4861b..6dd7ac1 100644
--- a/src/ipcz/node_name.cc
+++ b/src/ipcz/node_name.cc
@@ -16,8 +16,6 @@
 
 static_assert(std::is_standard_layout<NodeName>::value, "Invalid NodeName");
 
-NodeName::~NodeName() = default;
-
 std::string NodeName::ToString() const {
   std::string name(33, 0);
   int length = snprintf(name.data(), name.size(), "%016" PRIx64 "%016" PRIx64,
diff --git a/src/ipcz/node_name.h b/src/ipcz/node_name.h
index be83f79..11d672a 100644
--- a/src/ipcz/node_name.h
+++ b/src/ipcz/node_name.h
@@ -28,7 +28,6 @@
  public:
   constexpr NodeName() = default;
   constexpr NodeName(uint64_t high, uint64_t low) : high_(high), low_(low) {}
-  ~NodeName();
 
   bool is_valid() const { return high_ != 0 || low_ != 0; }
 
diff --git a/src/ipcz/ref_counted_fragment.cc b/src/ipcz/ref_counted_fragment.cc
index 14b21cf..cd4a2af 100644
--- a/src/ipcz/ref_counted_fragment.cc
+++ b/src/ipcz/ref_counted_fragment.cc
@@ -10,8 +10,6 @@
 
 RefCountedFragment::RefCountedFragment() = default;
 
-RefCountedFragment::~RefCountedFragment() = default;
-
 void RefCountedFragment::AddRef() {
   ref_count_.fetch_add(1, std::memory_order_relaxed);
 }
diff --git a/src/ipcz/ref_counted_fragment.h b/src/ipcz/ref_counted_fragment.h
index ff69c45..09c01f3 100644
--- a/src/ipcz/ref_counted_fragment.h
+++ b/src/ipcz/ref_counted_fragment.h
@@ -21,7 +21,6 @@
   enum { kUnmanagedRef };
 
   RefCountedFragment();
-  ~RefCountedFragment();
 
   int32_t ref_count_for_testing() const { return ref_count_; }
 
diff --git a/src/ipcz/remote_router_link.cc b/src/ipcz/remote_router_link.cc
index 7e9838a..ec47513 100644
--- a/src/ipcz/remote_router_link.cc
+++ b/src/ipcz/remote_router_link.cc
@@ -18,28 +18,38 @@
 
 RemoteRouterLink::RemoteRouterLink(Ref<NodeLink> node_link,
                                    SublinkId sublink,
+                                   FragmentRef<RouterLinkState> link_state,
                                    LinkType type,
                                    LinkSide side)
     : node_link_(std::move(node_link)),
       sublink_(sublink),
       type_(type),
-      side_(side) {}
+      side_(side),
+      link_state_(std::move(link_state)) {
+  ABSL_ASSERT(link_state_.is_null() || link_state_.is_addressable());
+}
 
 RemoteRouterLink::~RemoteRouterLink() = default;
 
 // static
-Ref<RemoteRouterLink> RemoteRouterLink::Create(Ref<NodeLink> node_link,
-                                               SublinkId sublink,
-                                               LinkType type,
-                                               LinkSide side) {
-  return AdoptRef(
-      new RemoteRouterLink(std::move(node_link), sublink, type, side));
+Ref<RemoteRouterLink> RemoteRouterLink::Create(
+    Ref<NodeLink> node_link,
+    SublinkId sublink,
+    FragmentRef<RouterLinkState> link_state,
+    LinkType type,
+    LinkSide side) {
+  return AdoptRef(new RemoteRouterLink(std::move(node_link), sublink,
+                                       std::move(link_state), type, side));
 }
 
 LinkType RemoteRouterLink::GetType() const {
   return type_;
 }
 
+RouterLinkState* RemoteRouterLink::GetLinkState() const {
+  return link_state_.get();
+}
+
 bool RemoteRouterLink::HasLocalPeer(const Router& router) {
   return false;
 }
@@ -157,6 +167,59 @@
   node_link()->Transmit(route_closed);
 }
 
+void RemoteRouterLink::MarkSideStable() {
+  side_is_stable_.store(true, std::memory_order_release);
+  if (RouterLinkState* state = GetLinkState()) {
+    state->SetSideStable(side_);
+  }
+}
+
+bool RemoteRouterLink::TryLockForBypass(const NodeName& bypass_request_source) {
+  RouterLinkState* state = GetLinkState();
+  if (!state || !state->TryLock(side_)) {
+    return false;
+  }
+
+  state->allowed_bypass_request_source = bypass_request_source;
+
+  // Balanced by an acquire in CanNodeRequestBypass().
+  std::atomic_thread_fence(std::memory_order_release);
+  return true;
+}
+
+bool RemoteRouterLink::TryLockForClosure() {
+  RouterLinkState* state = GetLinkState();
+  return state && state->TryLock(side_);
+}
+
+void RemoteRouterLink::Unlock() {
+  if (RouterLinkState* state = GetLinkState()) {
+    state->Unlock(side_);
+  }
+}
+
+bool RemoteRouterLink::FlushOtherSideIfWaiting() {
+  RouterLinkState* state = GetLinkState();
+  if (!state || !state->ResetWaitingBit(side_.opposite())) {
+    return false;
+  }
+
+  msg::FlushRouter flush;
+  flush.params().sublink = sublink_;
+  node_link()->Transmit(flush);
+  return true;
+}
+
+bool RemoteRouterLink::CanNodeRequestBypass(
+    const NodeName& bypass_request_source) {
+  RouterLinkState* state = GetLinkState();
+
+  // Balanced by a release in TryLockForBypass().
+  std::atomic_thread_fence(std::memory_order_acquire);
+  return state && state->is_locked_by(side_.opposite()) &&
+         state->allowed_bypass_request_source == bypass_request_source;
+}
+
 void RemoteRouterLink::Deactivate() {
   node_link()->RemoveRemoteRouterLink(sublink_);
 }
diff --git a/src/ipcz/remote_router_link.h b/src/ipcz/remote_router_link.h
index 4407db7..9967fa5 100644
--- a/src/ipcz/remote_router_link.h
+++ b/src/ipcz/remote_router_link.h
@@ -7,9 +7,11 @@
 
 #include <atomic>
 
+#include "ipcz/fragment_ref.h"
 #include "ipcz/link_side.h"
 #include "ipcz/link_type.h"
 #include "ipcz/router_link.h"
+#include "ipcz/router_link_state.h"
 #include "ipcz/sublink_id.h"
 #include "util/ref_counted.h"
 
@@ -35,9 +37,11 @@
   // using `sublink` specifically. `side` is the side of this link on which
   // this RemoteRouterLink falls (side A or B), and `type` indicates what type
   // of link it is -- which for remote links must be either kCentral,
-  // kPeripheralInward, or kPeripheralOutward.
+  // kPeripheralInward, or kPeripheralOutward. If the link is kCentral, a
+  // non-null `link_state` may be provided to use as the link's RouterLinkState.
   static Ref<RemoteRouterLink> Create(Ref<NodeLink> node_link,
                                       SublinkId sublink,
+                                      FragmentRef<RouterLinkState> link_state,
                                       LinkType type,
                                       LinkSide side);
 
@@ -46,16 +50,24 @@
 
   // RouterLink:
   LinkType GetType() const override;
+  RouterLinkState* GetLinkState() const override;
   bool HasLocalPeer(const Router& router) override;
   bool IsRemoteLinkTo(const NodeLink& node_link, SublinkId sublink) override;
   void AcceptParcel(Parcel& parcel) override;
   void AcceptRouteClosure(SequenceNumber sequence_length) override;
+  void MarkSideStable() override;
+  bool TryLockForBypass(const NodeName& bypass_request_source) override;
+  bool TryLockForClosure() override;
+  void Unlock() override;
+  bool FlushOtherSideIfWaiting() override;
+  bool CanNodeRequestBypass(const NodeName& bypass_request_source) override;
   void Deactivate() override;
   std::string Describe() const override;
 
  private:
   RemoteRouterLink(Ref<NodeLink> node_link,
                    SublinkId sublink,
+                   FragmentRef<RouterLinkState> link_state,
                    LinkType type,
                    LinkSide side);
 
@@ -65,6 +77,17 @@
   const SublinkId sublink_;
   const LinkType type_;
   const LinkSide side_;
+
+  // Local atomic cache of whether this side of the link is marked stable. If
+  // MarkSideStable() is called when no RouterLinkState is present, this will be
+  // used to remember it once a RouterLinkState is finally established.
+  std::atomic<bool> side_is_stable_{false};
+
+  // A reference to the shared memory Fragment containing the RouterLinkState
+  // shared by both ends of this RouterLink. Always null for non-central links,
+  // and may be null for a central links if its RouterLinkState has not yet been
+  // allocated or shared.
+  FragmentRef<RouterLinkState> link_state_;
 };
 
 }  // namespace ipcz
diff --git a/src/ipcz/router.cc b/src/ipcz/router.cc
index d9e9acc..279ffc5 100644
--- a/src/ipcz/router.cc
+++ b/src/ipcz/router.cc
@@ -267,8 +267,8 @@
     }
 
     Ref<RemoteRouterLink> new_link = from_node_link.AddRemoteRouterLink(
-        descriptor.new_sublink, LinkType::kPeripheralOutward, LinkSide::kB,
-        router);
+        descriptor.new_sublink, nullptr, LinkType::kPeripheralOutward,
+        LinkSide::kB, router);
     if (new_link) {
       router->outward_link_ = std::move(new_link);
 
@@ -326,8 +326,9 @@
   // RemoteRouterLink, because it's not yet safe for us to send messages to the
   // remote node regarding `new_sublink`. `descriptor` must be transmitted
   // first.
-  to_node_link.AddRemoteRouterLink(new_sublink, LinkType::kPeripheralInward,
-                                   LinkSide::kA, WrapRefCounted(this));
+  to_node_link.AddRemoteRouterLink(new_sublink, nullptr,
+                                   LinkType::kPeripheralInward, LinkSide::kA,
+                                   WrapRefCounted(this));
 }
 
 void Router::BeginProxyingToNewRouter(NodeLink& to_node_link,
diff --git a/src/ipcz/router.h b/src/ipcz/router.h
index 01d21ab..d132947 100644
--- a/src/ipcz/router.h
+++ b/src/ipcz/router.h
@@ -136,9 +136,6 @@
   // and SublinkId.
   void NotifyLinkDisconnected(const NodeLink& node_link, SublinkId sublink);
 
- private:
-  ~Router() override;
-
   // Flushes any inbound or outbound parcels, as well as any route closure
   // notifications. RouterLinks which are no longer needed for the operation of
   // this Router may be deactivated by this call.
@@ -152,6 +149,9 @@
   // into Router using a reference held on the calling stack.
   void Flush();
 
+ private:
+  ~Router() override;
+
   absl::Mutex mutex_;
 
   // The current computed portal status to be reflected by a portal controlling
diff --git a/src/ipcz/router_link.h b/src/ipcz/router_link.h
index 332b3ca..a682e18 100644
--- a/src/ipcz/router_link.h
+++ b/src/ipcz/router_link.h
@@ -6,6 +6,7 @@
 #define IPCZ_SRC_IPCZ_ROUTER_LINK_H_
 
 #include "ipcz/link_type.h"
+#include "ipcz/node_name.h"
 #include "ipcz/sequence_number.h"
 #include "ipcz/sublink_id.h"
 #include "util/ref_counted.h"
@@ -15,6 +16,7 @@
 class NodeLink;
 class Parcel;
 class Router;
+struct RouterLinkState;
 
 // A RouterLink represents one endpoint of a link between two Routers. All
 // subclasses must be thread-safe.
@@ -31,6 +33,10 @@
   // Indicates what type of link this is. See LinkType documentation.
   virtual LinkType GetType() const = 0;
 
+  // Returns a pointer to the link's RouterLinkState, if it has one. Otherwise
+  // returns null.
+  virtual RouterLinkState* GetLinkState() const = 0;
+
   // Returns true iff this is a LocalRouterLink whose peer router is `router`.
   virtual bool HasLocalPeer(const Router& router) = 0;
 
@@ -47,6 +53,47 @@
   // transmitted from the closed side before it was closed.
   virtual void AcceptRouteClosure(SequenceNumber sequence_length) = 0;
 
+  // Signals that this side of the link is in a stable state suitable for one
+  // side or the other to lock the link, either for bypass or closure
+  // propagation. Only once both sides are marked stable can either side lock
+  // the link with TryLock* methods below.
+  virtual void MarkSideStable() = 0;
+
+  // Attempts to lock the link for the router on this side to coordinate its own
+  // bypass. Returns true if and only if successful, meaning the link is locked
+  // and it's safe for the router who locked it to coordinate its own bypass by
+  // providing its inward and outward peers with a new central link over which
+  // they may communicate directly.
+  //
+  // On success, `bypass_request_source` is also stashed in this link's shared
+  // state so that the other side of the link can authenticate a bypass request
+  // coming from that node. This parameter may be omitted if the bypass does not
+  // not require authentication, e.g. because the requesting inward peer's node
+  // is the same as the proxy's own node, or that of the proxy's current outward
+  // peer.
+  [[nodiscard]] virtual bool TryLockForBypass(
+      const NodeName& bypass_request_source = {}) = 0;
+
+  // Attempts to lock the link for the router on this side to propagate route
+  // closure toward the other side. Returns true if and only if successful,
+  // meaning no further bypass operations will proceed on the link.
+  [[nodiscard]] virtual bool TryLockForClosure() = 0;
+
+  // Unlocks a link previously locked by one of the TryLock* methods above.
+  virtual void Unlock() = 0;
+
+  // Asks the other side to flush its router if and only if the side marked
+  // itself as waiting for both sides of the link to become stable, and both
+  // sides of the link are stable. Returns true if and only if a flush was
+  // actually issued to the other side.
+  virtual bool FlushOtherSideIfWaiting() = 0;
+
+  // Indicates whether this link can be bypassed by a request from the named
+  // node to one side of the link. True if and only if the proxy on the other
+  // side of this link has already initiated bypass and `bypass_request_source`
+  // matches the NodeName it stored in this link's shared state at that time.
+  virtual bool CanNodeRequestBypass(const NodeName& bypass_request_source) = 0;
+
   // Deactivates this RouterLink to sever any binding it may have to a specific
   // Router. Note that deactivation is not necessarily synchronous, so some
   // in-progress calls into a Router may still complete on behalf of this
diff --git a/src/ipcz/router_link_state.cc b/src/ipcz/router_link_state.cc
new file mode 100644
index 0000000..774104b
--- /dev/null
+++ b/src/ipcz/router_link_state.cc
@@ -0,0 +1,107 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ipcz/router_link_state.h"
+
+#include <cstring>
+
+#include "ipcz/link_side.h"
+
+namespace ipcz {
+
+RouterLinkState::RouterLinkState() = default;
+
+// static
+RouterLinkState& RouterLinkState::Initialize(void* where) {
+  auto& state = *static_cast<RouterLinkState*>(where);
+  new (&state) RouterLinkState();
+  memset(state.reserved, 0, sizeof(state.reserved));
+  std::atomic_thread_fence(std::memory_order_release);
+  return state;
+}
+
+void RouterLinkState::SetSideStable(LinkSide side) {
+  const Status kThisSideStable =
+      side == LinkSide::kA ? kSideAStable : kSideBStable;
+
+  Status expected = kUnstable;
+  while (!status.compare_exchange_weak(expected, expected | kThisSideStable,
+                                       std::memory_order_relaxed) &&
+         (expected & kThisSideStable) == 0) {
+  }
+}
+
+bool RouterLinkState::TryLock(LinkSide from_side) {
+  const Status kThisSideStable =
+      from_side == LinkSide::kA ? kSideAStable : kSideBStable;
+  const Status kOtherSideStable =
+      from_side == LinkSide::kA ? kSideBStable : kSideAStable;
+  const Status kLockedByThisSide =
+      from_side == LinkSide::kA ? kLockedBySideA : kLockedBySideB;
+  const Status kLockedByEitherSide = kLockedBySideA | kLockedBySideB;
+  const Status kThisSideWaiting =
+      from_side == LinkSide::kA ? kSideAWaiting : kSideBWaiting;
+
+  Status expected = kStable;
+  Status desired_bit = kLockedByThisSide;
+  while (!status.compare_exchange_weak(expected, expected | desired_bit,
+                                       std::memory_order_relaxed)) {
+    if ((expected & kLockedByEitherSide) != 0 ||
+        (expected & kThisSideStable) == 0) {
+      return false;
+    }
+
+    if (desired_bit == kLockedByThisSide &&
+        (expected & kOtherSideStable) == 0) {
+      // If we were trying to lock but the other side isn't stable, try to set
+      // our waiting bit instead.
+      desired_bit = kThisSideWaiting;
+    } else if (desired_bit == kThisSideWaiting &&
+               (expected & kStable) == kStable) {
+      // Otherwise if we were trying to set our waiting bit and the other side
+      // is now stable, go back to trying to lock the link.
+      desired_bit = kLockedByThisSide;
+    }
+  }
+
+  return desired_bit == kLockedByThisSide;
+}
+
+void RouterLinkState::Unlock(LinkSide from_side) {
+  const Status kLockedByThisSide =
+      from_side == LinkSide::kA ? kLockedBySideA : kLockedBySideB;
+  Status expected = kStable | kLockedByThisSide;
+  Status desired = kStable;
+  while (!status.compare_exchange_weak(expected, desired,
+                                       std::memory_order_relaxed) &&
+         (expected & kLockedByThisSide) != 0) {
+    desired = expected & ~kLockedByThisSide;
+  }
+}
+
+bool RouterLinkState::ResetWaitingBit(LinkSide side) {
+  const Status kThisSideWaiting =
+      side == LinkSide::kA ? kSideAWaiting : kSideBWaiting;
+  const Status kLockedByEitherSide = kLockedBySideA | kLockedBySideB;
+  Status expected = kStable | kThisSideWaiting;
+  Status desired = kStable;
+  while (!status.compare_exchange_weak(expected, desired,
+                                       std::memory_order_relaxed)) {
+    if ((expected & kStable) != kStable || (expected & kThisSideWaiting) == 0 ||
+        (expected & kLockedByEitherSide) != 0) {
+      // If the link isn't stable yet, or `side` wasn't waiting on it, or the
+      // link is already locked, there's no point changing the status here.
+      return false;
+    }
+
+    // At this point we know the link is stable, the identified side is waiting,
+    // and the link is not locked. Regardless of what other bits are set, mask
+    // off the waiting bit and try to update our status again.
+    desired = expected & ~kThisSideWaiting;
+  }
+
+  return true;
+}
+
+}  // namespace ipcz
diff --git a/src/ipcz/router_link_state.h b/src/ipcz/router_link_state.h
new file mode 100644
index 0000000..1ccefda
--- /dev/null
+++ b/src/ipcz/router_link_state.h
@@ -0,0 +1,125 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IPCZ_SRC_IPCZ_ROUTER_LINK_STATE_H_
+#define IPCZ_SRC_IPCZ_ROUTER_LINK_STATE_H_
+
+#include <atomic>
+#include <cstdint>
+#include <type_traits>
+
+#include "ipcz/ipcz.h"
+#include "ipcz/link_side.h"
+#include "ipcz/node_name.h"
+#include "ipcz/ref_counted_fragment.h"
+
+namespace ipcz {
+
+// Structure shared between both Routers connected by RouterLink. This is used
+// to synchronously query and reflect the state of each Router to the other,
+// and ultimately to facilitate orderly state changes across the route.
+//
+// This may live in shared memory, where it should be managed as a
+// RefCountedFragment.
+struct IPCZ_ALIGN(8) RouterLinkState : public RefCountedFragment {
+  RouterLinkState();
+
+  // In-place initialization of a new RouterLinkState at `where`.
+  static RouterLinkState& Initialize(void* where);
+
+  // Link status which both sides atomically update to coordinate orderly proxy
+  // bypass and route closure propagation. Used only for central links.
+  using Status = uint32_t;
+
+  // Status constants follow.
+
+  // This is a fresh central link established to bypass a proxy. The Routers on
+  // either side both still have decaying links and therefore cannot yet support
+  // another bypass operation.
+  static constexpr Status kUnstable = 0;
+
+  // Set if side A or B of this link is stable, respectively, meaning it has no
+  // decaying router links. If both bits are set, the link itself is considered
+  // to be stable.
+  static constexpr Status kSideAStable = 1 << 0;
+  static constexpr Status kSideBStable = 1 << 1;
+  static constexpr Status kStable = kSideAStable | kSideBStable;
+
+  // When either side attempts to lock this link and fails because ther other
+  // side is still unstable, they set their corresponding "waiting" bit instead.
+  // Once the other side is stable, this bit informs the other side that they
+  // should send a flush notification back to this side to unblock whatever
+  // operation was waiting for a stable link.
+  static constexpr Status kSideAWaiting = 1 << 2;
+  static constexpr Status kSideBWaiting = 1 << 3;
+
+  // Set if this link has been locked by side A or B, respectively. These bits
+  // are always mutually exclusive and may only be set once kStable are set. A
+  // A link may be locked to initiate bypass of one side, or to propagate route
+  // closure from one side.
+  static constexpr Status kLockedBySideA = 1 << 4;
+  static constexpr Status kLockedBySideB = 1 << 5;
+
+  std::atomic<Status> status{kUnstable};
+
+  // In a situation with three routers A-B-C and a central link between A and
+  // B, B will eventually ask C to connect directly to A and bypass B along the
+  // route. In order to facilitate this, B will also first stash C's name in
+  // this field on the central link between A and B. This is sufficient for A to
+  // validate that C is an appropriate source of such a bypass request.
+  NodeName allowed_bypass_request_source;
+
+  // Reserved slots.
+  uint32_t reserved[10];
+
+  bool is_locked_by(LinkSide side) const {
+    Status s = status.load(std::memory_order_relaxed);
+    if (side == LinkSide::kA) {
+      return (s & kLockedBySideA) != 0;
+    }
+    return (s & kLockedBySideB) != 0;
+  }
+
+  // Updates the status to reflect that the given `side` is stable, meaning that
+  // it's no longer holding onto any decaying links.
+  void SetSideStable(LinkSide side);
+
+  // Attempts to lock the state of this link from one side, so that the Router
+  // on that side can coordinate its own bypass or propagate its own side's
+  // closure. In order for this to succeed, both kStable bits must be set and
+  // the link must not already be locked. Returns true iff locked successfully.
+  //
+  // If the opposite side is still unstable, this sets the waiting bit for
+  // `from_side` and returns false.
+  //
+  // In any other situation, the status is unmodified and this returns false.
+  [[nodiscard]] bool TryLock(LinkSide from_side);
+
+  // Unlocks a link previously locked by TryLock().
+  void Unlock(LinkSide from_side);
+
+  // If both sides of the link are stable AND `side` was marked as waiting
+  // before that happened, this resets the waiting bit and returns true.
+  // Otherwise the link's status is unchanged and this returns false.
+  //
+  // Note that the waiting bit for `side` will have only been set if a prior
+  // attempt was made to TryLock() from that side, while the other side was
+  // still unstable.
+  bool ResetWaitingBit(LinkSide side);
+};
+
+// The size of this structure is fixed at 64 bytes to ensure that it fits the
+// smallest block allocation size supported by NodeLinkMemory.
+static_assert(sizeof(RouterLinkState) == 64,
+              "RouterLinkState size must be 64 bytes");
+
+// RouterLinkState instances may live in shared memory. Trivial copyability is
+// asserted here as a sort of proxy condition to catch changes which might break
+// that usage (e.g. introduction of a non-trivial destructor).
+static_assert(std::is_trivially_copyable<RouterLinkState>::value,
+              "RouterLinkState must be trivially copyable");
+
+}  // namespace ipcz
+
+#endif  // IPCZ_SRC_IPCZ_ROUTER_LINK_STATE_H_
diff --git a/src/ipcz/router_link_test.cc b/src/ipcz/router_link_test.cc
new file mode 100644
index 0000000..42ab8a8
--- /dev/null
+++ b/src/ipcz/router_link_test.cc
@@ -0,0 +1,196 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ipcz/router_link.h"
+
+#include <tuple>
+
+#include "ipcz/driver_transport.h"
+#include "ipcz/ipcz.h"
+#include "ipcz/link_side.h"
+#include "ipcz/link_type.h"
+#include "ipcz/local_router_link.h"
+#include "ipcz/node.h"
+#include "ipcz/node_link.h"
+#include "ipcz/node_name.h"
+#include "ipcz/remote_router_link.h"
+#include "ipcz/router.h"
+#include "ipcz/router_link_state.h"
+#include "reference_drivers/sync_reference_driver.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "util/ref_counted.h"
+
+namespace ipcz {
+namespace {
+
+enum class RouterLinkTestMode {
+  kLocal,
+  kRemote,
+};
+
+const IpczDriver& kTestDriver = reference_drivers::kSyncReferenceDriver;
+
+constexpr NodeName kTestBrokerName(1, 2);
+constexpr NodeName kTestNonBrokerName(2, 3);
+constexpr NodeName kTestPeer1Name(3, 4);
+constexpr NodeName kTestPeer2Name(4, 5);
+
+class RouterLinkTest : public testing::Test,
+                       public testing::WithParamInterface<RouterLinkTestMode> {
+ public:
+  void SetUp() override {
+    switch (GetParam()) {
+      case RouterLinkTestMode::kLocal:
+        std::tie(a_link_, b_link_) =
+            LocalRouterLink::ConnectRouters(LinkType::kCentral, {a_, b_});
+        break;
+
+      case RouterLinkTestMode::kRemote: {
+        auto transports = DriverTransport::CreatePair(kTestDriver);
+        auto alloc = NodeLinkMemory::Allocate(broker_);
+        broker_node_link_ = NodeLink::Create(
+            broker_, LinkSide::kA, kTestBrokerName, kTestNonBrokerName,
+            Node::Type::kNormal, 0, transports.first,
+            std::move(alloc.node_link_memory));
+        non_broker_node_link_ = NodeLink::Create(
+            non_broker_, LinkSide::kB, kTestNonBrokerName, kTestBrokerName,
+            Node::Type::kBroker, 0, transports.second,
+            NodeLinkMemory::Adopt(non_broker_,
+                                  std::move(alloc.primary_buffer_memory)));
+        broker_->AddLink(kTestNonBrokerName, broker_node_link_);
+        non_broker_->AddLink(kTestBrokerName, non_broker_node_link_);
+
+        auto fragment =
+            broker_node_link_->memory().buffer_pool().AllocateFragment(
+                sizeof(RouterLinkState));
+        auto link_state = FragmentRef<RouterLinkState>(
+            RefCountedFragment::kAdoptExistingRef,
+            WrapRefCounted(&broker_node_link_->memory()), fragment);
+        RouterLinkState::Initialize(link_state.get());
+        a_link_ = broker_node_link_->AddRemoteRouterLink(
+            SublinkId{0}, link_state, LinkType::kCentral, LinkSide::kA, a_);
+        b_link_ = non_broker_node_link_->AddRemoteRouterLink(
+            SublinkId{0}, link_state, LinkType::kCentral, LinkSide::kB, b_);
+        a_->SetOutwardLink(a_link_);
+        b_->SetOutwardLink(b_link_);
+
+        broker_node_link_->transport()->Activate();
+        non_broker_node_link_->transport()->Activate();
+        break;
+      }
+    }
+
+    ASSERT_EQ(a_link_->GetLinkState(), b_link_->GetLinkState());
+    link_state_ = a_link_->GetLinkState();
+  }
+
+  void TearDown() override {
+    a_->CloseRoute();
+    b_->CloseRoute();
+    broker_->Close();
+    non_broker_->Close();
+  }
+
+  Router& a() { return *a_; }
+  Router& b() { return *b_; }
+  RouterLink& a_link() { return *a_link_; }
+  RouterLink& b_link() { return *b_link_; }
+  RouterLinkState& link_state() { return *link_state_; }
+  RouterLinkState::Status link_status() { return link_state_->status; }
+
+ private:
+  const Ref<Node> broker_{MakeRefCounted<Node>(Node::Type::kBroker,
+                                               kTestDriver,
+                                               IPCZ_INVALID_DRIVER_HANDLE)};
+  const Ref<Node> non_broker_{MakeRefCounted<Node>(Node::Type::kNormal,
+                                                   kTestDriver,
+                                                   IPCZ_INVALID_DRIVER_HANDLE)};
+  Ref<NodeLink> broker_node_link_;
+  Ref<NodeLink> non_broker_node_link_;
+
+  const Ref<Router> a_{MakeRefCounted<Router>()};
+  const Ref<Router> b_{MakeRefCounted<Router>()};
+  Ref<RouterLink> a_link_;
+  Ref<RouterLink> b_link_;
+  RouterLinkState* link_state_ = nullptr;
+};
+
+TEST_P(RouterLinkTest, Locking) {
+  EXPECT_EQ(RouterLinkState::kUnstable, link_status());
+
+  // No locking can take place until both sides are marked stable.
+  EXPECT_FALSE(a_link().TryLockForBypass(kTestPeer1Name));
+  EXPECT_FALSE(a_link().TryLockForClosure());
+  a_link().MarkSideStable();
+  b_link().MarkSideStable();
+  EXPECT_EQ(RouterLinkState::kStable, link_status());
+
+  // Only one side can lock for bypass, and (only) the other side can use this
+  // to validate the source of a future bypass request.
+  EXPECT_FALSE(b_link().CanNodeRequestBypass(kTestPeer1Name));
+  EXPECT_TRUE(a_link().TryLockForBypass(kTestPeer1Name));
+  EXPECT_FALSE(b_link().TryLockForBypass(kTestPeer2Name));
+  EXPECT_FALSE(b_link().CanNodeRequestBypass(kTestPeer2Name));
+  EXPECT_TRUE(b_link().CanNodeRequestBypass(kTestPeer1Name));
+  EXPECT_FALSE(a_link().CanNodeRequestBypass(kTestPeer1Name));
+  EXPECT_EQ(RouterLinkState::kStable | RouterLinkState::kLockedBySideA,
+            link_status());
+  a_link().Unlock();
+  EXPECT_EQ(RouterLinkState::kStable, link_status());
+
+  EXPECT_TRUE(b_link().TryLockForBypass(kTestPeer2Name));
+  EXPECT_FALSE(a_link().TryLockForBypass(kTestPeer1Name));
+  EXPECT_FALSE(a_link().CanNodeRequestBypass(kTestPeer1Name));
+  EXPECT_FALSE(b_link().CanNodeRequestBypass(kTestPeer2Name));
+  EXPECT_TRUE(a_link().CanNodeRequestBypass(kTestPeer2Name));
+  EXPECT_EQ(RouterLinkState::kStable | RouterLinkState::kLockedBySideB,
+            link_status());
+  b_link().Unlock();
+  EXPECT_EQ(RouterLinkState::kStable, link_status());
+
+  EXPECT_TRUE(a_link().TryLockForClosure());
+  EXPECT_FALSE(b_link().TryLockForClosure());
+  EXPECT_EQ(RouterLinkState::kStable | RouterLinkState::kLockedBySideA,
+            link_status());
+  a_link().Unlock();
+  EXPECT_EQ(RouterLinkState::kStable, link_status());
+
+  EXPECT_TRUE(b_link().TryLockForClosure());
+  EXPECT_FALSE(a_link().TryLockForClosure());
+  EXPECT_EQ(RouterLinkState::kStable | RouterLinkState::kLockedBySideB,
+            link_status());
+  b_link().Unlock();
+  EXPECT_EQ(RouterLinkState::kStable, link_status());
+}
+
+TEST_P(RouterLinkTest, FlushOtherSideIfWaiting) {
+  // FlushOtherSideIfWaiting() does nothing if the other side is not, in fact,
+  // waiting for something.
+  EXPECT_FALSE(a_link().FlushOtherSideIfWaiting());
+  EXPECT_FALSE(b_link().FlushOtherSideIfWaiting());
+  EXPECT_EQ(RouterLinkState::kUnstable, link_status());
+
+  // Mark B stable and try to lock the link. Since A is not yet stable, this
+  // should fail and set B's waiting bit.
+  b_link().MarkSideStable();
+  EXPECT_FALSE(b_link().TryLockForBypass(kTestPeer1Name));
+  EXPECT_EQ(RouterLinkState::kSideBStable | RouterLinkState::kSideBWaiting,
+            link_status());
+
+  // Now mark A stable. The FlushOtherSideIfWaiting() should successfully
+  // flush B and clear its waiting bit.
+  a_link().MarkSideStable();
+  EXPECT_EQ(RouterLinkState::kStable | RouterLinkState::kSideBWaiting,
+            link_status());
+  EXPECT_TRUE(a_link().FlushOtherSideIfWaiting());
+  EXPECT_EQ(RouterLinkState::kStable, link_status());
+}
+
+INSTANTIATE_TEST_SUITE_P(,
+                         RouterLinkTest,
+                         ::testing::Values(RouterLinkTestMode::kLocal,
+                                           RouterLinkTestMode::kRemote));
+
+}  // namespace
+}  // namespace ipcz