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}