ipcz: Support ConnectNode() between non-brokers

This allows ConnectNode() to be used on transports between two
non-broker nodes when exactly one of the nodes is already connected to
a broker. In this case the node with a broker connection specifies
IPCZ_CONNECT_NODE_SHARE_BROKER while the other node must specify
IPCZ_CONNECT_NODE_INHERIT_BROKER.

Internally, the first node then passes the given transport on to the
broker, who then performs a handshake with the new non-broker before
introducing it to the referrer.

This is necessary to support some cases where Chrome child processes
may launch child processes of their own.

Bug: 1299283
Change-Id: Ib2bad50d34d7410aac1654837a17180298fde701
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3802772
Commit-Queue: Ken Rockot <rockot@google.com>
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1033112}
NOKEYCHECK=True
GitOrigin-RevId: bde0265b8110c28dcb65f735ad743b4628013ebf
diff --git a/src/connect_test.cc b/src/connect_test.cc
index 1a717db..787db90 100644
--- a/src/connect_test.cc
+++ b/src/connect_test.cc
@@ -6,6 +6,7 @@
 
 #include "ipcz/ipcz.h"
 #include "ipcz/node_messages.h"
+#include "reference_drivers/blob.h"
 #include "test/multinode_test.h"
 #include "test/test_transport_listener.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -61,7 +62,7 @@
 TEST_P(ConnectTest, DisconnectWithoutBrokerHandshake) {
   TransportPair transports = CreateTransports();
   auto controller =
-      SpawnTestNode<ExpectDisconnectFromBroker>(transports.theirs);
+      SpawnTestNodeWithTransport<ExpectDisconnectFromBroker>(transports.theirs);
   EXPECT_EQ(IPCZ_RESULT_OK,
             GetDriver().Close(transports.ours, IPCZ_NO_FLAGS, nullptr));
   controller->WaitForShutdown();
@@ -82,7 +83,7 @@
 TEST_P(ConnectTest, DisconnectOnBadBrokerMessage) {
   TransportPair transports = CreateTransports();
   auto controller =
-      SpawnTestNode<ExpectDisconnectFromBroker>(transports.theirs);
+      SpawnTestNodeWithTransport<ExpectDisconnectFromBroker>(transports.theirs);
 
   // Send some garbage to the other node.
   const char kBadMessage[] = "this will never be a valid handshake message!";
@@ -126,6 +127,119 @@
   controller->WaitForShutdown();
 }
 
+constexpr std::string_view kBlob1Contents = "from q";
+constexpr std::string_view kBlob2Contents = "from p";
+
+MULTINODE_TEST_NODE(ConnectTestNode, NonBrokerToNonBrokerClientChild) {
+  IpczHandle parent = ConnectToParent(IPCZ_CONNECT_NODE_INHERIT_BROKER);
+
+  std::string expected_contents;
+  IpczHandle portal;
+  IpczHandle box;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            WaitToGet(parent, &expected_contents, {&portal, 1}));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(portal, nullptr, {&box, 1}));
+  EXPECT_EQ(expected_contents, UnboxBlob(box));
+
+  PingPong(portal);
+  WaitForDirectRemoteLink(portal);
+  CloseAll({parent, portal});
+}
+
+MULTINODE_TEST_NODE(ConnectTestNode, NonBrokerToNonBrokerClient) {
+  IpczHandle b = ConnectToBroker();
+  IpczHandle c = SpawnTestNode<NonBrokerToNonBrokerClientChild>(
+      IPCZ_CONNECT_NODE_SHARE_BROKER);
+
+  std::string expected_contents;
+  IpczHandle portal;
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitToGet(b, &expected_contents, {&portal, 1}));
+  EXPECT_EQ(IPCZ_RESULT_OK, Put(c, expected_contents, {&portal, 1}));
+
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c, IPCZ_TRAP_PEER_CLOSED));
+  CloseAll({c, b});
+}
+
+TEST_P(ConnectTest, NonBrokerToNonBroker) {
+  IpczHandle c1 = SpawnTestNode<NonBrokerToNonBrokerClient>();
+  IpczHandle c2 = SpawnTestNode<NonBrokerToNonBrokerClient>();
+
+  auto [q, p] = OpenPortals();
+  IpczHandle q_box = BoxBlob(kBlob1Contents);
+  IpczHandle p_box = BoxBlob(kBlob2Contents);
+  EXPECT_EQ(IPCZ_RESULT_OK, Put(q, "", {&q_box, 1}));
+  EXPECT_EQ(IPCZ_RESULT_OK, Put(p, "", {&p_box, 1}));
+  EXPECT_EQ(IPCZ_RESULT_OK, Put(c1, kBlob2Contents, {&q, 1}));
+  EXPECT_EQ(IPCZ_RESULT_OK, Put(c2, kBlob1Contents, {&p, 1}));
+
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c1, IPCZ_TRAP_PEER_CLOSED));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c2, IPCZ_TRAP_PEER_CLOSED));
+  CloseAll({c1, c2});
+}
+
+MULTINODE_TEST_NODE(ConnectTestNode, BadNonBrokerReferralClient) {
+  IpczHandle b = ConnectToBroker();
+
+  TransportPair transports = CreateTransports();
+
+  // Transmit something invalid from the referred node's side of the transport.
+  const char kBadMessage[] = "i am a terrible node plz reject";
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            GetDriver().Transmit(transports.theirs, kBadMessage,
+                                 std::size(kBadMessage), nullptr, 0,
+                                 IPCZ_NO_FLAGS, nullptr));
+
+  // Now refer our imaginary other node using our end of the transport. The
+  // broker should reject the referral and we should eventually observe
+  // disconnection of our initial portal to the referred node.
+  IpczHandle p;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().ConnectNode(node(), transports.ours, 1,
+                               IPCZ_CONNECT_NODE_SHARE_BROKER, nullptr, &p));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(p, IPCZ_TRAP_PEER_CLOSED));
+  CloseAll({b, p});
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            GetDriver().Close(transports.theirs, IPCZ_NO_FLAGS, nullptr));
+}
+
+TEST_P(ConnectTest, BadNonBrokerReferral) {
+  IpczHandle c = SpawnTestNode<BadNonBrokerReferralClient>();
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c, IPCZ_TRAP_PEER_CLOSED));
+  Close(c);
+}
+
+MULTINODE_TEST_NODE(ConnectTestNode, FailedNonBrokerReferralReferredClient) {
+  IpczHandle p;
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            ipcz().ConnectNode(node(), ReleaseTransport(), 1,
+                               IPCZ_CONNECT_NODE_INHERIT_BROKER, nullptr, &p));
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(p, IPCZ_TRAP_PEER_CLOSED));
+  Close(p);
+}
+
+MULTINODE_TEST_NODE(ConnectTestNode, FailedNonBrokerReferralClient) {
+  IpczHandle b = ConnectToBroker();
+
+  TransportPair transports = CreateTransports();
+  auto controller =
+      SpawnTestNodeWithTransport<FailedNonBrokerReferralReferredClient>(
+          transports.theirs);
+
+  // Disconnect the transport instead of passing to our broker with
+  // ConnectNode(). The referred client should observe disconnection of its
+  // initial portals and terminate itself.
+  EXPECT_EQ(IPCZ_RESULT_OK,
+            GetDriver().Close(transports.ours, IPCZ_NO_FLAGS, nullptr));
+  controller->WaitForShutdown();
+  Close(b);
+}
+
+TEST_P(ConnectTest, FailedNonBrokerReferral) {
+  IpczHandle c = SpawnTestNode<FailedNonBrokerReferralClient>();
+  EXPECT_EQ(IPCZ_RESULT_OK, WaitForConditionFlags(c, IPCZ_TRAP_PEER_CLOSED));
+  Close(c);
+}
+
 INSTANTIATE_MULTINODE_TEST_SUITE_P(ConnectTest);
 
 }  // namespace
