ipcz: Propagate node disconnection to routers

Implements node disconnection propagation. When a NodeLink is severed,
it will now notify all Routers to which it has a bound sublink. Routers
treat this as equivalent to route closure from the appropriate end of
the route, if that end was not already closed.

Bug: 1299283
Change-Id: I47c1a838173e354a9421278c50ff5b5b37fdbdc3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3659408
Commit-Queue: Ken Rockot <rockot@google.com>
Reviewed-by: Alex Gough <ajgo@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1007148}
NOKEYCHECK=True
GitOrigin-RevId: 7908ff715a08f2f25837fb7cc05e8e504538227a
diff --git a/src/ipcz/local_router_link.cc b/src/ipcz/local_router_link.cc
index 1314d3e..3d0ce11 100644
--- a/src/ipcz/local_router_link.cc
+++ b/src/ipcz/local_router_link.cc
@@ -82,6 +82,11 @@
   return state_->GetRouter(side_.opposite()).get() == &router;
 }
 
+bool LocalRouterLink::IsRemoteLinkTo(const NodeLink& node_link,
+                                     SublinkId sublink) {
+  return false;
+}
+
 void LocalRouterLink::AcceptParcel(Parcel& parcel) {
   if (Ref<Router> receiver = state_->GetRouter(side_.opposite())) {
     receiver->AcceptInboundParcel(parcel);
diff --git a/src/ipcz/local_router_link.h b/src/ipcz/local_router_link.h
index 393b451..2137609 100644
--- a/src/ipcz/local_router_link.h
+++ b/src/ipcz/local_router_link.h
@@ -26,6 +26,7 @@
   // RouterLink:
   LinkType GetType() 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 Deactivate() override;
diff --git a/src/ipcz/node.cc b/src/ipcz/node.cc
index b604b10..aa24e13 100644
--- a/src/ipcz/node.cc
+++ b/src/ipcz/node.cc
@@ -124,4 +124,29 @@
   return name;
 }
 
+// static
+void Node::SimulateDisconnectForTesting(IpczHandle first, IpczHandle second) {
+  Node* node0 = Node::FromHandle(first);
+  Node* node1 = Node::FromHandle(second);
+
+  node0->SimulateDisconnectForTesting(node1->GetAssignedName());
+  node1->SimulateDisconnectForTesting(node0->GetAssignedName());
+}
+
+void Node::SimulateDisconnectForTesting(const NodeName& name) {
+  absl::MutexLock lock(&mutex_);
+  auto it = node_links_.find(name);
+  if (it == node_links_.end()) {
+    return;
+  }
+
+  Ref<NodeLink> link = std::move(it->second);
+  node_links_.erase(it);
+
+  if (broker_link_ == link) {
+    broker_link_.reset();
+  }
+  link->SimulateDisconnectForTesting();
+}
+
 }  // namespace ipcz
diff --git a/src/ipcz/node.h b/src/ipcz/node.h
index 7806a56..d00f2ad 100644
--- a/src/ipcz/node.h
+++ b/src/ipcz/node.h
@@ -87,9 +87,16 @@
   // randomness.
   NodeName GenerateRandomName() const;
 
+  // Forces disconnection of two specific nodes which have an established
+  // NodeLink to each other.
+  static void SimulateDisconnectForTesting(IpczHandle first, IpczHandle second);
+
  private:
   ~Node() override;
 
+  // Forces disconnection of a specific node from this one.
+  void SimulateDisconnectForTesting(const NodeName& name);
+
   const Type type_;
   const IpczDriver driver_;
   const IpczDriverHandle driver_node_;
diff --git a/src/ipcz/node_link.cc b/src/ipcz/node_link.cc
index 2e3afda..fb301fe 100644
--- a/src/ipcz/node_link.cc
+++ b/src/ipcz/node_link.cc
@@ -138,6 +138,11 @@
   transport_->Transmit(message);
 }
 
