Skip to main content

netget/client/tls/
mod.rs

1//! TLS client implementation
2//!
3//! Provides a generic TLS client that establishes encrypted connections
4//! and allows the LLM to implement custom application protocols.
5
6pub mod actions;
7
8pub use actions::TlsClientProtocol;
9
10use anyhow::{Context, Result};
11use std::net::SocketAddr;
12use std::sync::Arc;
13use tokio::io::{AsyncReadExt, AsyncWriteExt};
14use tokio::net::TcpStream;
15use tokio::sync::{mpsc, Mutex};
16use tokio_rustls::rustls::pki_types::ServerName;
17use tokio_rustls::rustls::{ClientConfig, RootCertStore};
18use tokio_rustls::TlsConnector;
19use tracing::{debug, error, info, trace};
20
21use crate::client::llm_budget::call_llm_for_client;
22use crate::client::tls::actions::{TLS_CLIENT_CONNECTED_EVENT, TLS_CLIENT_DATA_RECEIVED_EVENT};
23use crate::llm::ollama_client::OllamaClient;
24use crate::llm::ClientLlmResult;
25use crate::logging::patterns;
26use crate::protocol::Event;
27use crate::state::app_state::AppState;
28use crate::state::{ClientId, ClientStatus};
29
30/// Connection state for LLM processing
31#[derive(Debug, Clone, PartialEq)]
32enum ConnectionState {
33    Idle,
34    Processing,
35    Accumulating,
36}
37
38/// Per-client data for LLM handling
39struct ClientData {
40    state: ConnectionState,
41    queued_data: Vec<u8>,
42}
43
44/// TLS client that connects to a remote TLS server
45pub struct TlsClient;
46
47impl TlsClient {
48    /// Connect to a TLS server with integrated LLM actions
49    pub async fn connect_with_llm_actions(
50        remote_addr: String,
51        llm_client: OllamaClient,
52        app_state: Arc<AppState>,
53        status_tx: mpsc::UnboundedSender<String>,
54        client_id: ClientId,
55        startup_params: Option<crate::protocol::StartupParams>,
56    ) -> Result<SocketAddr> {
57        // Extract startup parameters
58        let accept_invalid_certs = startup_params
59            .as_ref()
60            .map(|p| p.get_optional_bool("accept_invalid_certs"))
61            .transpose()?
62            .flatten()
63            .unwrap_or(false);
64
65        let server_name_override = startup_params
66            .as_ref()
67            .map(|p| p.get_optional_string("server_name"))
68            .transpose()?
69            .flatten();
70
71        let custom_ca_cert_pem = startup_params
72            .as_ref()
73            .map(|p| p.get_optional_string("custom_ca_cert_pem"))
74            .transpose()?
75            .flatten();
76
77        debug!(
78            "TLS client {} connecting to {} (accept_invalid_certs: {})",
79            client_id, remote_addr, accept_invalid_certs
80        );
81
82        // Determine server name for SNI (before connecting)
83        let server_name_str = if let Some(override_name) = server_name_override {
84            override_name
85        } else {
86            // Extract hostname from remote_addr (e.g., "example.com:443" -> "example.com")
87            remote_addr
88                .split(':')
89                .next()
90                .unwrap_or(&remote_addr)
91                .to_string()
92        };
93
94        // Make sure rustls has a process-level CryptoProvider before any builder runs.
95        //
96        // `ClientConfig::builder()` *panics* when it cannot pick one automatically, and it
97        // cannot whenever more than one provider feature is live in the build - which is
98        // the normal case here, because `all-protocols` pulls in dependencies that enable
99        // both `ring` and `aws-lc-rs`. The panic reads "Could not automatically determine
100        // the process-level CryptoProvider from Rustls crate features".
101        //
102        // `src/bin/netget.rs` installs the provider at startup, so the shipped binary was
103        // fine and this stayed hidden; anything else using NetGet as a library - every
104        // test, and the protocol smoke sweep that found this - reached the builder with no
105        // provider installed and took down the connecting task instead of returning `Err`.
106        // The servers already guard themselves this way (`src/server/tls_cert_manager.rs`,
107        // `src/server/proxy/tls_mitm.rs`); the TLS client did not.
108        //
109        // Installing is idempotent-by-ignoring: `install_default` returns `Err` if one is
110        // already installed, which is exactly the case we want to do nothing about.
111        let _ = tokio_rustls::rustls::crypto::ring::default_provider().install_default();
112
113        // Create TLS config
114        let config = if accept_invalid_certs {
115            // Accept any certificate (for testing with self-signed certs)
116            debug!("TLS client {} accepting invalid certificates", client_id);
117            ClientConfig::builder()
118                .dangerous()
119                .with_custom_certificate_verifier(Arc::new(NoVerification))
120                .with_no_client_auth()
121        } else if let Some(ref ca_pem) = custom_ca_cert_pem {
122            // Use custom CA certificate for validation
123            debug!("TLS client {} using custom CA certificate", client_id);
124
125            // Parse CA certificate from PEM
126            let ca_certs = rustls_pemfile::certs(&mut ca_pem.as_bytes())
127                .collect::<Result<Vec<_>, _>>()
128                .context("Failed to parse custom CA certificate PEM")?;
129
130            if ca_certs.is_empty() {
131                return Err(anyhow::anyhow!(
132                    "No certificates found in custom_ca_cert_pem"
133                ));
134            }
135
136            // Create root store with custom CA
137            let mut root_store = RootCertStore::empty();
138            for cert in ca_certs {
139                root_store
140                    .add(cert)
141                    .context("Failed to add custom CA certificate to root store")?;
142            }
143
144            ClientConfig::builder()
145                .with_root_certificates(root_store)
146                .with_no_client_auth()
147        } else {
148            // Use webpki roots for validation
149            let root_store = RootCertStore {
150                roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(),
151            };
152            ClientConfig::builder()
153                .with_root_certificates(root_store)
154                .with_no_client_auth()
155        };
156
157        let connector = TlsConnector::from(Arc::new(config));
158
159        // Connect TCP stream (DNS resolution happens automatically)
160        let tcp_stream = TcpStream::connect(&remote_addr)
161            .await
162            .context(format!("Failed to connect to {}", remote_addr))?;
163
164        let local_addr = tcp_stream.local_addr()?;
165        let remote_socket_addr = tcp_stream.peer_addr()?;
166        debug!(
167            "TLS client {} TCP connected to {} (local: {})",
168            client_id, remote_socket_addr, local_addr
169        );
170
171        // Perform TLS handshake
172        let server_name = ServerName::try_from(server_name_str.clone())
173            .map_err(|e| anyhow::anyhow!("Invalid server name '{}': {}", server_name_str, e))?;
174
175        let tls_stream = connector
176            .connect(server_name, tcp_stream)
177            .await
178            .context("TLS handshake failed")?;
179
180        info!(
181            "TLS client {} connected to {} (TLS handshake complete)",
182            client_id, remote_socket_addr
183        );
184
185        // Update client state
186        app_state
187            .update_client_status(client_id, ClientStatus::Connected)
188            .await;
189        let _ = status_tx.send(format!("[CLIENT] TLS client {} connected", client_id));
190        let _ = status_tx.send("__UPDATE_UI__".to_string());
191
192        // Split stream
193        let (mut read_half, write_half) = tokio::io::split(tls_stream);
194        let write_half_arc = Arc::new(Mutex::new(write_half));
195        let write_half_for_connected = write_half_arc.clone();
196
197        // Initialize client data
198        let client_data = Arc::new(Mutex::new(ClientData {
199            state: ConnectionState::Idle,
200            queued_data: Vec::new(),
201        }));
202
203        // Call LLM with tls_client_connected event
204        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
205            let event = Event::new(
206                &TLS_CLIENT_CONNECTED_EVENT,
207                serde_json::json!({
208                    "remote_addr": remote_socket_addr.to_string(),
209                    "server_name": server_name_str,
210                }),
211            );
212
213            // The client's memory lives in `AppState`, which is where `set_memory` /
214            // `append_memory` write it. This used to read a `ClientData.memory` field that
215            // nothing but this file ever wrote, so a model that stored something with
216            // `set_memory` was handed an empty memory on its very next turn.
217            //
218            // Reading it here also keeps the guard out of the call: written inline as an
219            // argument, `client_data.lock().await.memory` would create a temporary guard
220            // living to the end of the whole `match`, and the arm that writes the memory back
221            // would then deadlock on the same non-reentrant mutex. That shape deadlocked the
222            // NNTP client's read loop.
223            let memory_snapshot = app_state
224                .get_memory_for_client(client_id)
225                .await
226                .unwrap_or_default();
227
228            match call_llm_for_client(
229                &llm_client,
230                &app_state,
231                client_id.to_string(),
232                &instruction,
233                &memory_snapshot,
234                Some(&event),
235                &crate::client::tls::actions::TlsClientProtocol,
236                &status_tx,
237            )
238            .await
239            {
240                Ok(result) => {
241                    // Update memory if provided
242                    if let Some(new_memory) = result.memory_updates {
243                        app_state.set_memory_for_client(client_id, new_memory).await;
244                    }
245
246                    // Execute actions from LLM response
247                    use crate::llm::actions::client_trait::Client;
248                    let protocol = crate::client::tls::actions::TlsClientProtocol::new();
249                    for action in result.actions {
250                        match protocol.execute_action(action) {
251                            Ok(
252                                crate::llm::actions::client_trait::ClientActionResult::SendData(
253                                    bytes,
254                                ),
255                            ) => {
256                                let mut write_guard = write_half_for_connected.lock().await;
257                                if let Err(e) = write_guard.write_all(&bytes).await {
258                                    error!("Failed to send data after connect: {}", e);
259                                } else if let Err(e) = write_guard.flush().await {
260                                    error!("Failed to flush after connect: {}", e);
261                                } else {
262                                    info!("Sent {} {}", bytes.len(), patterns::TLS_CLIENT_SENT);
263                                }
264                            }
265                            Ok(
266                                crate::llm::actions::client_trait::ClientActionResult::Disconnect,
267                            ) => {
268                                info!("LLM requested disconnect after connect");
269                                return Ok(local_addr);
270                            }
271                            Ok(
272                                crate::llm::actions::client_trait::ClientActionResult::WaitForMore,
273                            ) => {
274                                // Just wait for data
275                            }
276                            Ok(_) => {
277                                // Other action results
278                            }
279                            Err(e) => {
280                                error!("Failed to execute action after connect: {}", e);
281                            }
282                        }
283                    }
284                }
285                Err(e) => {
286                    error!("LLM error on tls_client_connected event: {}", e);
287                }
288            }
289        }
290
291        // Spawn read loop
292        // Registered with AppState so stop_client can abort this task —
293        // dropping a JoinHandle only detaches it in Tokio.
294        let task_registrar = app_state.clone();
295        // Command channel: lets the dashboard inject actions into this loop
296        // via AppState::send_to_client (see client/command_support.rs).
297        let mut command_rx =
298            crate::client::command_support::register_command_channel(&app_state, client_id).await;
299
300        let task_handle = tokio::spawn(async move {
301            info!("TLS client {} read loop started", client_id);
302            let mut buffer = vec![0u8; 8192];
303
304            loop {
305                let read_result = tokio::select! {
306                    read = read_half.read(&mut buffer) => read,
307                    Some(cmd) = command_rx.recv() => {
308                        let disconnect = crate::client::command_support::handle_stream_client_command(
309                            &crate::client::tls::actions::TlsClientProtocol,
310                            &write_half_arc,
311                            cmd,
312                            client_id,
313                            &app_state,
314                            &status_tx,
315                        )
316                        .await;
317                        if disconnect {
318                            app_state
319                                .update_client_status(client_id, ClientStatus::Disconnected)
320                                .await;
321                            let _ = status_tx.send(format!(
322                                "[CLIENT] TLS client {} disconnected (injected action)",
323                                client_id
324                            ));
325                            let _ = status_tx.send("__UPDATE_UI__".to_string());
326                            break;
327                        }
328                        continue;
329                    }
330                };
331                match read_result {
332                    Ok(0) => {
333                        info!(
334                            "TLS client {} {}",
335                            client_id,
336                            patterns::TLS_CLIENT_DISCONNECTED
337                        );
338                        app_state
339                            .update_client_status(client_id, ClientStatus::Disconnected)
340                            .await;
341                        let _ = status_tx
342                            .send(format!("[CLIENT] TLS client {} disconnected", client_id));
343                        let _ = status_tx.send("__UPDATE_UI__".to_string());
344                        break;
345                    }
346                    Ok(n) => {
347                        let data = buffer[..n].to_vec();
348                        info!(
349                            "TLS client {} received {} {}",
350                            client_id,
351                            n,
352                            patterns::TLS_CLIENT_RECEIVED
353                        );
354                        trace!("TLS client {} received {} bytes", client_id, n);
355
356                        // Decide under the lock what this chunk belongs to, then drop the
357                        // guard before doing anything that awaits.
358                        //
359                        // `Accumulating` is not a terminal state: it means the model answered
360                        // `wait_for_more`, so the bytes it has already seen are the front of
361                        // this turn and the new chunk completes it. Treating it as "keep
362                        // queueing" left the client permanently deaf after a single
363                        // `wait_for_more` - nothing ever moved it back to Idle, so no further
364                        // read reached the model.
365                        let payload = {
366                            let mut client_data_lock = client_data.lock().await;
367                            match client_data_lock.state {
368                                ConnectionState::Processing => {
369                                    client_data_lock.queued_data.extend_from_slice(&data);
370                                    None
371                                }
372                                ConnectionState::Idle | ConnectionState::Accumulating => {
373                                    let mut merged =
374                                        std::mem::take(&mut client_data_lock.queued_data);
375                                    merged.extend_from_slice(&data);
376                                    client_data_lock.state = ConnectionState::Processing;
377                                    Some(merged)
378                                }
379                            }
380                        };
381
382                        let Some(mut payload) = payload else {
383                            continue;
384                        };
385
386                        let Some(instruction) =
387                            app_state.get_instruction_for_client(client_id).await
388                        else {
389                            // No instruction: nothing can be asked, so do not hold the
390                            // connection in Processing forever.
391                            let mut client_data_lock = client_data.lock().await;
392                            client_data_lock.state = ConnectionState::Idle;
393                            continue;
394                        };
395
396                        let protocol =
397                            Arc::new(crate::client::tls::actions::TlsClientProtocol::new());
398                        let mut disconnect_requested = false;
399
400                        loop {
401                            // Try to decode as UTF-8, fallback to hex
402                            let data_str = match String::from_utf8(payload.clone()) {
403                                Ok(utf8) => utf8,
404                                Err(_) => format!("HEX:{}", hex::encode(&payload)),
405                            };
406
407                            let event = Event::new(
408                                &TLS_CLIENT_DATA_RECEIVED_EVENT,
409                                serde_json::json!({
410                                    "data": data_str,
411                                    "data_length": payload.len(),
412                                }),
413                            );
414
415                            // From AppState, for the reasons in the connect path above:
416                            // that is where the client's memory actually lives, and taking a
417                            // guard inline here would deadlock the write below.
418                            let memory_snapshot = app_state
419                                .get_memory_for_client(client_id)
420                                .await
421                                .unwrap_or_default();
422
423                            let mut wait_for_more = false;
424                            match call_llm_for_client(
425                                &llm_client,
426                                &app_state,
427                                client_id.to_string(),
428                                &instruction,
429                                &memory_snapshot,
430                                Some(&event),
431                                protocol.as_ref(),
432                                &status_tx,
433                            )
434                            .await
435                            {
436                                Ok(ClientLlmResult {
437                                    actions,
438                                    memory_updates,
439                                }) => {
440                                    if let Some(mem) = memory_updates {
441                                        app_state.set_memory_for_client(client_id, mem).await;
442                                    }
443
444                                    for action in actions {
445                                        use crate::llm::actions::client_trait::Client;
446                                        use crate::llm::actions::client_trait::ClientActionResult;
447                                        match protocol.as_ref().execute_action(action) {
448                                            Ok(ClientActionResult::SendData(bytes)) => {
449                                                let mut write_guard = write_half_arc.lock().await;
450                                                if let Err(e) = write_guard.write_all(&bytes).await
451                                                {
452                                                    error!(
453                                                        "TLS client {} send failed: {}",
454                                                        client_id, e
455                                                    );
456                                                } else if let Err(e) = write_guard.flush().await {
457                                                    error!(
458                                                        "TLS client {} flush failed: {}",
459                                                        client_id, e
460                                                    );
461                                                } else {
462                                                    trace!(
463                                                        "TLS client {} sent {} bytes",
464                                                        client_id,
465                                                        bytes.len()
466                                                    );
467                                                }
468                                            }
469                                            Ok(ClientActionResult::Disconnect) => {
470                                                info!("TLS client {} disconnecting", client_id);
471                                                disconnect_requested = true;
472                                                break;
473                                            }
474                                            Ok(ClientActionResult::WaitForMore) => {
475                                                wait_for_more = true;
476                                            }
477                                            Ok(_) => {}
478                                            Err(e) => {
479                                                error!(
480                                                    "TLS client {} could not execute action: {}",
481                                                    client_id, e
482                                                );
483                                            }
484                                        }
485                                    }
486                                }
487                                Err(e) => {
488                                    error!("LLM error for TLS client {}: {}", client_id, e);
489                                }
490                            }
491
492                            if disconnect_requested {
493                                break;
494                            }
495
496                            let mut client_data_lock = client_data.lock().await;
497                            if wait_for_more {
498                                // Keep the bytes the model has already seen: the next chunk is
499                                // appended to them and the whole thing is re-offered.
500                                let mut pending = std::mem::take(&mut payload);
501                                pending.extend_from_slice(&client_data_lock.queued_data);
502                                client_data_lock.queued_data = pending;
503                                client_data_lock.state = ConnectionState::Accumulating;
504                                break;
505                            }
506
507                            if client_data_lock.queued_data.is_empty() {
508                                client_data_lock.state = ConnectionState::Idle;
509                                break;
510                            }
511
512                            // Data arrived while the model was thinking: it is the next turn,
513                            // not something to discard (it used to be cleared unread).
514                            payload = std::mem::take(&mut client_data_lock.queued_data);
515                        }
516
517                        if disconnect_requested {
518                            let mut write_guard = write_half_arc.lock().await;
519                            let _ = write_guard.shutdown().await;
520                            drop(write_guard);
521                            app_state
522                                .update_client_status(client_id, ClientStatus::Disconnected)
523                                .await;
524                            let _ = status_tx.send(format!(
525                                "[CLIENT] TLS client {} disconnected (model requested)",
526                                client_id
527                            ));
528                            let _ = status_tx.send("__UPDATE_UI__".to_string());
529                            break;
530                        }
531                    }
532                    Err(e) => {
533                        error!("TLS client {} read error: {}", client_id, e);
534                        app_state
535                            .update_client_status(client_id, ClientStatus::Error(e.to_string()))
536                            .await;
537                        let _ = status_tx.send("__UPDATE_UI__".to_string());
538                        break;
539                    }
540                }
541            }
542
543            // The loop owns the only receiver; dropping the registered handle
544            // makes later send_to_client calls fail fast instead of timing out.
545            app_state.remove_client_handle(client_id).await;
546        });
547        task_registrar
548            .register_client_task(client_id, task_handle)
549            .await;
550
551        Ok(local_addr)
552    }
553}
554
555/// Custom certificate verifier that accepts any certificate
556/// Used when accept_invalid_certs is true (for testing self-signed certs)
557#[derive(Debug)]
558struct NoVerification;
559
560impl tokio_rustls::rustls::client::danger::ServerCertVerifier for NoVerification {
561    fn verify_server_cert(
562        &self,
563        _end_entity: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
564        _intermediates: &[tokio_rustls::rustls::pki_types::CertificateDer<'_>],
565        _server_name: &tokio_rustls::rustls::pki_types::ServerName<'_>,
566        _ocsp_response: &[u8],
567        _now: tokio_rustls::rustls::pki_types::UnixTime,
568    ) -> Result<tokio_rustls::rustls::client::danger::ServerCertVerified, tokio_rustls::rustls::Error>
569    {
570        // Accept all certificates
571        Ok(tokio_rustls::rustls::client::danger::ServerCertVerified::assertion())
572    }
573
574    fn verify_tls12_signature(
575        &self,
576        _message: &[u8],
577        _cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
578        _dss: &tokio_rustls::rustls::DigitallySignedStruct,
579    ) -> Result<
580        tokio_rustls::rustls::client::danger::HandshakeSignatureValid,
581        tokio_rustls::rustls::Error,
582    > {
583        // Accept all signatures
584        Ok(tokio_rustls::rustls::client::danger::HandshakeSignatureValid::assertion())
585    }
586
587    fn verify_tls13_signature(
588        &self,
589        _message: &[u8],
590        _cert: &tokio_rustls::rustls::pki_types::CertificateDer<'_>,
591        _dss: &tokio_rustls::rustls::DigitallySignedStruct,
592    ) -> Result<
593        tokio_rustls::rustls::client::danger::HandshakeSignatureValid,
594        tokio_rustls::rustls::Error,
595    > {
596        // Accept all signatures
597        Ok(tokio_rustls::rustls::client::danger::HandshakeSignatureValid::assertion())
598    }
599
600    fn supported_verify_schemes(&self) -> Vec<tokio_rustls::rustls::SignatureScheme> {
601        // Support all signature schemes
602        vec![
603            tokio_rustls::rustls::SignatureScheme::RSA_PKCS1_SHA256,
604            tokio_rustls::rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
605            tokio_rustls::rustls::SignatureScheme::ED25519,
606            tokio_rustls::rustls::SignatureScheme::RSA_PSS_SHA256,
607        ]
608    }
609}