diff --git a/src/ipcz/node.cc b/src/ipcz/node.cc
index 258b5a5..19e584a 100644
--- a/src/ipcz/node.cc
+++ b/src/ipcz/node.cc
@@ -81,9 +81,17 @@
 }
 
 void Node::SetBrokerLink(Ref<NodeLink> link) {
-  absl::MutexLock lock(&mutex_);
-  ABSL_ASSERT(!broker_link_);
-  broker_link_ = std::move(link);
+  std::vector<BrokerLinkCallback> callbacks;
+  {
+    absl::MutexLock lock(&mutex_);
+    ABSL_ASSERT(!broker_link_);
+    broker_link_ = link;
+    broker_link_callbacks_.swap(callbacks);
+  }
+
+  for (auto& callback : callbacks) {
+    callback(link);
+  }
 }
 
 void Node::SetAssignedName(const NodeName& name) {
@@ -351,6 +359,21 @@
   }
 }
 
+void Node::WaitForBrokerLinkAsync(BrokerLinkCallback callback) {
+  Ref<NodeLink> broker_link;
+  {
+    absl::MutexLock lock(&mutex_);
+    if (!broker_link_) {
+      broker_link_callbacks_.push_back(std::move(callback));
+      return;
+    }
+
+    broker_link = broker_link_;
+  }
+
+  callback(std::move(broker_link));
+}
+
 void Node::ShutDown() {
   NodeLinkMap node_links;
   {
diff --git a/src/ipcz/node.h b/src/ipcz/node.h
index 5f36a9b..14bea6c 100644
--- a/src/ipcz/node.h
+++ b/src/ipcz/node.h
@@ -157,6 +157,12 @@
   // Drops this node's link to the named node, if one exists.
   void DropLink(const NodeName& name);
 
+  // Asynchronously waits for this Node to acquire a broker link and then
+  // invokes `callback` with it. If this node already has a broker link then the
+  // callback is invoked immediately, before this method returns.
+  using BrokerLinkCallback = std::function<void(Ref<NodeLink>)>;
+  void WaitForBrokerLinkAsync(BrokerLinkCallback callback);
+
  private:
   ~Node() override;
 
@@ -232,6 +238,11 @@
   using IntroductionKey = std::pair<NodeName, NodeName>;
   absl::flat_hash_set<IntroductionKey> in_progress_introductions_
       ABSL_GUARDED_BY(mutex_);
+
+  // Set of callbacks waiting to be invoked as soon as this Node acquires a
+  // broker link.
+  std::vector<BrokerLinkCallback> broker_link_callbacks_
+      ABSL_GUARDED_BY(mutex_);
 };
 
 }  // namespace ipcz
diff --git a/src/ipcz/node_connector.cc b/src/ipcz/node_connector.cc
index d33b320..fc44b57 100644
--- a/src/ipcz/node_connector.cc
+++ b/src/ipcz/node_connector.cc
@@ -142,14 +142,265 @@
   }
 };
 
