Skip to main content

netget/server/torrent_dht/
mod.rs

1//! BitTorrent DHT server implementation
2//!
3//! UDP-based Kademlia DHT for distributed peer discovery (BEP 5)
4
5pub mod actions;
6
7use anyhow::Result;
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::net::UdpSocket;
11use tokio::sync::mpsc;
12use tracing::{debug, error, info, trace};
13
14use crate::llm::action_helper::call_llm;
15use crate::llm::ollama_client::OllamaClient;
16use crate::protocol::Event;
17use crate::server::connection::ConnectionId;
18use crate::state::app_state::AppState;
19use crate::utils::WireFailure;
20use crate::{console_debug, console_trace};
21use actions::TorrentDhtProtocol;
22
23/// BEP 5 error code 201 — "Generic Error".
24const KRPC_GENERIC_ERROR: i64 = 201;
25/// BEP 5 error code 202 — "Server Error". Used for the transient (overloaded) case so a
26/// querying node distinguishes "come back later" from a permanent fault on this node.
27const KRPC_SERVER_ERROR: i64 = 202;
28
29/// Bound converted values independently of the wire decoder's preflight.
30pub const MAX_DHT_VALUE_DEPTH: usize = crate::utils::bencode::MAX_BENCODE_DEPTH;
31
32/// BitTorrent DHT server
33pub struct TorrentDhtServer;
34
35impl TorrentDhtServer {
36    /// Spawn BitTorrent DHT server with LLM actions
37    pub async fn spawn_with_llm_actions(
38        listen_addr: SocketAddr,
39        llm_client: OllamaClient,
40        app_state: Arc<AppState>,
41        status_tx: mpsc::UnboundedSender<String>,
42        server_id: crate::state::ServerId,
43    ) -> Result<SocketAddr> {
44        let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
45        let local_addr = socket.local_addr()?;
46        info!(
47            "BitTorrent DHT server (action-based) listening on {}",
48            local_addr
49        );
50        let _ = status_tx.send(format!(
51            "[INFO] BitTorrent DHT server listening on {}",
52            local_addr
53        ));
54
55        let protocol = Arc::new(TorrentDhtProtocol::new());
56
57        let task_registrar = app_state.clone();
58        let accept_handle = tokio::spawn(async move {
59            let mut buffer = vec![0u8; 65535];
60
61            loop {
62                match socket.recv_from(&mut buffer).await {
63                    Ok((n, peer_addr)) => {
64                        let data = buffer[..n].to_vec();
65                        let connection_id =
66                            ConnectionId::new(app_state.get_next_unified_id().await);
67
68                        // Add connection to ServerInstance
69                        use crate::state::server::{
70                            ConnectionState as ServerConnectionState, ConnectionStatus,
71                            ProtocolConnectionInfo,
72                        };
73                        let now = crate::utils::clock::Instant::now();
74                        let conn_state = ServerConnectionState {
75                            id: connection_id,
76                            remote_addr: peer_addr,
77                            local_addr,
78                            bytes_sent: 0,
79                            bytes_received: n as u64,
80                            packets_sent: 0,
81                            packets_received: 1,
82                            last_activity: now,
83                            status: ConnectionStatus::Active,
84                            status_changed_at: now,
85                            protocol_info: ProtocolConnectionInfo::empty(),
86                        };
87                        app_state
88                            .add_connection_to_server(server_id, conn_state)
89                            .await;
90                        let _ = status_tx.send("__UPDATE_UI__".to_string());
91
92                        console_debug!(
93                            status_tx,
94                            "BitTorrent DHT received {} bytes from {}",
95                            n,
96                            peer_addr
97                        );
98
99                        // TRACE: Log full payload
100                        let hex_str = hex::encode(&data);
101                        console_trace!(status_tx, "BitTorrent DHT data (hex): {}", hex_str);
102
103                        let llm_clone = llm_client.clone();
104                        let state_clone = app_state.clone();
105                        let status_clone = status_tx.clone();
106                        let socket_clone = socket.clone();
107                        let protocol_clone = protocol.clone();
108
109                        // Tracked, not detached: stop_server must abort this task too.
110                        let task_owner = app_state.clone();
111                        task_owner.spawn_server_task(server_id, async move {
112                            // Parse bencode KRPC message
113                            match Self::parse_krpc_message(&data) {
114                                Ok((query_type, params)) => {
115                                    debug!("BitTorrent DHT query type: {}", query_type);
116                                    let _ = status_clone.send(format!(
117                                        "[DEBUG] BitTorrent DHT query type: {}",
118                                        query_type
119                                    ));
120
121                                    // Create event for LLM
122                                    let event_type = match query_type.as_str() {
123                                        "ping" => &actions::DHT_PING_QUERY_EVENT,
124                                        "find_node" => &actions::DHT_FIND_NODE_QUERY_EVENT,
125                                        "get_peers" => &actions::DHT_GET_PEERS_QUERY_EVENT,
126                                        "announce_peer" => &actions::DHT_ANNOUNCE_PEER_QUERY_EVENT,
127                                        // An unrecognised `q` still reaches the ping
128                                        // handler, whose reply shape (`{"id": ...}`) is the
129                                        // KRPC minimum. `query_type` in the event data says
130                                        // what actually arrived, so a handler can answer
131                                        // with send_dht_error_response code 204 instead.
132                                        other => {
133                                            tracing::warn!(
134                                                "BitTorrent DHT: unsupported query '{}', \
135                                                 routing to dht_ping_query",
136                                                other
137                                            );
138                                            &actions::DHT_PING_QUERY_EVENT
139                                        }
140                                    };
141                                    // Kept out of the event so a failure reply can still echo
142                                    // `t`: a KRPC reply that does not carry the querying
143                                    // node's transaction id is dropped, and the peer waits
144                                    // out its own timeout exactly as if we had said nothing.
145                                    let transaction_id_hex = params
146                                        .get("transaction_id")
147                                        .and_then(|v| v.as_str())
148                                        .map(|s| s.to_string());
149
150                                    let event = Event::new(event_type, serde_json::json!(params));
151
152                                    debug!("BitTorrent DHT calling LLM for {} query", query_type);
153                                    let _ = status_clone.send(format!(
154                                        "[DEBUG] BitTorrent DHT calling LLM for {} query",
155                                        query_type
156                                    ));
157
158                                    // Call LLM
159                                    match call_llm(
160                                        &llm_clone,
161                                        &state_clone,
162                                        server_id,
163                                        None,
164                                        &event,
165                                        protocol_clone.as_ref(),
166                                    )
167                                    .await
168                                    {
169                                        Ok(execution_result) => {
170                                            for message in &execution_result.messages {
171                                                info!("{}", message);
172                                                let _ = status_clone
173                                                    .send(format!("[INFO] {}", message));
174                                            }
175
176                                            debug!(
177                                                "BitTorrent DHT got {} protocol results",
178                                                execution_result.protocol_results.len()
179                                            );
180                                            let _ = status_clone.send(format!(
181                                                "[DEBUG] BitTorrent DHT got {} protocol results",
182                                                execution_result.protocol_results.len()
183                                            ));
184
185                                            // An explicit refusal by the model is a KRPC
186                                            // error reply it asked for itself; keep it
187                                            // distinguishable in the log from our own
188                                            // fail-closed reply, which says nothing about
189                                            // what the model thought.
190                                            let model_rejected =
191                                                execution_result.raw_actions.iter().any(|a| {
192                                                    a.get("type").and_then(|t| t.as_str())
193                                                        == Some("send_dht_error_response")
194                                                });
195
196                                            let mut sent_any = false;
197                                            for protocol_result in execution_result.protocol_results
198                                            {
199                                                if let Some(output_data) =
200                                                    protocol_result.get_all_output().first()
201                                                {
202                                                    if let Err(e) = socket_clone
203                                                        .send_to(output_data, peer_addr)
204                                                        .await
205                                                    {
206                                                        error!(
207                                                            "Failed to send DHT response: {}",
208                                                            e
209                                                        );
210                                                    } else {
211                                                        sent_any = true;
212                                                        debug!(
213                                                            "BitTorrent DHT sent {} bytes to {}",
214                                                            output_data.len(),
215                                                            peer_addr
216                                                        );
217                                                        let _ = status_clone.send(format!("[DEBUG] BitTorrent DHT sent {} bytes to {}", output_data.len(), peer_addr));
218
219                                                        let hex_str = hex::encode(output_data);
220                                                        trace!(
221                                                            "BitTorrent DHT sent (hex): {}",
222                                                            hex_str
223                                                        );
224                                                        let _ = status_clone.send(format!(
225                                                            "[TRACE] BitTorrent DHT sent (hex): {}",
226                                                            hex_str
227                                                        ));
228                                                    }
229                                                }
230                                            }
231
232                                            if sent_any {
233                                                debug!(
234                                                    "BitTorrent DHT {} from {} decision={}",
235                                                    query_type,
236                                                    peer_addr,
237                                                    if model_rejected {
238                                                        "model_reject"
239                                                    } else {
240                                                        "model_answer"
241                                                    }
242                                                );
243                                            } else {
244                                                // The model produced nothing that reaches the
245                                                // wire. Staying silent would leave the
246                                                // querying node blocked until its own
247                                                // timeout, so answer with a category.
248                                                tracing::warn!(
249                                                    "BitTorrent DHT {} from {} decision=\
250                                                     fail_closed_no_action",
251                                                    query_type,
252                                                    peer_addr
253                                                );
254                                                let _ = status_clone.send(format!(
255                                                    "[WARN] BitTorrent DHT {} from {}: no action \
256                                                     from the model, replying KRPC error",
257                                                    query_type, peer_addr
258                                                ));
259                                                Self::send_failure_reply(
260                                                    &socket_clone,
261                                                    peer_addr,
262                                                    transaction_id_hex.as_deref(),
263                                                    WireFailure::Unavailable,
264                                                    &status_clone,
265                                                )
266                                                .await;
267                                            }
268                                        }
269                                        Err(e) => {
270                                            // The error itself goes to the log and the status
271                                            // stream only — the peer gets a bare BEP 5 error
272                                            // code and a fixed category string.
273                                            let failure = WireFailure::classify(&e);
274                                            error!(
275                                                "BitTorrent DHT {} from {} \
276                                                 decision=fail_closed_llm_error \
277                                                 category={:?}: {}",
278                                                query_type, peer_addr, failure, e
279                                            );
280                                            let _ = status_clone.send(format!(
281                                                "[ERROR] BitTorrent DHT LLM call failed for {} \
282                                                 from {}: {}",
283                                                query_type, peer_addr, e
284                                            ));
285                                            Self::send_failure_reply(
286                                                &socket_clone,
287                                                peer_addr,
288                                                transaction_id_hex.as_deref(),
289                                                failure,
290                                                &status_clone,
291                                            )
292                                            .await;
293                                        }
294                                    }
295                                }
296                                Err(e) => {
297                                    error!("Failed to parse KRPC message: {}", e);
298                                    let _ = status_clone.send(format!(
299                                        "[ERROR] Failed to parse KRPC message: {}",
300                                        e
301                                    ));
302                                }
303                            }
304                        }).await;
305                    }
306                    Err(e) => {
307                        error!("BitTorrent DHT receive error: {}", e);
308                        break;
309                    }
310                }
311            }
312        });
313
314        task_registrar
315            .register_server_task(server_id, accept_handle)
316            .await;
317
318        Ok(local_addr)
319    }
320
321    /// Answer a query netget cannot serve with a BEP 5 error reply carrying only a category.
322    ///
323    /// `{"t": <echoed>, "y": "e", "e": [code, text]}`. The code separates the two categories
324    /// so a querying node can back off rather than record a permanent fault here: 202
325    /// ("Server Error") for a saturated backend, 201 ("Generic Error") for everything else.
326    /// The text comes from [`WireFailure::text`], which is `&'static str` — the backend
327    /// error, the model name and any path stay in the log.
328    async fn send_failure_reply(
329        socket: &UdpSocket,
330        peer_addr: SocketAddr,
331        transaction_id_hex: Option<&str>,
332        failure: WireFailure,
333        status_tx: &mpsc::UnboundedSender<String>,
334    ) {
335        // Without `t` there is nothing for the querying node to correlate the reply with, and
336        // BEP 5 requires it on every message; an unaddressed datagram is worse than silence.
337        let Some(transaction_id) = transaction_id_hex.and_then(|t| hex::decode(t).ok()) else {
338            tracing::warn!(
339                "BitTorrent DHT: cannot send failure reply to {} — query carried no usable \
340                 transaction id",
341                peer_addr
342            );
343            return;
344        };
345
346        let code = if failure.is_overloaded() {
347            KRPC_SERVER_ERROR
348        } else {
349            KRPC_GENERIC_ERROR
350        };
351
352        let mut response = std::collections::HashMap::new();
353        response.insert(
354            b"t".to_vec(),
355            serde_bencode::value::Value::Bytes(transaction_id),
356        );
357        response.insert(
358            b"y".to_vec(),
359            serde_bencode::value::Value::Bytes(b"e".to_vec()),
360        );
361        response.insert(
362            b"e".to_vec(),
363            serde_bencode::value::Value::List(vec![
364                serde_bencode::value::Value::Int(code),
365                serde_bencode::value::Value::Bytes(failure.text().as_bytes().to_vec()),
366            ]),
367        );
368
369        let encoded = match serde_bencode::to_bytes(&serde_bencode::value::Value::Dict(response)) {
370            Ok(bytes) => bytes,
371            Err(e) => {
372                error!("BitTorrent DHT: failed to encode KRPC error reply: {}", e);
373                return;
374            }
375        };
376
377        match socket.send_to(&encoded, peer_addr).await {
378            Ok(sent) => {
379                debug!(
380                    "BitTorrent DHT sent KRPC error {} ({} bytes) to {}",
381                    code, sent, peer_addr
382                );
383                let _ = status_tx.send(format!(
384                    "[DEBUG] BitTorrent DHT sent KRPC error {} to {}",
385                    code, peer_addr
386                ));
387            }
388            Err(e) => {
389                error!(
390                    "BitTorrent DHT: failed to send KRPC error reply to {}: {}",
391                    peer_addr, e
392                );
393            }
394        }
395    }
396
397    fn parse_krpc_message(data: &[u8]) -> Result<(String, serde_json::Value)> {
398        use serde_bencode::value::Value;
399
400        // Bound the nesting before serde_bencode sees it. `serde_bencode` recurses once per
401        // `l`/`d` with no depth counter, so a datagram of 65,000 `l` bytes — which is what
402        // fits in one UDP packet and costs an attacker one sendto() — overflows the worker
403        // thread's stack. That is a SIGSEGV, not a panic: the task this runs in cannot
404        // contain it and the whole netget process dies. Measured against 0.2.4.
405        crate::utils::bencode::check_bencode_structure(data)
406            .map_err(|e| anyhow::anyhow!("Rejected KRPC datagram: {}", e))?;
407
408        // Decode bencode
409        let value: Value = serde_bencode::from_bytes(data)?;
410
411        if let Value::Dict(dict) = value {
412            // Get message type (q = query, r = response, e = error)
413            let msg_type = dict
414                .get::<[u8]>(b"y")
415                .and_then(|v| {
416                    if let Value::Bytes(bytes) = v {
417                        String::from_utf8(bytes.clone()).ok()
418                    } else {
419                        None
420                    }
421                })
422                .ok_or_else(|| anyhow::anyhow!("Missing 'y' field"))?;
423
424            if msg_type == "q" {
425                // Query message
426                let query_type = dict
427                    .get::<[u8]>(b"q")
428                    .and_then(|v| {
429                        if let Value::Bytes(bytes) = v {
430                            String::from_utf8(bytes.clone()).ok()
431                        } else {
432                            None
433                        }
434                    })
435                    .ok_or_else(|| anyhow::anyhow!("Missing 'q' field"))?;
436
437                // Get transaction ID
438                let transaction_id = dict.get::<[u8]>(b"t").and_then(|v| {
439                    if let Value::Bytes(bytes) = v {
440                        Some(hex::encode(bytes))
441                    } else {
442                        None
443                    }
444                });
445
446                // Get arguments.
447                //
448                // `query_type` is always present so a handler can tell an announce_peer
449                // from an unsupported method, both of which reach the ping event.
450                let mut params = serde_json::Map::new();
451                params.insert("query_type".to_string(), serde_json::json!(&query_type));
452                if let Some(transaction_id) = transaction_id {
453                    params.insert(
454                        "transaction_id".to_string(),
455                        serde_json::json!(transaction_id),
456                    );
457                }
458
459                if let Some(Value::Dict(args)) = dict.get::<[u8]>(b"a") {
460                    for (k, v) in args {
461                        let key = String::from_utf8_lossy(k).to_string();
462                        // `id`, `target` and `info_hash` are always 20 raw bytes and are
463                        // documented as hex. bencode_to_json would render them as text
464                        // whenever all 20 bytes happened to be printable ASCII, so the
465                        // same field arrived hex-encoded or not depending on the client's
466                        // random ID — and hex::decode on the response side then failed.
467                        let value = match (key.as_str(), v) {
468                            ("id" | "target" | "info_hash", Value::Bytes(bytes)) => {
469                                serde_json::json!(hex::encode(bytes))
470                            }
471                            _ => Self::bencode_to_json(v)?,
472                        };
473                        params.insert(key, value);
474                    }
475                }
476
477                Ok((query_type, serde_json::Value::Object(params)))
478            } else {
479                Err(anyhow::anyhow!("Not a query message"))
480            }
481        } else {
482            Err(anyhow::anyhow!("Invalid KRPC message"))
483        }
484    }
485
486    /// Convert a bencode value without relying on where it was decoded or constructed.
487    /// Lists and dictionaries each count as one level; scalars consume no depth.
488    pub fn bencode_to_json(value: &serde_bencode::value::Value) -> Result<serde_json::Value> {
489        Self::bencode_to_json_bounded(value, 0)
490    }
491
492    fn bencode_to_json_bounded(
493        value: &serde_bencode::value::Value,
494        depth: usize,
495    ) -> Result<serde_json::Value> {
496        use serde_bencode::value::Value;
497
498        if matches!(value, Value::List(_) | Value::Dict(_)) && depth >= MAX_DHT_VALUE_DEPTH {
499            anyhow::bail!("DHT value nesting exceeds the {MAX_DHT_VALUE_DEPTH} level limit");
500        }
501
502        Ok(match value {
503            Value::Int(i) => serde_json::json!(i),
504            Value::Bytes(bytes) => {
505                // Try to decode as UTF-8 string, otherwise hex encode
506                if let Ok(s) = String::from_utf8(bytes.clone()) {
507                    if s.chars()
508                        .all(|c| c.is_ascii_graphic() || c.is_ascii_whitespace())
509                    {
510                        serde_json::json!(s)
511                    } else {
512                        serde_json::json!(hex::encode(bytes))
513                    }
514                } else {
515                    serde_json::json!(hex::encode(bytes))
516                }
517            }
518            Value::List(list) => {
519                let json_list = list
520                    .iter()
521                    .map(|v| Self::bencode_to_json_bounded(v, depth + 1))
522                    .collect::<Result<Vec<_>>>()?;
523                serde_json::Value::Array(json_list)
524            }
525            Value::Dict(dict) => {
526                let mut json_obj = serde_json::Map::new();
527                for (k, v) in dict {
528                    let key = String::from_utf8_lossy(k).to_string();
529                    json_obj.insert(key, Self::bencode_to_json_bounded(v, depth + 1)?);
530                }
531                serde_json::Value::Object(json_obj)
532            }
533        })
534    }
535}