feat: firewalled peer connections via server-brokered connect

Implement the FIREWALLED connection mode: when a direct outbound connection to
a peer fails (peer unreachable), fall back to asking the server to broker it.

- Add MessageFactory::build_connect_to_peer (server code 18).
- A pre-established outbound failure now emits ClientOperation::PeerConnectFailed
  instead of a normal disconnect; the client registers a correlation token and
  sends ConnectToPeer so the peer connects back.
- The listener accepts the peer's PierceFirewall (peer code 0) connect-back,
  matches the pending token, registers it as the P control connection, and
  fires PeerConnected to flush the queued download.

Fix a latent aliasing bug this surfaced: a peer actor could emit two terminal
notifications (a connect-failure, then a disconnect when later replaced and
stopped), and the second removed the *newer* actor registered under the same
username, killing the brokered connection mid-transfer. An actor now reports a
terminal outcome at most once (disconnect_reported guard).

Verified end to end against soulfind: the mock advertises an unlistened port so
the direct connect is refused, the server brokers the connection, the mock
pierces back, and the file transfers — bytes match. A companion test
(soulfind_brokers_connect_to_peer_between_users) pins the server behaviour the
mode relies on.
This commit is contained in:
michel
2026-07-19 17:26:52 +02:00
parent e4093b1ea8
commit b82b03a143
5 changed files with 427 additions and 5 deletions
+33 -3
View File
@@ -53,6 +53,14 @@ pub struct PeerActor {
/// True when we initiated this connection (no stream supplied at
/// construction), so we must send a `PeerInit` once connected.
outbound: bool,
/// Set once the connection is up. A pre-established failure on an outbound
/// connection means the peer is unreachable (likely firewalled).
established: bool,
/// Set once this actor has reported a terminal outcome (disconnect or
/// connect-failure). Guards against a second notification — e.g. when a
/// replaced actor is later stopped — which would otherwise alias and evict
/// a newer actor registered under the same username.
disconnect_reported: bool,
}
impl PeerActor {
@@ -83,6 +91,8 @@ impl PeerActor {
queued_messages: Vec::new(),
own_username,
outbound,
established: false,
disconnect_reported: false,
}
}
@@ -429,9 +439,22 @@ impl PeerActor {
self.stream.take();
if let Err(e) = self.client_channel.send(
ClientOperation::PeerDisconnected(username, Some(error.into())),
) {
if self.disconnect_reported {
return;
}
self.disconnect_reported = true;
// A direct outbound connection that never established means the peer is
// unreachable (likely firewalled): signal a connect failure so the
// client can fall back to server-brokered connect. Anything else is a
// normal disconnect.
let op = if self.outbound && !self.established {
ClientOperation::PeerConnectFailed(username)
} else {
ClientOperation::PeerDisconnected(username, Some(error.into()))
};
if let Err(e) = self.client_channel.send(op) {
error!("Failed to send disconnect notification: {}", e);
}
}
@@ -441,6 +464,11 @@ impl PeerActor {
self.stream.take();
if self.disconnect_reported {
return;
}
self.disconnect_reported = true;
if let Err(e) = self
.client_channel
.send(ClientOperation::PeerDisconnected(username, None))
@@ -552,6 +580,8 @@ impl PeerActor {
}
};
self.established = true;
let Some(ref mut stream) = self.stream else {
return;
};