+// Unlike other NodeConnectors, this one doesn't activate its transport or
+// listen for any messages. Instead it uses an existing broker link to pass the
+// transport along to a broker and wait for a reply to confirm either
+// acceptance or rejection of the referral.
+class NodeConnectorForReferrer : public NodeConnector {
+ public:
+  NodeConnectorForReferrer(Ref<Node> node,
+                           Ref<DriverTransport> transport,
+                           IpczConnectNodeFlags flags,
+                           std::vector<Ref<Portal>> waiting_portals,
+                           Ref<NodeLink> broker_link,
+                           ConnectCallback callback)
+      : NodeConnector(std::move(node),
+                      /*transport=*/nullptr,
+                      flags,
+                      std::move(waiting_portals),
+                      std::move(callback)),
+        transport_for_broker_(std::move(transport)),
+        broker_link_(std::move(broker_link)) {}
+
+  ~NodeConnectorForReferrer() override = default;
+
+  // NodeConnector:
+  bool Connect() override {
+    ABSL_ASSERT(node_->type() == Node::Type::kNormal);
+    if (!broker_link_) {
+      // If there's no broker link yet, wait for one.
+      node_->WaitForBrokerLinkAsync(
+          [connector = WrapRefCounted(this)](Ref<NodeLink> broker_link) {
+            connector->broker_link_ = std::move(broker_link);
+            connector->Connect();
+          });
+      return true;
+    }
+
+    broker_link_->ReferNonBroker(
+        std::move(transport_for_broker_), checked_cast<uint32_t>(num_portals()),
+        [connector = WrapRefCounted(this)](
+            Ref<NodeLink> link_to_referred_node,
+            uint32_t remote_num_initial_portals) {
+          if (link_to_referred_node) {
+            connector->AcceptConnection(std::move(link_to_referred_node),
+                                        LinkSide::kA,
+                                        remote_num_initial_portals);
+          } else {
+            connector->RejectConnection();
+          }
+        });
+    return true;
+  }
+
+ private:
+  Ref<DriverTransport> transport_for_broker_;
+  Ref<NodeLink> broker_link_;
+};
+
+// A NodeConnector used by a referred node to await acceptance by the broker.
+class NodeConnectorForReferredNonBroker : public NodeConnector {
+ public:
+  NodeConnectorForReferredNonBroker(Ref<Node> node,
+                                    Ref<DriverTransport> transport,
+                                    IpczConnectNodeFlags flags,
+                                    std::vector<Ref<Portal>> waiting_portals,
+                                    ConnectCallback callback)
+      : NodeConnector(std::move(node),
+                      std::move(transport),
+                      flags,
+                      std::move(waiting_portals),
+                      std::move(callback)) {}
+
+  ~NodeConnectorForReferredNonBroker() override = default;
+
+  // NodeConnector:
+  bool Connect() override {
+    ABSL_ASSERT(node_->type() == Node::Type::kNormal);
+    msg::ConnectToReferredBroker connect;
+    connect.params().protocol_version = msg::kProtocolVersion;
+    connect.params().num_initial_portals =
+        checked_cast<uint32_t>(num_portals());
+    return IPCZ_RESULT_OK == transport_->Transmit(connect);
+  }
+
+  // NodeMessageListener overrides:
+  bool OnConnectToReferredNonBroker(
+      msg::ConnectToReferredNonBroker& connect) override {
+    DVLOG(4) << "New node accepting ConnectToReferredNonBroker with assigned "
+             << "name " << connect.params().name.ToString() << " from broker "
+             << connect.params().broker_name.ToString() << " as referred by "
+             << connect.params().referrer_name.ToString();
+
+    DriverMemory broker_buffer(
+        connect.TakeDriverObject(connect.params().broker_link_buffer));
+    Ref<DriverTransport> referrer_transport = MakeRefCounted<DriverTransport>(
+        connect.TakeDriverObject(connect.params().referrer_link_transport));
+    DriverMemory referrer_buffer(
+        connect.TakeDriverObject(connect.params().referrer_link_buffer));
+    if (!broker_buffer.is_valid() || !referrer_buffer.is_valid() ||
+        !referrer_transport->driver_object().is_valid()) {
+      return false;
+    }
+
+    // Ensure this NodeConnector stays alive until this method returns.
+    // Otherwise the last reference may be dropped when the new NodeLink takes
+    // over listening on `transport_`.
+    Ref<NodeConnector> self(this);
+    const uint32_t broker_protocol_version = std::min(
+        connect.params().broker_protocol_version, msg::kProtocolVersion);
+    auto broker_link = NodeLink::CreateActive(
+        node_, LinkSide::kB, connect.params().name,
+        connect.params().broker_name, Node::Type::kBroker,
+        broker_protocol_version, transport_,
+        NodeLinkMemory::Create(node_, broker_buffer.Map()));
+    node_->SetAssignedName(connect.params().name);
+    node_->SetBrokerLink(broker_link);
+    if ((flags_ & IPCZ_CONNECT_NODE_TO_ALLOCATION_DELEGATE) != 0) {
+      node_->SetAllocationDelegate(broker_link);
+    }
+    node_->AddLink(connect.params().broker_name, std::move(broker_link));
+
+    const uint32_t referrer_protocol_version = std::min(
+        connect.params().referrer_protocol_version, msg::kProtocolVersion);
+    auto referrer_link = NodeLink::CreateInactive(
+        node_, LinkSide::kB, connect.params().name,
+        connect.params().referrer_name, Node::Type::kNormal,
+        referrer_protocol_version, std::move(referrer_transport),
+        NodeLinkMemory::Create(node_, referrer_buffer.Map()));
+
+    AcceptConnection(referrer_link, LinkSide::kB,
+                     connect.params().num_initial_portals);
+    referrer_link->Activate();
+    return true;
+  }
+};
+
+// The NodeConnector used by a broker to await a handshake from the referred
+// node before responding to both that node and the referrer.
+class NodeConnectorForBrokerReferral : public NodeConnector {
+ public:
+  NodeConnectorForBrokerReferral(Ref<Node> node,
+                                 uint64_t referral_id,
+                                 uint32_t num_initial_portals,
+                                 Ref<NodeLink> referrer,
+                                 Ref<DriverTransport> transport)
+      : NodeConnector(std::move(node),
+                      std::move(transport),
+                      IPCZ_NO_FLAGS,
+                      /*waiting_portals=*/{},
+                      /*callback=*/nullptr),
+        referral_id_(referral_id),
+        num_initial_portals_(num_initial_portals),
+        referrer_(std::move(referrer)) {
+    ABSL_ASSERT(link_memory_.mapping.is_valid());
+    ABSL_ASSERT(client_link_memory_.mapping.is_valid());
+  }
+
+  ~NodeConnectorForBrokerReferral() override = default;
+
+  // NodeConnector:
+  bool Connect() override { return true; }
+
+  // NodeMessageListener overrides:
+  bool OnConnectToReferredBroker(
+      msg::ConnectToReferredBroker& connect_to_broker) override {
+    DVLOG(4) << "Accepting ConnectToReferredBroker on broker "
+             << broker_name_.ToString() << " from new referred node "
+             << referred_node_name_.ToString();
+
+    // Ensure this NodeConnector stays alive until this method returns.
+    // Otherwise the last reference may be dropped when the new NodeLink below
+    // takes over listening on `transport_`.
+    Ref<NodeConnector> self(this);
+
+    // First, accept the new non-broker client on this broker node. There are
+    // no initial portals on this link, as this link was not established
+    // directly by the application. Note that this takes over listsening on
+    // `transport_`
+    const uint32_t protocol_version = std::min(
+        connect_to_broker.params().protocol_version, msg::kProtocolVersion);
+    Ref<NodeLink> link_to_referree = NodeLink::CreateActive(
+        node_, LinkSide::kA, broker_name_, referred_node_name_,
+        Node::Type::kNormal, protocol_version, transport_,
+        NodeLinkMemory::Create(node_, std::move(link_memory_.mapping)));
+    AcceptConnection(link_to_referree, LinkSide::kA, /*num_remote_portals=*/0);
+
+    // Now we can create a new link to introduce both clients -- the referrer
+    // and the referree -- to each other.
+    auto [referrer, referree] = DriverTransport::CreatePair(
+        node_->driver(), referrer_->transport().get(), transport_.get());
+
+    // Give the referred node a reply with sufficient details for it to
+    // establish links to both this broker and the referrer simultaneously.
+    //
+    // SUBTLE: It's important that this message is sent before the message to
+    // the referrer below. Otherwise the referrer might begin relaying messages
+    // through the broker to the referree before this handshake is sent to the
+    // referree, which would be bad.
+    msg::ConnectToReferredNonBroker connect;
+    connect.params().name = referred_node_name_;
+    connect.params().broker_name = broker_name_;
+    connect.params().referrer_name = referrer_->remote_node_name();
+    connect.params().broker_protocol_version = protocol_version;
+    connect.params().referrer_protocol_version =
+        referrer_->remote_protocol_version();
+    connect.params().num_initial_portals = num_initial_portals_;
+    connect.params().broker_link_buffer =
+        connect.AppendDriverObject(link_memory_.memory.TakeDriverObject());
+    connect.params().referrer_link_transport =
+        connect.AppendDriverObject(referree->TakeDriverObject());
+    connect.params().referrer_link_buffer = connect.AppendDriverObject(
+        client_link_memory_.memory.Clone().TakeDriverObject());
+    link_to_referree->Transmit(connect);
+
+    // Finally, give the referrer a repy which includes details of its new link
+    // to the referred node.
+    msg::NonBrokerReferralAccepted accepted;
+    accepted.params().referral_id = referral_id_;
+    accepted.params().protocol_version =
+        connect_to_broker.params().protocol_version;
+    accepted.params().num_initial_portals =
+        connect_to_broker.params().num_initial_portals;
+    accepted.params().name = referred_node_name_;
+    accepted.params().transport =
+        accepted.AppendDriverObject(referrer->TakeDriverObject());
+    accepted.params().buffer = accepted.AppendDriverObject(
+        client_link_memory_.memory.TakeDriverObject());
+    referrer_->Transmit(accepted);
+    return true;
+  }
+
+  void OnTransportError() override {
+    msg::NonBrokerReferralRejected rejected;
+    rejected.params().referral_id = referral_id_;
+    referrer_->Transmit(rejected);
+
+    NodeConnector::OnTransportError();
+  }
+
+ private:
+  const uint64_t referral_id_;
+  const uint32_t num_initial_portals_;
+  const Ref<NodeLink> referrer_;
+  const NodeName broker_name_{node_->GetAssignedName()};
+  const NodeName referred_node_name_{node_->GenerateRandomName()};
+  DriverMemoryWithMapping link_memory_{
+      NodeLinkMemory::AllocateMemory(node_->driver())};
+  DriverMemoryWithMapping client_link_memory_{
+      NodeLinkMemory::AllocateMemory(node_->driver())};
+};
+
 std::pair<Ref<NodeConnector>, IpczResult> CreateConnector(
     Ref<Node> node,
     Ref<DriverTransport> transport,
     IpczConnectNodeFlags flags,
     const std::vector<Ref<Portal>>& initial_portals,
+    Ref<NodeLink> broker_link,
     NodeConnector::ConnectCallback callback) {
   const bool from_broker = node->type() == Node::Type::kBroker;
   const bool to_broker = (flags & IPCZ_CONNECT_NODE_TO_BROKER) != 0;
+  const bool share_broker = (flags & IPCZ_CONNECT_NODE_SHARE_BROKER) != 0;
   const bool inherit_broker = (flags & IPCZ_CONNECT_NODE_INHERIT_BROKER) != 0;
   if (from_broker) {
     if (to_broker) {
@@ -171,9 +422,21 @@
             IPCZ_RESULT_OK};
   }
 
-  // TODO: Implement non-broker to non-broker connections (broker sharing.)
-  ABSL_ASSERT(!inherit_broker);
-  return {nullptr, IPCZ_RESULT_FAILED_PRECONDITION};
+  if (share_broker) {
+    return {MakeRefCounted<NodeConnectorForReferrer>(
+                std::move(node), std::move(transport), flags, initial_portals,
+                std::move(broker_link), std::move(callback)),
+            IPCZ_RESULT_OK};
+  }
+
+  if (inherit_broker) {
+    return {MakeRefCounted<NodeConnectorForReferredNonBroker>(
+                std::move(node), std::move(transport), flags, initial_portals,
+                std::move(callback)),
+            IPCZ_RESULT_OK};
+  }
+
+  return {nullptr, IPCZ_RESULT_INVALID_ARGUMENT};
 }
 
 }  // namespace
