feat: direct peer connections — download() connects to unconnected peers

Implement the DIRECT connection-initiation mode so a download can target a
peer the client is not already connected to:

- Fix the outbound peer handshake. on_connection_established sent
  build_watch_user() (a server WatchUser message, wrongly framed with
  get_data()) for outbound connections; it now sends a proper length-prefixed
  PeerInit (peer code 1) with our own username. own_username is plumbed in
  via PeerRegistry -> PeerActor; inbound peers (outbound=false) are unchanged.
- download() now auto-connects: if there is no control connection to the peer
  it sends GetPeerAddress and defers the QueueUpload until the outbound stream
  handshakes, signalled by a new ClientOperation::PeerConnected. A never-
  connected client still resolves the download to Failed (unit test).
- Add Client::connect_peer(username) to open/pre-warm a control connection.

Verified end to end against soulfind: a_file_downloads_from_a_peer_via_direct_connection
logs a mock peer into the server, advertises its port via SetWaitPort, and the
client resolves the address, dials it, and downloads the file — bytes match.
The F-serve leg is shared with the passive-download mock.
This commit is contained in:
michel
2026-07-19 17:12:17 +02:00
parent 06cd638736
commit e4093b1ea8
5 changed files with 387 additions and 29 deletions
+34 -6
View File
@@ -49,6 +49,10 @@ pub struct PeerActor {
dispatcher: Option<MessageDispatcher<PeerMessage>>,
dispatcher_receiver: Option<Receiver<PeerMessage>>,
queued_messages: Vec<PeerMessage>,
own_username: String,
/// True when we initiated this connection (no stream supplied at
/// construction), so we must send a `PeerInit` once connected.
outbound: bool,
}
impl PeerActor {
@@ -58,7 +62,9 @@ impl PeerActor {
stream: Option<TcpStream>,
reader: Option<MessageReader>,
client_channel: Sender<ClientOperation>,
own_username: String,
) -> Self {
let outbound = stream.is_none();
let connection_state = if stream.is_some() {
ConnectionState::Connected
} else {
@@ -75,6 +81,8 @@ impl PeerActor {
dispatcher: None,
dispatcher_receiver: None,
queued_messages: Vec::new(),
own_username,
outbound,
}
}
@@ -529,8 +537,12 @@ impl PeerActor {
}
fn on_connection_established(&mut self) {
let (username, token) = match self.peer.read_safe() {
Ok(peer) => (peer.username.clone(), peer.token),
let (username, connection_type, token) = match self.peer.read_safe() {
Ok(peer) => (
peer.username.clone(),
peer.connection_type.clone(),
peer.token,
),
Err(e) => {
error!(
"[peer_actor] on_connection_established peer lock: {}",
@@ -544,11 +556,19 @@ impl PeerActor {
return;
};
if let Some(token) = token {
let handshake_msg = MessageFactory::build_watch_user(token);
if let Err(e) = stream.write_all(&handshake_msg.get_data()) {
// Connections we initiated must announce themselves with a PeerInit
// (peer code 1); inbound peers already sent us theirs, so we stay
// silent for them. `get_buffer` prepends the length prefix the peer's
// MessageReader expects.
if self.outbound {
let init = MessageFactory::build_peer_init_message(
&self.own_username,
connection_type,
token.unwrap_or(0),
);
if let Err(e) = stream.write_all(&init.get_buffer()) {
error!(
"[peer:{}] Failed to send watch user handshake: {}",
"[peer:{}] Failed to send PeerInit handshake: {}",
username, e
);
self.disconnect_with_error(e);
@@ -563,6 +583,14 @@ impl PeerActor {
self.handle_message(msg);
}
// Tell the client the control connection is live so any downloads
// queued for this peer can now be requested over a handshaken stream.
if self.outbound {
let _ = self
.client_channel
.send(ClientOperation::PeerConnected(username));
}
if let Some(ref handle) = self.self_handle {
handle.send(PeerMessage::ProcessRead).ok();
}
+11 -2
View File
@@ -15,6 +15,7 @@ pub struct PeerRegistry {
peers: Arc<Mutex<HashMap<String, ActorHandle<PeerMessage>>>>,
actor_system: Arc<ActorSystem>,
client_channel: Sender<ClientOperation>,
own_username: String,
}
impl PeerRegistry {
@@ -22,11 +23,13 @@ impl PeerRegistry {
pub fn new(
actor_system: Arc<ActorSystem>,
client_channel: Sender<ClientOperation>,
own_username: String,
) -> Self {
Self {
peers: Arc::new(Mutex::new(HashMap::new())),
actor_system,
client_channel,
own_username,
}
}
@@ -38,8 +41,13 @@ impl PeerRegistry {
) -> Result<ActorHandle<PeerMessage>, String> {
let username = peer.username.clone();
let actor =
PeerActor::new(peer, stream, reader, self.client_channel.clone());
let actor = PeerActor::new(
peer,
stream,
reader,
self.client_channel.clone(),
self.own_username.clone(),
);
let handle =
self.actor_system.spawn_with_handle(actor, |actor, handle| {
@@ -157,6 +165,7 @@ impl Clone for PeerRegistry {
peers: self.peers.clone(),
actor_system: self.actor_system.clone(),
client_channel: self.client_channel.clone(),
own_username: self.own_username.clone(),
}
}
}