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
+57 -1
View File
@@ -16,7 +16,6 @@ const PEER_INIT_MESSAGE_CODE: u8 = 1;
#[derive(Clone)]
struct ConnectionContext {
#[allow(dead_code)]
client_sender: Sender<ClientOperation>,
client_context: Arc<RwLock<ClientContext>>,
own_username: String,
@@ -227,6 +226,54 @@ fn handle_file_connection(
}
}
/// Handle a `PierceFirewall` (peer init code 0): a peer we asked the server to
/// broker (because a direct connection failed) connecting back to us. It quotes
/// the correlation token we registered; we register the stream as its P control
/// connection and tell the client the connection is live.
fn handle_pierce_firewall(
mut message: Message,
stream: TcpStream,
reader: MessageReader,
context: &ConnectionContext,
peer_ip: &str,
peer_port: u16,
) {
message.set_pointer(5); // skip length prefix (4) + int8 code (1)
let token = message.read_int32();
let username = match context.client_context.write_safe() {
Ok(mut ctx) => ctx.take_pending_connect(token),
Err(e) => {
error!("[listener] pierce firewall lock: {}", e);
return;
}
};
let Some(username) = username else {
debug!(
"[listener:{peer_ip}:{peer_port}] PierceFirewall token {token} is not pending; ignoring"
);
return;
};
let peer = Peer::new(
username.clone(),
ConnectionType::P,
peer_ip.to_string(),
peer_port.into(),
None,
0,
0,
0,
);
handle_peer_connection(peer, stream, reader, context, peer_ip, peer_port);
// Inbound peers don't self-announce, so nudge the client to flush any
// downloads queued for this now-connected peer.
let _ = context
.client_sender
.send(ClientOperation::PeerConnected(username));
}
fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
let Ok(peer_addr) = stream.peer_addr() else {
error!("[listener] failed to get peer address");
@@ -245,6 +292,15 @@ fn handle_incoming_connection(stream: TcpStream, context: ConnectionContext) {
return;
};
// A firewalled peer brokered through the server connects back with a
// PierceFirewall (code 0) instead of a PeerInit (code 1).
if message.get_message_code() == 0 {
handle_pierce_firewall(
message, stream, reader, &context, &peer_ip, peer_port,
);
return;
}
let Some(init_data) = parse_peer_init_message(message) else {
error!(
"[listener:{peer_ip}:{peer_port}] Invalid or unknown peer init message"