+void NodeLink::SimulateDisconnectForTesting() {
+  OnTransportError();
+  Deactivate();
+}
+
 SequenceNumber NodeLink::GenerateOutgoingSequenceNumber() {
   return SequenceNumber(next_outgoing_sequence_number_generator_.fetch_add(
       1, std::memory_order_relaxed));
@@ -243,7 +248,15 @@
 }
 
 void NodeLink::OnTransportError() {
-  // TODO: Notify all routers attached to sublinks here that this link is dead.
+  SublinkMap sublinks;
+  {
+    absl::MutexLock lock(&mutex_);
+    sublinks.swap(sublinks_);
+  }
+
+  for (auto& [id, sublink] : sublinks) {
+    sublink.receiver->NotifyLinkDisconnected(*this, id);
+  }
 }
 
 NodeLink::Sublink::Sublink(Ref<RemoteRouterLink> router_link,
diff --git a/src/ipcz/node_link.h b/src/ipcz/node_link.h
index ed1aa12..537129f 100644
--- a/src/ipcz/node_link.h
+++ b/src/ipcz/node_link.h
@@ -105,6 +105,10 @@
   // memory.
   void Transmit(Message& message);
 
+  // Simulates disconnection by invoking the code path which handles transport
+  // errors and deactiving this link.
+  void SimulateDisconnectForTesting();
+
  private:
   NodeLink(Ref<Node> node,
            LinkSide link_side,
diff --git a/src/ipcz/remote_router_link.cc b/src/ipcz/remote_router_link.cc
index 9bd1ce4..7e9838a 100644
--- a/src/ipcz/remote_router_link.cc
+++ b/src/ipcz/remote_router_link.cc
@@ -44,6 +44,11 @@
   return false;
 }
 
+bool RemoteRouterLink::IsRemoteLinkTo(const NodeLink& node_link,
+                                      SublinkId sublink) {
+  return node_link_.get() == &node_link && sublink_ == sublink;
+}
+
 void RemoteRouterLink::AcceptParcel(Parcel& parcel) {
   const absl::Span<Ref<APIObject>> objects = parcel.objects_view();
 
diff --git a/src/ipcz/remote_router_link.h b/src/ipcz/remote_router_link.h
index e2111bb..4407db7 100644
--- a/src/ipcz/remote_router_link.h
+++ b/src/ipcz/remote_router_link.h
@@ -47,6 +47,7 @@
   // RouterLink:
   LinkType GetType() 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 Deactivate() override;
diff --git a/src/ipcz/router.cc b/src/ipcz/router.cc
index 5c90b9d..97e1231 100644
--- a/src/ipcz/router.cc
+++ b/src/ipcz/router.cc
@@ -49,6 +49,9 @@
 
 bool Router::HasLocalPeer(Router& router) {
   absl::MutexLock lock(&mutex_);
+  if (!outward_link_) {
+    return false;
+  }
   return outward_link_->HasLocalPeer(router);
 }
 
@@ -159,7 +162,9 @@
         return false;
       }
 
-      if (!inward_link_) {
+      if (inward_link_) {
+        inward_link_->AcceptRouteClosure(sequence_length);
+      } else {
         status_.flags |= IPCZ_PORTAL_STATUS_PEER_CLOSED;
         if (inbound_parcels_.IsSequenceFullyConsumed()) {
           status_.flags |= IPCZ_PORTAL_STATUS_DEAD;
@@ -358,6 +363,35 @@
   Flush();
 }
 
+void Router::NotifyLinkDisconnected(const NodeLink& node_link,
+                                    SublinkId sublink) {
+  Ref<RouterLink> dead_outward_link;
+  SequenceNumber inbound_sequence_length;
+  Ref<RouterLink> dead_inward_link;
+  SequenceNumber outbound_sequence_length;
+  {
+    absl::MutexLock lock(&mutex_);
+    if (outward_link_ && outward_link_->IsRemoteLinkTo(node_link, sublink)) {
+      dead_outward_link = std::move(outward_link_);
+      inbound_sequence_length = inbound_parcels_.GetCurrentSequenceLength();
+    } else if (inward_link_ &&
+               inward_link_->IsRemoteLinkTo(node_link, sublink)) {
+      dead_inward_link = std::move(inward_link_);
+      outbound_sequence_length = outbound_parcels_.GetCurrentSequenceLength();
+    }
+  }
+
+  if (dead_outward_link) {
+    AcceptRouteClosureFrom(dead_outward_link->GetType(),
+                           inbound_sequence_length);
+  }
+
+  if (dead_inward_link) {
+    AcceptRouteClosureFrom(dead_inward_link->GetType(),
+                           outbound_sequence_length);
+  }
+}
+
 void Router::Flush() {
   Ref<RouterLink> outward_link;
   Ref<RouterLink> inward_link;
diff --git a/src/ipcz/router.h b/src/ipcz/router.h
index 9c674bc..2a09f30 100644
--- a/src/ipcz/router.h
+++ b/src/ipcz/router.h
@@ -127,6 +127,11 @@
   void BeginProxyingToNewRouter(NodeLink& to_node_link,
                                 const RouterDescriptor& descriptor);
 
+  // Notifies this Router that one of its links has been disconnected from a
+  // remote node. The link is identified by a combination of a specific NodeLink
+  // and SublinkId.
+  void NotifyLinkDisconnected(const NodeLink& node_link, SublinkId sublink);
+
  private:
   ~Router() override;
 
diff --git a/src/ipcz/router_link.h b/src/ipcz/router_link.h
index 84f3867..332b3ca 100644
--- a/src/ipcz/router_link.h
+++ b/src/ipcz/router_link.h
@@ -7,10 +7,12 @@
 
 #include "ipcz/link_type.h"
 #include "ipcz/sequence_number.h"
+#include "ipcz/sublink_id.h"
 #include "util/ref_counted.h"
 
 namespace ipcz {
 
+class NodeLink;
 class Parcel;
 class Router;
 
@@ -32,6 +34,10 @@
   // Returns true iff this is a LocalRouterLink whose peer router is `router`.
   virtual bool HasLocalPeer(const Router& router) = 0;
 
+  // Returns true iff this is a RemoteRouterLink routing over `node_link` via
+  // `sublink`.
+  virtual bool IsRemoteLinkTo(const NodeLink& node_link, SublinkId sublink) = 0;
+
   // Passes a parcel to the Router on the other side of this link to be queued
   // and/or router further.
   virtual void AcceptParcel(Parcel& parcel) = 0;
diff --git a/src/remote_portal_test.cc b/src/remote_portal_test.cc
index 36c4bf2..d05d759 100644
--- a/src/remote_portal_test.cc
+++ b/src/remote_portal_test.cc
@@ -5,6 +5,7 @@
 #include <utility>
 
 #include "ipcz/ipcz.h"
+#include "ipcz/node.h"
 #include "test/multinode_test.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -103,9 +104,49 @@
     EXPECT_EQ("hi", message);
   }
 
+  CloseAll({a, b});
   VerifyEndToEnd(c, d);
 
-  CloseAll({a, b, c, d, node1, node0});
+  CloseAll({c, d, node1, node0});
+}
+
+TEST_P(RemotePortalTest, DisconnectThroughProxy) {
+  // Exercises node disconnection. Namely if portals on nodes 1 and 3 are
+  // connected via proxy on node 2, and node 3 disappears, node 1's portal
+  // should observe peer closure.
+  IpczHandle node0 = CreateBrokerNode();
+  IpczHandle node1 = CreateNonBrokerNode();
+  IpczHandle node2 = CreateNonBrokerNode();
+  IpczHandle node3 = CreateNonBrokerNode();
+
+  auto [a, b] = ConnectBrokerToNonBroker(node0, node1);
+  auto [c, d] = ConnectBrokerToNonBroker(node0, node2);
+  auto [e, f] = ConnectBrokerToNonBroker(node0, node3);
+
+  auto [q, p] = OpenPortals(node0);
+
+  // Send `q` to `node1` and `p` to `node2`.
+  Put(a, "", {&q, 1});
+  Put(c, "", {&p, 1});
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, nullptr, {&q, 1}));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(d, nullptr, {&p, 1}));
+
+  // Now forward 'p' back to `node0` and then again to `node3`. This ensures
+  // that node2 will proxy between node1 and node3 for at least a small window
+  // of time.
+  Put(d, "", {&p, 1});
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(c, nullptr, {&p, 1}));
+  Put(e, "", {&p, 1});
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(f, nullptr, {&p, 1}));
+
+  // TODO: Once proxy reduction is implemented, the test setup should wait for
+  // a direct link between node2 and node3 before then severing only that
+  // connection. Without proxy reduction, no such direct link exists yet.
+  ipcz::Node::SimulateDisconnectForTesting(node0, node3);
+
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(q, IPCZ_TRAP_PEER_CLOSED));
+
+  CloseAll({a, b, c, d, e, f, q, p, node3, node2, node1, node0});
 }
 
 INSTANTIATE_MULTINODE_TEST_SUITE_P(RemotePortalTest);