@@ -197,25 +460,44 @@
     return IPCZ_RESULT_INVALID_ARGUMENT;
   }
 
-  // TODO: Implement broker sharing and inheritance.
-  ABSL_ASSERT(!share_broker && !inherit_broker);
-
-  auto [connector, result] =
-      CreateConnector(std::move(node), std::move(transport), flags,
-                      initial_portals, std::move(callback));
+  auto [connector, result] = CreateConnector(
+      std::move(node), std::move(transport), flags, initial_portals,
+      std::move(broker_link), std::move(callback));
   if (result != IPCZ_RESULT_OK) {
     return result;
   }
 
-  if (!connector->ActivateTransportAndConnect()) {
-    // The driver either failed to activate its transport, or failed to transmit
-    // a handshake message.
+  if (!share_broker && !connector->ActivateTransport()) {
+    // Note that when referring another node to our own broker, we don't
+    // activate the transport, since the transport will be passed to the broker.
+    // See NodeConnectorForReferrer.
+    return IPCZ_RESULT_UNKNOWN;
+  }
+
+  if (!connector->Connect()) {
     return IPCZ_RESULT_UNKNOWN;
   }
 
   return IPCZ_RESULT_OK;
 }
 
+// static
+bool NodeConnector::HandleNonBrokerReferral(
+    Ref<Node> node,
+    uint64_t referral_id,
+    uint32_t num_initial_portals,
+    Ref<NodeLink> referrer,
+    Ref<DriverTransport> transport_to_referred_node) {
+  ABSL_ASSERT(node->type() == Node::Type::kBroker);
+  auto connector = MakeRefCounted<NodeConnectorForBrokerReferral>(
+      std::move(node), referral_id, num_initial_portals, std::move(referrer),
+      std::move(transport_to_referred_node));
+
+  // The connector effectively owns itself and lives only until its transport is
+  // disconnected or it receives a greeting from the referred node.
+  return connector->ActivateTransport();
+}
+
 NodeConnector::NodeConnector(Ref<Node> node,
                              Ref<DriverTransport> transport,
                              IpczConnectNodeFlags flags,
@@ -244,17 +526,18 @@
     callback_(nullptr);
   }
   EstablishWaitingPortals(nullptr, LinkSide::kA, 0);
