Skip to main content

netget/client/torrent_peer/
mod.rs

1//! BitTorrent Peer Wire Protocol client implementation
2pub mod actions;
3
4pub use actions::TorrentPeerClientProtocol;
5
6use anyhow::{bail, Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::net::TcpStream;
11use tokio::sync::{mpsc, Mutex};
12use tracing::{error, info, trace};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::torrent_peer::actions::{PEER_HANDSHAKE_EVENT, PEER_MESSAGE_EVENT};
16use crate::llm::ollama_client::OllamaClient;
17use crate::llm::ClientLlmResult;
18use crate::protocol::Event;
19use crate::state::app_state::AppState;
20use crate::state::{ClientId, ClientStatus};
21
22/// Maximum inbound peer message, including its message ID. Normal block messages
23/// are much smaller; this also leaves room for large bitfields and extensions.
24pub const MAX_PEER_MESSAGE_BYTES: usize = 8 * 1024 * 1024;
25
26/// Read one length-prefixed peer message without trusting its allocation size.
27/// An empty message is the protocol's keepalive. Do not cancel this read midway.
28pub async fn read_peer_message<R: AsyncReadExt + Unpin>(reader: &mut R) -> Result<Vec<u8>> {
29    read_peer_message_with_timeout(reader, crate::client::response_reader::RESPONSE_DEADLINE).await
30}
31
32pub async fn read_peer_message_with_timeout<R: AsyncReadExt + Unpin>(
33    reader: &mut R,
34    deadline: std::time::Duration,
35) -> Result<Vec<u8>> {
36    let first = reader.read_u8().await?;
37    tokio::time::timeout(deadline, async {
38        let mut length = [first, 0, 0, 0];
39        reader.read_exact(&mut length[1..]).await?;
40        let len = u32::from_be_bytes(length) as usize;
41        if len > MAX_PEER_MESSAGE_BYTES {
42            bail!("peer message length {len} exceeds the {MAX_PEER_MESSAGE_BYTES}-byte cap");
43        }
44        let mut message = vec![0; len];
45        reader.read_exact(&mut message).await?;
46        Ok(message)
47    })
48    .await
49    .map_err(|_| anyhow::anyhow!("peer message deadline exceeded"))?
50}
51
52/// Peer wire message types
53#[repr(u8)]
54#[allow(dead_code)]
55enum MessageType {
56    Choke = 0,
57    Unchoke = 1,
58    Interested = 2,
59    NotInterested = 3,
60    Have = 4,
61    Bitfield = 5,
62    Request = 6,
63    Piece = 7,
64    Cancel = 8,
65    Port = 9,
66}
67
68/// BitTorrent Peer Wire Protocol client
69pub struct TorrentPeerClient;
70
71impl TorrentPeerClient {
72    /// Connect to a BitTorrent peer with integrated LLM actions
73    pub async fn connect_with_llm_actions(
74        remote_addr: String,
75        llm_client: OllamaClient,
76        app_state: Arc<AppState>,
77        status_tx: mpsc::UnboundedSender<String>,
78        client_id: ClientId,
79    ) -> Result<SocketAddr> {
80        // Connect to peer
81        let stream = TcpStream::connect(&remote_addr)
82            .await
83            .context(format!("Failed to connect to peer at {}", remote_addr))?;
84
85        let local_addr = stream.local_addr()?;
86        let remote_sock_addr = stream.peer_addr()?;
87
88        info!(
89            "BitTorrent Peer client {} connected to {} (local: {})",
90            client_id, remote_sock_addr, local_addr
91        );
92
93        // Update client state
94        app_state
95            .update_client_status(client_id, ClientStatus::Connected)
96            .await;
97        let _ = status_tx.send(format!(
98            "[CLIENT] BitTorrent Peer client {} connected",
99            client_id
100        ));
101        let _ = status_tx.send("__UPDATE_UI__".to_string());
102
103        // Split stream
104        let (mut read_half, write_half) = tokio::io::split(stream);
105        let write_half_arc = Arc::new(Mutex::new(write_half));
106
107        // Command channel for the dashboard's [ send_* ] / [ disconnect ] rows. Registered
108        // BEFORE the connected-event LLM call (which a manual `*` rule can park for minutes),
109        // so an injected send works for the whole park. The read loop uses `read_exact`,
110        // which is NOT cancellation-safe, so the channel is drained by its own task sharing
111        // the write half rather than by a `select!` arm.
112        let command_rx =
113            crate::client::command_support::register_command_channel(&app_state, client_id).await;
114        let cmd_state = app_state.clone();
115        let cmd_tx = status_tx.clone();
116        let cmd_write = write_half_arc.clone();
117        let cmd_protocol = Arc::new(TorrentPeerClientProtocol::new());
118        let cmd_task = tokio::spawn(async move {
119            Self::command_loop(
120                command_rx,
121                cmd_protocol,
122                cmd_write,
123                client_id,
124                cmd_state,
125                cmd_tx,
126            )
127            .await;
128        });
129        app_state.register_client_task(client_id, cmd_task).await;
130
131        // Spawn read loop for peer messages
132        let app_state_clone = app_state.clone();
133        let status_tx_clone = status_tx.clone();
134        let write_half_clone = write_half_arc.clone();
135        let llm_client_clone = llm_client.clone();
136        // Registered with AppState so stop_client can abort this task —
137        // dropping a JoinHandle only detaches it in Tokio.
138        let task_registrar = app_state.clone();
139        let task_handle = tokio::spawn(async move {
140            // First, wait for handshake
141            let mut handshake_buf = vec![0u8; 68]; // 1 + 19 + 8 + 20 + 20
142            let handshake_result = tokio::time::timeout(
143                crate::client::response_reader::RESPONSE_DEADLINE,
144                read_half.read_exact(&mut handshake_buf),
145            )
146            .await
147            .unwrap_or_else(|_| {
148                Err(std::io::Error::new(
149                    std::io::ErrorKind::TimedOut,
150                    "peer handshake deadline exceeded",
151                ))
152            });
153            match handshake_result {
154                Ok(_) => {
155                    trace!("Peer client {} received handshake", client_id);
156
157                    // Parse handshake (simplified)
158                    let pstrlen = handshake_buf[0];
159                    if pstrlen == 19 && &handshake_buf[1..20] == b"BitTorrent protocol" {
160                        let reserved = &handshake_buf[20..28];
161                        let info_hash = &handshake_buf[28..48];
162                        let peer_id = &handshake_buf[48..68];
163
164                        trace!(
165                            "Handshake: info_hash={:?}, peer_id={:?}",
166                            hex::encode(info_hash),
167                            hex::encode(peer_id)
168                        );
169
170                        // Call LLM with handshake event
171                        if let Some(instruction) =
172                            app_state_clone.get_instruction_for_client(client_id).await
173                        {
174                            let protocol = Arc::new(crate::client::torrent_peer::actions::TorrentPeerClientProtocol::new());
175                            let event = Event::new(
176                                &PEER_HANDSHAKE_EVENT,
177                                serde_json::json!({
178                                    "info_hash": hex::encode(info_hash),
179                                    "peer_id": hex::encode(peer_id),
180                                    "reserved": hex::encode(reserved),
181                                }),
182                            );
183
184                            let memory = app_state_clone
185                                .get_memory_for_client(client_id)
186                                .await
187                                .unwrap_or_default();
188
189                            match call_llm_for_client(
190                                &llm_client_clone,
191                                &app_state_clone,
192                                client_id.to_string(),
193                                &instruction,
194                                &memory,
195                                Some(&event),
196                                protocol.as_ref(),
197                                &status_tx_clone,
198                            )
199                            .await
200                            {
201                                Ok(ClientLlmResult {
202                                    actions,
203                                    memory_updates,
204                                }) => {
205                                    if let Some(mem) = memory_updates {
206                                        app_state_clone.set_memory_for_client(client_id, mem).await;
207                                    }
208
209                                    for action in actions {
210                                        if let Err(e) = Self::execute_peer_action(
211                                            client_id,
212                                            action,
213                                            &write_half_clone,
214                                            protocol.as_ref(),
215                                        )
216                                        .await
217                                        {
218                                            error!("Failed to execute peer action: {}", e);
219                                        }
220                                    }
221                                }
222                                Err(e) => {
223                                    error!("LLM error for Peer client {}: {}", client_id, e);
224                                }
225                            }
226                        }
227                    }
228
229                    // Now read peer wire messages. Refuse an oversized announcement
230                    // before allocating or waiting for any of its body bytes.
231                    loop {
232                        match read_peer_message(&mut read_half).await {
233                            Ok(msg_buf) => {
234                                if msg_buf.is_empty() {
235                                    trace!("Peer client {} received keep-alive", client_id);
236                                    continue;
237                                }
238                                let msg_type = msg_buf[0];
239                                let payload = &msg_buf[1..];
240
241                                trace!(
242                                    "Peer client {} received message type {}, len {}",
243                                    client_id,
244                                    msg_type,
245                                    payload.len()
246                                );
247
248                                // Call LLM with message event
249                                if let Some(instruction) =
250                                    app_state_clone.get_instruction_for_client(client_id).await
251                                {
252                                    let protocol = Arc::new(crate::client::torrent_peer::actions::TorrentPeerClientProtocol::new());
253                                    let event = Event::new(
254                                        &PEER_MESSAGE_EVENT,
255                                        serde_json::json!({
256                                            "message_type": msg_type,
257                                            "payload_len": payload.len(),
258                                            "payload_hex": hex::encode(payload),
259                                        }),
260                                    );
261
262                                    let memory = app_state_clone
263                                        .get_memory_for_client(client_id)
264                                        .await
265                                        .unwrap_or_default();
266
267                                    match call_llm_for_client(
268                                        &llm_client_clone,
269                                        &app_state_clone,
270                                        client_id.to_string(),
271                                        &instruction,
272                                        &memory,
273                                        Some(&event),
274                                        protocol.as_ref(),
275                                        &status_tx_clone,
276                                    )
277                                    .await
278                                    {
279                                        Ok(ClientLlmResult {
280                                            actions,
281                                            memory_updates,
282                                        }) => {
283                                            if let Some(mem) = memory_updates {
284                                                app_state_clone
285                                                    .set_memory_for_client(client_id, mem)
286                                                    .await;
287                                            }
288
289                                            for action in actions {
290                                                if let Err(e) = Self::execute_peer_action(
291                                                    client_id,
292                                                    action,
293                                                    &write_half_clone,
294                                                    protocol.as_ref(),
295                                                )
296                                                .await
297                                                {
298                                                    error!("Failed to execute peer action: {}", e);
299                                                }
300                                            }
301                                        }
302                                        Err(e) => {
303                                            error!("LLM error: {}", e);
304                                        }
305                                    }
306                                }
307                            }
308                            Err(e) => {
309                                error!("Peer client {} message read error: {}", client_id, e);
310                                break;
311                            }
312                        }
313                    }
314                }
315                Err(e) => {
316                    error!("Peer client {} handshake read error: {}", client_id, e);
317                }
318            }
319
320            // Every read-loop exit (handshake error, message/length read error, EOF, an
321            // injected or LLM disconnect) lands here: drop the command handle so the rail
322            // stops offering [ send ] on a dead client and a late send fails fast.
323            app_state_clone.remove_client_handle(client_id).await;
324            app_state_clone
325                .update_client_status(client_id, ClientStatus::Disconnected)
326                .await;
327            let _ = status_tx_clone.send(format!(
328                "[CLIENT] BitTorrent Peer client {} disconnected",
329                client_id
330            ));
331            let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
332        });
333        task_registrar
334            .register_client_task(client_id, task_handle)
335            .await;
336
337        // Call LLM with connected event
338        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
339            let protocol =
340                Arc::new(crate::client::torrent_peer::actions::TorrentPeerClientProtocol::new());
341            let event = Event::new(
342                &PEER_HANDSHAKE_EVENT,
343                serde_json::json!({
344                    "status": "connected",
345                    "remote_addr": remote_sock_addr.to_string(),
346                }),
347            );
348
349            let memory = app_state
350                .get_memory_for_client(client_id)
351                .await
352                .unwrap_or_default();
353            let write_half_clone = write_half_arc.clone();
354
355            // Registered with AppState so stop_client can abort this task —
356            // dropping a JoinHandle only detaches it in Tokio.
357            let task_registrar = app_state.clone();
358            let task_handle = tokio::spawn(async move {
359                match call_llm_for_client(
360                    &llm_client,
361                    &app_state,
362                    client_id.to_string(),
363                    &instruction,
364                    &memory,
365                    Some(&event),
366                    protocol.as_ref(),
367                    &status_tx,
368                )
369                .await
370                {
371                    Ok(ClientLlmResult {
372                        actions,
373                        memory_updates,
374                    }) => {
375                        if let Some(mem) = memory_updates {
376                            app_state.set_memory_for_client(client_id, mem).await;
377                        }
378
379                        for action in actions {
380                            if let Err(e) = Self::execute_peer_action(
381                                client_id,
382                                action,
383                                &write_half_clone,
384                                protocol.as_ref(),
385                            )
386                            .await
387                            {
388                                error!("Failed to execute peer action: {}", e);
389                            }
390                        }
391                    }
392                    Err(e) => {
393                        error!("LLM error: {}", e);
394                    }
395                }
396            });
397            task_registrar
398                .register_client_task(client_id, task_handle)
399                .await;
400        }
401
402        Ok(local_addr)
403    }
404
405    /// Execute a peer action, putting its bytes on the wire. This is the single encoder for
406    /// the peer-wire Custom results, shared by the LLM path and the injected command loop.
407    async fn execute_peer_action(
408        client_id: ClientId,
409        action: serde_json::Value,
410        write_half: &Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
411        protocol: &dyn crate::llm::actions::client_trait::Client,
412    ) -> Result<PeerApplied> {
413        use crate::llm::actions::client_trait::ClientActionResult;
414
415        match protocol.execute_action(action)? {
416            ClientActionResult::Custom { name, data } if name == "peer_handshake" => {
417                let info_hash_hex = data
418                    .get("info_hash")
419                    .and_then(|v| v.as_str())
420                    .context("Missing info_hash")?;
421                let peer_id_hex = data
422                    .get("peer_id")
423                    .and_then(|v| v.as_str())
424                    .context("Missing peer_id")?;
425
426                let info_hash = hex::decode(info_hash_hex)?;
427                let peer_id = hex::decode(peer_id_hex)?;
428
429                // Build handshake
430                let mut handshake = Vec::new();
431                handshake.push(19); // pstrlen
432                handshake.extend_from_slice(b"BitTorrent protocol"); // pstr
433                handshake.extend_from_slice(&[0u8; 8]); // reserved
434                handshake.extend_from_slice(&info_hash); // info_hash
435                handshake.extend_from_slice(&peer_id); // peer_id
436
437                write_half.lock().await.write_all(&handshake).await?;
438                trace!("Peer client {} sent handshake", client_id);
439                Ok(PeerApplied::Sent(handshake.len()))
440            }
441            ClientActionResult::Custom { name, data } if name == "peer_message" => {
442                let msg_type = data
443                    .get("message_type")
444                    .and_then(|v| v.as_u64())
445                    .context("Missing message_type")?;
446                let msg_type = u8::try_from(msg_type).context("peer message_type exceeds u8")?;
447                let payload_hex = data.get("payload").and_then(|v| v.as_str()).unwrap_or("");
448                let payload = if !payload_hex.is_empty() {
449                    hex::decode(payload_hex)?
450                } else {
451                    vec![]
452                };
453
454                // Build message
455                if payload.len() >= MAX_PEER_MESSAGE_BYTES {
456                    anyhow::bail!("peer message exceeds byte cap");
457                }
458                let msg_len =
459                    u32::try_from(1 + payload.len()).context("peer message length exceeds u32")?;
460                let mut message = Vec::new();
461                message.extend_from_slice(&msg_len.to_be_bytes());
462                message.push(msg_type);
463                message.extend_from_slice(&payload);
464
465                write_half.lock().await.write_all(&message).await?;
466                trace!("Peer client {} sent message type {}", client_id, msg_type);
467                Ok(PeerApplied::Sent(message.len()))
468            }
469            ClientActionResult::Disconnect => {
470                info!("Peer client {} disconnecting", client_id);
471                Ok(PeerApplied::Disconnect)
472            }
473            _ => Ok(PeerApplied::Nothing),
474        }
475    }
476
477    /// Drain injected commands until the channel closes (client removed) or an injected
478    /// `disconnect` ends the session.
479    ///
480    /// The generic `command_support::handle_stream_client_command` cannot run this client's
481    /// vocabulary because the wire verbs yield `ClientActionResult::Custom`, so each action
482    /// goes through [`Self::execute_peer_action`] - the same encoder the LLM path uses - and
483    /// the outcome is recorded and replied exactly the way the generic arm does it.
484    async fn command_loop(
485        mut command_rx: tokio::sync::mpsc::Receiver<crate::state::client_handles::ClientCommand>,
486        protocol: Arc<TorrentPeerClientProtocol>,
487        write_half: Arc<Mutex<tokio::io::WriteHalf<TcpStream>>>,
488        client_id: ClientId,
489        app_state: Arc<AppState>,
490        status_tx: mpsc::UnboundedSender<String>,
491    ) {
492        use crate::llm::actions::protocol_trait::Protocol;
493        use crate::state::client_handles::ClientSendOutcome;
494        use crate::state::AccessLogOwner;
495
496        while let Some(command) = command_rx.recv().await {
497            let action = command.action.clone();
498            let outcome: anyhow::Result<ClientSendOutcome> = match Self::execute_peer_action(
499                client_id,
500                action.clone(),
501                &write_half,
502                protocol.as_ref(),
503            )
504            .await
505            {
506                Err(e) => Ok(ClientSendOutcome::Rejected {
507                    error: e.to_string(),
508                }),
509                Ok(PeerApplied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
510                Ok(PeerApplied::Sent(0)) | Ok(PeerApplied::Nothing) => {
511                    Ok(ClientSendOutcome::Executed {
512                        detail: "executed (nothing to write)".to_string(),
513                    })
514                }
515                Ok(PeerApplied::Sent(bytes_sent)) => Ok(ClientSendOutcome::Sent { bytes_sent }),
516            };
517
518            let outcome_json = match &outcome {
519                Ok(o) => serde_json::to_value(o).unwrap_or(serde_json::Value::Null),
520                Err(e) => serde_json::json!({"error": e.to_string()}),
521            };
522            app_state
523                .record_access_log(
524                    AccessLogOwner::Client(client_id.as_u32()),
525                    protocol.protocol_name(),
526                    None,
527                    "injected_action",
528                    action,
529                    vec![outcome_json],
530                )
531                .await;
532
533            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
534            if let Err(e) = &outcome {
535                error!("Peer client {} injected action failed: {}", client_id, e);
536                let _ = status_tx.send(format!(
537                    "[WARN] Client {} injected action failed: {}",
538                    client_id, e
539                ));
540            }
541            let _ = status_tx.send("__UPDATE_UI__".to_string());
542            crate::client::command_support::reply(command, outcome);
543
544            if disconnect {
545                // BitTorrent has no graceful-close message; half-close so the peer reads EOF
546                // and the read loop runs its normal disconnect path (which drops the handle).
547                let _ = write_half.lock().await.shutdown().await;
548                break;
549            }
550        }
551    }
552}
553
554/// Result of putting one executed peer action on the wire.
555enum PeerApplied {
556    /// `n` bytes written to the peer.
557    Sent(usize),
558    /// The action requested disconnect (nothing written; the caller half-closes).
559    Disconnect,
560    /// Nothing to do (an action variant this client does not turn into wire bytes).
561    Nothing,
562}