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
+61 -1
View File
@@ -18,7 +18,7 @@ use std::{
net::TcpStream,
sync::{
RwLock,
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicU32, Ordering},
mpsc::{Receiver, Sender},
},
thread::{self, sleep},
@@ -31,6 +31,13 @@ use std::{
use crate::{debug, error, info, trace, warn};
const DEFAULT_LISTEN_PORT: u16 = 2234;
/// Source of non-zero correlation tokens for server-brokered connections.
static NEXT_CONNECT_TOKEN: AtomicU32 = AtomicU32::new(1);
fn next_connect_token() -> u32 {
NEXT_CONNECT_TOKEN.fetch_add(1, Ordering::Relaxed).max(1)
}
#[derive(Debug, Clone)]
pub struct ClientSettings {
pub username: String,
@@ -98,6 +105,10 @@ pub enum ClientOperation {
SetServerSender(Sender<ServerMessage>),
PrivateMessageReceived(UserMessage),
PeerConnected(String),
/// A direct outbound connection to this peer failed before it was
/// established — the peer is likely firewalled, so fall back to asking the
/// server to broker the connection.
PeerConnectFailed(String),
}
pub struct ClientContext {
pub peer_registry: Option<PeerRegistry>,
@@ -106,6 +117,9 @@ pub struct ClientContext {
server_sender: Option<Sender<ServerMessage>>,
searches: HashMap<String, Search>,
private_messages: Vec<UserMessage>,
/// Correlation tokens for server-brokered (firewalled) connections, mapping
/// a token we sent in a ConnectToPeer to the peer we expect back.
pending_connect_tokens: HashMap<u32, String>,
actor_system: Arc<ActorSystem>,
}
impl Default for ClientContext {
@@ -333,11 +347,23 @@ impl ClientContext {
server_sender: None,
searches: HashMap::new(),
private_messages: Vec::new(),
pending_connect_tokens: HashMap::new(),
downloads: DownloadStore::new(),
actor_system,
}
}
/// Remember that a server-brokered connection to `username` is pending under
/// `token`; the peer will quote it back in a PierceFirewall.
pub fn add_pending_connect(&mut self, token: u32, username: String) {
self.pending_connect_tokens.insert(token, username);
}
/// Resolve and consume the peer expected for a brokered connection `token`.
pub fn take_pending_connect(&mut self, token: u32) -> Option<String> {
self.pending_connect_tokens.remove(&token)
}
/// Record a private message received from another user.
pub fn push_private_message(&mut self, message: UserMessage) {
self.private_messages.push(message);
@@ -1277,6 +1303,40 @@ impl Client {
}
}
}
ClientOperation::PeerConnectFailed(username) => {
// Direct connect failed: ask the server to
// broker it. Register a correlation token, then
// send ConnectToPeer so the (firewalled) peer
// connects back to our listener quoting it.
let token = next_connect_token();
let server_sender = match client_context
.write_safe()
{
Ok(mut ctx) => {
ctx.add_pending_connect(
token,
username.clone(),
);
ctx.server_sender.clone()
}
Err(e) => {
error!(
"[client] PeerConnectFailed write: {}",
e
);
continue;
}
};
if let Some(sender) = server_sender {
let msg = crate::message::server::MessageFactory::build_connect_to_peer(
token,
&username,
ConnectionType::P,
);
let _ = sender
.send(ServerMessage::SendMessage(msg));
}
}
}
}
Err(e) => {