-  transport_->Deactivate();
+  if (transport_) {
+    transport_->Deactivate();
+  }
 }
 
-bool NodeConnector::ActivateTransportAndConnect() {
+bool NodeConnector::ActivateTransport() {
   transport_->set_listener(WrapRefCounted(this));
   if (transport_->Activate() != IPCZ_RESULT_OK) {
     RejectConnection();
     return false;
   }
-
-  return Connect();
+  return true;
 }
 
 void NodeConnector::EstablishWaitingPortals(Ref<NodeLink> to_link,
diff --git a/src/ipcz/node_connector.h b/src/ipcz/node_connector.h
index e5345d0..d137943 100644
--- a/src/ipcz/node_connector.h
+++ b/src/ipcz/node_connector.h
@@ -49,6 +49,18 @@
                                 const std::vector<Ref<Portal>>& initial_portals,
                                 ConnectCallback callback = nullptr);
 
+  // Handles a request on `node` (which must be a broker) to accept a new
+  // non-broker node referral from `referrer`, referring a new non-broker node
+  // on the remote end of `transport_to_referred_node`. This performs a
+  // handshake with the referred node before introducing it and the referrer to
+  // each other.
+  static bool HandleNonBrokerReferral(
+      Ref<Node> node,
+      uint64_t referral_id,
+      uint32_t num_initial_portals,
+      Ref<NodeLink> referrer,
+      Ref<DriverTransport> transport_to_referred_node);
+
   virtual bool Connect() = 0;
 
  protected:
@@ -78,15 +90,15 @@
   const IpczConnectNodeFlags flags_;
   const std::vector<Ref<Portal>> waiting_portals_;
 
+  // NodeMessageListener overrides:
+  void OnTransportError() override;
+
  private:
-  bool ActivateTransportAndConnect();
+  bool ActivateTransport();
   void EstablishWaitingPortals(Ref<NodeLink> to_link,
                                LinkSide link_side,
                                size_t max_valid_portals);
 
-  // NodeMessageListener overrides:
-  void OnTransportError() override;
-
   const ConnectCallback callback_;
 };
 
diff --git a/src/ipcz/node_link.cc b/src/ipcz/node_link.cc
index 1a09862..1bf9a92 100644
--- a/src/ipcz/node_link.cc
+++ b/src/ipcz/node_link.cc
@@ -19,6 +19,7 @@
 #include "ipcz/link_type.h"
 #include "ipcz/message.h"
 #include "ipcz/node.h"
+#include "ipcz/node_connector.h"
 #include "ipcz/node_link_memory.h"
 #include "ipcz/node_messages.h"
 #include "ipcz/parcel.h"
@@ -213,6 +214,33 @@
   Transmit(reject);
 }
 
+void NodeLink::ReferNonBroker(Ref<DriverTransport> transport,
+                              uint32_t num_initial_portals,
+                              ReferralCallback callback) {
+  ABSL_ASSERT(node_->type() == Node::Type::kNormal &&
+              remote_node_type_ == Node::Type::kBroker);
+
+  uint64_t referral_id;
+  {
+    absl::MutexLock lock(&mutex_);
+    for (;;) {
+      referral_id = next_referral_id_++;
+      auto [it, inserted] =
+          pending_referrals_.try_emplace(referral_id, std::move(callback));
+      if (inserted) {
+        break;
+      }
+    }
+  }
+
+  msg::ReferNonBroker refer;
+  refer.params().referral_id = referral_id;
+  refer.params().num_initial_portals = num_initial_portals;
+  refer.params().transport =
+      refer.AppendDriverObject(transport->TakeDriverObject());
+  Transmit(refer);
+}
+
 void NodeLink::AcceptBypassLink(
     const NodeName& current_peer_node,
     SublinkId current_peer_sublink,
@@ -327,6 +355,82 @@
       1, std::memory_order_relaxed));
 }
 
+bool NodeLink::OnReferNonBroker(msg::ReferNonBroker& refer) {
+  if (remote_node_type_ != Node::Type::kNormal ||
+      node()->type() != Node::Type::kBroker) {
+    return false;
+  }
+
+  DriverObject transport = refer.TakeDriverObject(refer.params().transport);
+  if (!transport.is_valid()) {
+    return false;
+  }
+
+  return NodeConnector::HandleNonBrokerReferral(
+      node(), refer.params().referral_id, refer.params().num_initial_portals,
+      WrapRefCounted(this),
+      MakeRefCounted<DriverTransport>(std::move(transport)));
+}
+
+bool NodeLink::OnNonBrokerReferralAccepted(
+    msg::NonBrokerReferralAccepted& accepted) {
+  if (remote_node_type_ != Node::Type::kBroker) {
+    return false;
+  }
+
+  ReferralCallback callback;
+  {
+    absl::MutexLock lock(&mutex_);
+    auto it = pending_referrals_.find(accepted.params().referral_id);
+    if (it == pending_referrals_.end()) {
+      return false;
+    }
+    callback = std::move(it->second);
+    pending_referrals_.erase(it);
+  }
+
+  const uint32_t protocol_version =
+      std::min(msg::kProtocolVersion, accepted.params().protocol_version);
+  auto transport = MakeRefCounted<DriverTransport>(
+      accepted.TakeDriverObject(accepted.params().transport));
+  DriverMemory buffer(accepted.TakeDriverObject(accepted.params().buffer));
+  if (!transport->driver_object().is_valid() || !buffer.is_valid()) {
+    // Not quite a validation failure if the broker simply failed to allocate
+    // resources for this link. Treat it like a connection failure.
+    callback(/*link=*/nullptr, /*num_initial_portals=*/0);
+    return true;
+  }
+
+  Ref<NodeLink> link_to_referree = NodeLink::CreateInactive(
+      node_, LinkSide::kA, local_node_name_, accepted.params().name,
+      Node::Type::kNormal, protocol_version, std::move(transport),
+      NodeLinkMemory::Create(node_, buffer.Map()));
+  callback(link_to_referree, accepted.params().num_initial_portals);
+  link_to_referree->Activate();
+  return true;
+}
+
+bool NodeLink::OnNonBrokerReferralRejected(
+    msg::NonBrokerReferralRejected& rejected) {
+  if (remote_node_type_ != Node::Type::kBroker) {
+    return false;
+  }
+
+  ReferralCallback callback;
+  {
+    absl::MutexLock lock(&mutex_);
+    auto it = pending_referrals_.find(rejected.params().referral_id);
+    if (it == pending_referrals_.end()) {
+      return false;
+    }
+    callback = std::move(it->second);
+    pending_referrals_.erase(it);
+  }
+
+  callback(/*link=*/nullptr, /*num_initial_portals=*/0);
+  return true;
+}
+
 bool NodeLink::OnRequestIntroduction(msg::RequestIntroduction& request) {
   // TODO: Support broker-to-broker introduction requests.
   if (remote_node_type_ != Node::Type::kNormal ||
diff --git a/src/ipcz/node_link.h b/src/ipcz/node_link.h
index 0ba40fa..f3b83cb 100644
--- a/src/ipcz/node_link.h
+++ b/src/ipcz/node_link.h
@@ -145,6 +145,18 @@
   // node identified by `name`.
   void RejectIntroduction(const NodeName& name);
 
+  // May be called on a link from a non-broker to a broker in order to refer a
+  // new node to the remote broker. `transport` is a transport whose peer
+  // endpoint belongs to the referred node, and `num_initial_portals` is the
+  // number of initial portals expected on the resulting link from this side.
+  // Upon success, `callback` is invoked with a new NodeLink to the referred
+  // node and the number of initial portals expected by that side. On failure,
+  // `callback` is invoked with a null link.
+  using ReferralCallback = std::function<void(Ref<NodeLink>, uint32_t)>;
+  void ReferNonBroker(Ref<DriverTransport> transport,
+                      uint32_t num_initial_portals,
+                      ReferralCallback callback);
+
   // Sends a request to the remote node to establish a new RouterLink over this
   // this NodeLink, to replace an existing RouterLink between the remote node
   // and `current_peer_node`. `current_peer_sublink` identifies the specific
@@ -215,6 +227,11 @@
   SequenceNumber GenerateOutgoingSequenceNumber();
 
   // NodeMessageListener overrides:
+  bool OnReferNonBroker(msg::ReferNonBroker& refer) override;
+  bool OnNonBrokerReferralAccepted(
+      msg::NonBrokerReferralAccepted& accepted) override;
+  bool OnNonBrokerReferralRejected(
+      msg::NonBrokerReferralRejected& rejected) override;
   bool OnRequestIntroduction(msg::RequestIntroduction& request) override;
   bool OnAcceptIntroduction(msg::AcceptIntroduction& accept) override;
   bool OnRejectIntroduction(msg::RejectIntroduction& reject) override;
@@ -278,6 +295,11 @@
   using PartialParcelKey = std::tuple<SublinkId, SequenceNumber>;
   using PartialParcelMap = absl::flat_hash_map<PartialParcelKey, Parcel>;
   PartialParcelMap partial_parcels_ ABSL_GUARDED_BY(mutex_);
+
+  // Tracks pending referrals sent to the broker.
+  uint64_t next_referral_id_ = 0;
+  absl::flat_hash_map<uint64_t, ReferralCallback> pending_referrals_
+      ABSL_GUARDED_BY(mutex_);
 };
 
 }  // namespace ipcz
diff --git a/src/ipcz/node_messages_generator.h b/src/ipcz/node_messages_generator.h
index 36f6d6f..d3e8a94 100644
--- a/src/ipcz/node_messages_generator.h
+++ b/src/ipcz/node_messages_generator.h
@@ -54,6 +54,135 @@
   IPCZ_MSG_PARAM(uint32_t, num_initial_portals)
 IPCZ_MSG_END()
 
+// Sent from a non-broker to its broker when calling ConnectNode() with
+// IPCZ_CONNECT_NODE_SHARE_BROKER. In this case the transport given to
+// ConnectNode() is passed along to the broker via this message, and the broker
+// assumes the other end of that transport belongs to a new non-broker node who
+// wishes to join the network.
+//
+// The broker performs an initial handshake with the referred node -- it waits
+// for a ConnectToReferredBroker message on the new transport and then sends a
+// ConnectToReferredNonBroker over the same transport, as well as a
+// NonBrokerReferralAccepted message back to the original referrer who sent this
+// request.
+//
+// If this request is invalid or the broker otherwise fails to establish a link
+// to the referred node, the broker instead responds to the referrer with a
+// NonBrokerReferralRejected message.
+IPCZ_MSG_BEGIN(ReferNonBroker, IPCZ_MSG_ID(2), IPCZ_MSG_VERSION(0))
+  // A unique (for the transmitting NodeLink) identifier for this referral, used
+  // to associate a corresponding NonBrokerReferralAccepted/Rejected response
+  // from the broker.
+  IPCZ_MSG_PARAM(uint64_t, referral_id)
+
+  // The number of initial portals the referrer will assume on its own transport
+  // to the referred node if the referral is successful and the broker responds
+  // with NonBrokerReferralAccepted. This value is passed along to the referred
+  // node via ConnectToReferredNonBroker.
+  IPCZ_MSG_PARAM(uint32_t, num_initial_portals)
+
+  // The transport given to ConnectNode() with IPCZ_CONNECT_NODE_SHARE_BROKER.
+  IPCZ_MSG_PARAM_DRIVER_OBJECT(transport)
+IPCZ_MSG_END()
+
+// Sent from a non-broker to its tentative broker when calling ConnectNode()
+// with IPCZ_CONNECT_NODE_INHERIT_BROKER. The other end of the transport given
+// to that ConnectNode() call must itself be given to ConnectNode() by some
+// other non-broker calling with IPCZ_CONNECT_NODE_SHARE_BROKER. That other node
+// will pass the transport to the broker using a ReferNonBroker message.
+//
+// Once ConnectToReferredBroker is received by the broker on the new transport,
+// the broker sends back a ConnectToReferredNonBroker to the sender of this
+// message, as well as a NonBrokerReferralAccepted message to the original
+// referrer.
+IPCZ_MSG_BEGIN(ConnectToReferredBroker, IPCZ_MSG_ID(3), IPCZ_MSG_VERSION(0))
+  // The highest protocol version known and desired by the sender.
+  IPCZ_MSG_PARAM(uint32_t, protocol_version)
+
+  // The number of initial portals assumed on the sender's end of the transport.
+  // This is passed along by the broker to the referrer via
+  // NonBrokerReferralAccepted.
+  IPCZ_MSG_PARAM(uint32_t, num_initial_portals)
+IPCZ_MSG_END()
+
+// Sent from a broker to a referred non-broker node over a transport that was
+// provided to the broker by some other non-broker via a ReferNonBroker message.
+//
+// This is sent to the referred node if and only if the referral has been
+// accepted by the broker, and only the broker has received a
+// ConnectToReferredBroker message over the same transport that sends this
+// message.
+IPCZ_MSG_BEGIN(ConnectToReferredNonBroker, IPCZ_MSG_ID(4), IPCZ_MSG_VERSION(0))
+  // The newly assigned name of the node receiving this message.
+  IPCZ_MSG_PARAM(NodeName, name)
+
+  // The name of the broker node which has accepted the referred recipient of
+  // this message.
+  IPCZ_MSG_PARAM(NodeName, broker_name)
+
+  // The name of the node which referred the recipient to the broker sending
+  // this message.
+  IPCZ_MSG_PARAM(NodeName, referrer_name)
+
+  // The highest protocol version known and desired by the sending broker.
+  IPCZ_MSG_PARAM(uint32_t, broker_protocol_version)
+
+  // The highest protocol version known and desired by the referrer.
+  IPCZ_MSG_PARAM(uint32_t, referrer_protocol_version)
+
+  // The number of initial portals assumed by the referred on its initial link
+  // to the receipient of this message.
+  IPCZ_MSG_PARAM(uint32_t, num_initial_portals)
+
+  // A driver memory object to serve as the primary NodeLinkMemory buffer for
+  // the NodeLink between the broker and the recipient of this message (i.e.
+  // the NodeLink established from the transport which carries this message.)
+  IPCZ_MSG_PARAM_DRIVER_OBJECT(broker_link_buffer)
+
+  // A new transport and primary buffer the receipient can use to establish a
+  // new NodeLink to the referrer. The other end of the transport (and another
+  // handle to the same memory object) is given to the referrer via
+  // NonBrokerReferralAccepted.
+  IPCZ_MSG_PARAM_DRIVER_OBJECT(referrer_link_transport)
+  IPCZ_MSG_PARAM_DRIVER_OBJECT(referrer_link_buffer)
+IPCZ_MSG_END()
+
+// Sent from a broker to a non-broker who previously referred another node via
+// ReferNonBroker. This message indicates that the referral was accepted, and it
+// provides objects and details necessary for the recipient (i.e. the referrer)
+// to establish a direct NodeLink to the referred node.
+IPCZ_MSG_BEGIN(NonBrokerReferralAccepted, IPCZ_MSG_ID(5), IPCZ_MSG_VERSION(0))
+  // A unique identifier for the referral in question, as provided by the
+  // original ReferNonBroker message sent by the receipient of this message.
+  IPCZ_MSG_PARAM(uint64_t, referral_id)
+
+  // The highest protocol version known and desired by the referred node.
+  IPCZ_MSG_PARAM(uint32_t, protocol_version)
+
+  // The number of initial portals assumed by the referred node on its end of
+  // the link conveyed by `transport` in this message.
+  IPCZ_MSG_PARAM(uint32_t, num_initial_portals)
+
+  // The name of the referred node, as assigned by the broker.
+  IPCZ_MSG_PARAM(NodeName, name)
+
+  // A driver transport and primary buffer memory object the receipient can use
+  // to establish a direct NodeLink to the referred node.
+  IPCZ_MSG_PARAM_DRIVER_OBJECT(transport)
+  IPCZ_MSG_PARAM_DRIVER_OBJECT(buffer)
+IPCZ_MSG_END()
+
+// Sent from a broker to a non-broker who previously referred another node via
+// ReferNonBroker. This message indicates that the referral was rejected. No
+// link to the referred node has been established by the broker, and none will
+// be provided to the referrer. This can occur for example if the referred node
+// disconnects from the broker before establishing a handshake.
+IPCZ_MSG_BEGIN(NonBrokerReferralRejected, IPCZ_MSG_ID(6), IPCZ_MSG_VERSION(0))
+  // A unique identifier for the referral in question, as provided by the
+  // original ReferNonBroker message sent by the receipient of this message.
+  IPCZ_MSG_PARAM(uint64_t, referral_id)
+IPCZ_MSG_END()
+
 // Sent by a non-broker node to a broker node, asking the broker to introduce
 // the non-broker to the node identified by `name`. If the broker is willing and
 // able to comply with this request, it will send an AcceptIntroduction message
diff --git a/src/test/multinode_test.cc b/src/test/multinode_test.cc
index dd9345c..6d92c89 100644
--- a/src/test/multinode_test.cc
+++ b/src/test/multinode_test.cc
@@ -10,12 +10,12 @@
 
 #include "ipcz/ipcz.h"
 #include "reference_drivers/async_reference_driver.h"
+#include "reference_drivers/blob.h"
 #include "reference_drivers/sync_reference_driver.h"
 #include "third_party/abseil-cpp/absl/base/macros.h"
 #include "third_party/abseil-cpp/absl/types/optional.h"
 #include "third_party/abseil-cpp/absl/types/variant.h"
 #include "third_party/ipcz/src/test_buildflags.h"
-#include "util/log.h"
 
 #if BUILDFLAG(ENABLE_IPCZ_MULTIPROCESS_TESTS)
 #include "reference_drivers/file_descriptor.h"
@@ -150,8 +150,8 @@
   ABSL_ASSERT(result == IPCZ_RESULT_OK);
 }
 
-void TestNode::ConnectToBroker(absl::Span<IpczHandle> portals) {
-  uint32_t flags = IPCZ_CONNECT_NODE_TO_BROKER;
+void TestNode::ConnectToParent(absl::Span<IpczHandle> portals,
+                               IpczConnectNodeFlags flags) {
   if (driver_mode_ == DriverMode::kAsyncDelegatedAlloc ||
       driver_mode_ == DriverMode::kAsyncObjectBrokeringAndDelegatedAlloc) {
     flags |= IPCZ_CONNECT_NODE_TO_ALLOCATION_DELEGATE;
@@ -164,16 +164,43 @@
   ASSERT_EQ(IPCZ_RESULT_OK, result);
 }
 
-IpczHandle TestNode::ConnectToBroker() {
+void TestNode::ConnectToBroker(absl::Span<IpczHandle> portals) {
+  ConnectToParent(portals, IPCZ_CONNECT_NODE_TO_BROKER);
+}
+
+IpczHandle TestNode::ConnectToParent(IpczConnectNodeFlags flags) {
   IpczHandle portal;
-  ConnectToBroker({&portal, 1});
+  ConnectToParent({&portal, 1}, flags);
   return portal;
 }
 
+IpczHandle TestNode::ConnectToBroker() {
+  return ConnectToParent(IPCZ_CONNECT_NODE_TO_BROKER);
+}
+
 std::pair<IpczHandle, IpczHandle> TestNode::OpenPortals() {
   return TestBase::OpenPortals(node_);
 }
 
+IpczHandle TestNode::BoxBlob(std::string_view contents) {
+  auto blob = MakeRefCounted<reference_drivers::Blob>(contents);
+  IpczHandle box;
+  const IpczResult result = ipcz().Box(
+      node_, reference_drivers::Blob::ReleaseAsHandle(std::move(blob)),
+      IPCZ_NO_FLAGS, nullptr, &box);
+  ABSL_ASSERT(result == IPCZ_RESULT_OK);
+  return box;
+}
+
+// Extracts the string contents of a boxed test driver blob.
+std::string TestNode::UnboxBlob(IpczHandle box) {
+  IpczDriverHandle handle;
+  const IpczResult result = ipcz().Unbox(box, IPCZ_NO_FLAGS, nullptr, &handle);
+  ABSL_ASSERT(result == IPCZ_RESULT_OK);
+  auto blob = reference_drivers::Blob::TakeFromHandle(handle);
+  return blob->message();
+}
+
 void TestNode::CloseThisNode() {
   if (node_ != IPCZ_INVALID_HANDLE) {
     IpczHandle node = std::exchange(node_, IPCZ_INVALID_HANDLE);
@@ -184,15 +211,17 @@
 Ref<TestNode::TestNodeController> TestNode::SpawnTestNodeImpl(
     IpczHandle from_node,
     const internal::TestNodeDetails& details,
-    PortalsOrTransport portals_or_transport) {
+    PortalsOrTransport portals_or_transport,
+    IpczConnectNodeFlags flags) {
   struct Connect {
-    explicit Connect(TestNode& test) : test(test) {}
+    Connect(TestNode& test, IpczConnectNodeFlags flags)
+        : test(test), flags(flags) {}
 
     IpczDriverHandle operator()(absl::Span<IpczHandle> portals) {
       TransportPair transports = test.CreateTransports();
       const IpczResult result =
           test.ipcz().ConnectNode(test.node(), transports.ours, portals.size(),
-                                  IPCZ_NO_FLAGS, nullptr, portals.data());
+                                  flags, nullptr, portals.data());
       ABSL_ASSERT(result == IPCZ_RESULT_OK);
       return transports.theirs;
     }
@@ -202,9 +231,10 @@
     }
 
     TestNode& test;
+    const IpczConnectNodeFlags flags;
   };
 
-  Connect connect(*this);
+  Connect connect(*this, flags);
   IpczDriverHandle their_transport = absl::visit(connect, portals_or_transport);
 
   Ref<TestNodeController> controller;
diff --git a/src/test/multinode_test.h b/src/test/multinode_test.h
index 75e3f4e..eb7f437 100644
--- a/src/test/multinode_test.h
+++ b/src/test/multinode_test.h
@@ -157,29 +157,45 @@
   void Initialize(DriverMode driver_mode,
                   IpczCreateNodeFlags create_node_flags);
 
-  // May be called at most once by the TestNode body, to connect initial
-  // `portals` to the broker.
+  // May be called at most once by the TestNode body to connect initial
+  // `portals` to the node that spawned this one. Extra `flags` may be passed to
+  // the corresponding ConnectNode() call.
+  void ConnectToParent(absl::Span<IpczHandle> portals,
+                       IpczConnectNodeFlags flags = IPCZ_NO_FLAGS);
+
+  // May be called instead of ConnectToParent() when the portal that spawned
+  // this one is a broker.
   void ConnectToBroker(absl::Span<IpczHandle> portals);
 
   // Shorthand for the above, for the common case with only one initial portal.
+  IpczHandle ConnectToParent(IpczConnectNodeFlags flags = IPCZ_NO_FLAGS);
   IpczHandle ConnectToBroker();
 
   // Opens a new portal pair on this node.
   std::pair<IpczHandle, IpczHandle> OpenPortals();
 
+  // Creates a new test driver blob object and boxes it. Returns a handle to the
+  // box.
+  IpczHandle BoxBlob(std::string_view contents);
+
+  // Extracts the string contents of a boxed test driver blob.
+  std::string UnboxBlob(IpczHandle box);
+
   // Spawns a new test node of TestNodeType and populates `portals` with a set
   // of initial portals connected to the node, via a new transport.
   template <typename TestNodeType>
-  Ref<TestNodeController> SpawnTestNode(absl::Span<IpczHandle> portals) {
-    return SpawnTestNodeImpl(node_, TestNodeType::kDetails, portals);
+  Ref<TestNodeController> SpawnTestNode(
+      absl::Span<IpczHandle> portals,
+      IpczConnectNodeFlags flags = IPCZ_NO_FLAGS) {
+    return SpawnTestNodeImpl(node_, TestNodeType::kDetails, portals, flags);
   }
 
   // Shorthand for the above, for the common case with only one initial portal
   // and no need for the test body to retain a controller for the node.
   template <typename TestNodeType>
-  IpczHandle SpawnTestNode() {
+  IpczHandle SpawnTestNode(IpczConnectNodeFlags flags = IPCZ_NO_FLAGS) {
     IpczHandle portal;
-    SpawnTestNode<TestNodeType>({&portal, 1});
+    SpawnTestNode<TestNodeType>({&portal, 1}, flags);
     return portal;
   }
 
@@ -187,8 +203,10 @@
   // its broker connection. The caller is resposible for the other end of that
   // connection.
   template <typename TestNodeType>
-  Ref<TestNodeController> SpawnTestNode(IpczDriverHandle transport) {
-    return SpawnTestNodeImpl(node_, TestNodeType::kDetails, transport);
+  Ref<TestNodeController> SpawnTestNodeWithTransport(
+      IpczDriverHandle transport,
+      IpczConnectNodeFlags flags = IPCZ_NO_FLAGS) {
+    return SpawnTestNodeImpl(node_, TestNodeType::kDetails, transport, flags);
   }
 
   // Forcibly closes this Node, severing all links to other nodes and implicitly
@@ -238,7 +256,8 @@
   Ref<TestNodeController> SpawnTestNodeImpl(
       IpczHandle from_node,
       const internal::TestNodeDetails& details,
-      PortalsOrTransport portals_or_transport);
+      PortalsOrTransport portals_or_transport,
+      IpczConnectNodeFlags flags);
 
   DriverMode driver_mode_ = DriverMode::kSync;
   IpczHandle node_ = IPCZ_INVALID_HANDLE;