netget/client/socks5/mod.rs
1//! SOCKS5 client implementation
2pub mod actions;
3
4pub use actions::Socks5ClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::io::{AsyncReadExt, AsyncWriteExt};
10use tokio::sync::{mpsc, Mutex};
11use tokio_socks::tcp::Socks5Stream;
12use tracing::{error, info, trace, warn};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::socks5::actions::{
16 SOCKS5_CLIENT_CONNECTED_EVENT, SOCKS5_CLIENT_DATA_RECEIVED_EVENT,
17};
18use crate::llm::ollama_client::OllamaClient;
19use crate::llm::ClientLlmResult;
20use crate::protocol::Event;
21use crate::state::app_state::AppState;
22use crate::state::{ClientId, ClientStatus};
23
24/// Connection state for LLM processing
25#[derive(Debug, Clone, PartialEq)]
26enum ConnectionState {
27 Idle,
28 Processing,
29 Accumulating,
30}
31
32/// Per-client data for LLM handling
33struct ClientData {
34 state: ConnectionState,
35 queued_data: Vec<u8>,
36 memory: String,
37}
38
39/// SOCKS5 client that connects to a target server through a SOCKS5 proxy
40pub struct Socks5Client;
41
42impl Socks5Client {
43 /// Connect to a target server through a SOCKS5 proxy with integrated LLM actions
44 pub async fn connect_with_llm_actions(
45 proxy_addr: String,
46 llm_client: OllamaClient,
47 app_state: Arc<AppState>,
48 status_tx: mpsc::UnboundedSender<String>,
49 client_id: ClientId,
50 startup_params: Option<crate::protocol::StartupParams>,
51 ) -> Result<SocketAddr> {
52 // Extract target address from startup params
53 let target_addr = startup_params
54 .as_ref()
55 .map(|p| p.get_string("target_addr"))
56 .transpose()?
57 .context("Missing required startup parameter 'target_addr'")?;
58
59 // Extract optional authentication
60 let auth_username = startup_params
61 .as_ref()
62 .map(|p| p.get_optional_string("auth_username"))
63 .transpose()?
64 .flatten();
65
66 let auth_password = startup_params
67 .as_ref()
68 .map(|p| p.get_optional_string("auth_password"))
69 .transpose()?
70 .flatten();
71
72 info!(
73 "SOCKS5 client {} connecting to {} through proxy {}",
74 client_id, target_addr, proxy_addr
75 );
76
77 // Connect through SOCKS5 proxy
78 let stream = if let (Some(username), Some(password)) = (auth_username, auth_password) {
79 // Connect with authentication
80 Socks5Stream::connect_with_password(
81 proxy_addr.as_str(),
82 target_addr.as_str(),
83 username.as_str(),
84 password.as_str(),
85 )
86 .await
87 .context(format!(
88 "Failed to connect to {} through SOCKS5 proxy {} with auth",
89 target_addr, proxy_addr
90 ))?
91 } else {
92 // Connect without authentication
93 Socks5Stream::connect(proxy_addr.as_str(), target_addr.as_str())
94 .await
95 .context(format!(
96 "Failed to connect to {} through SOCKS5 proxy {}",
97 target_addr, proxy_addr
98 ))?
99 };
100
101 // Get addresses before consuming the stream
102 let tcp_stream = stream.into_inner();
103 let local_addr = tcp_stream.local_addr()?;
104 let proxy_sock_addr = tcp_stream.peer_addr()?;
105
106 info!(
107 "SOCKS5 client {} connected through proxy {} to target {} (local: {})",
108 client_id, proxy_sock_addr, target_addr, local_addr
109 );
110
111 // Update client state
112 app_state
113 .update_client_status(client_id, ClientStatus::Connected)
114 .await;
115 let _ = status_tx.send(format!(
116 "[CLIENT] SOCKS5 client {} connected to {} through proxy",
117 client_id, target_addr
118 ));
119 let _ = status_tx.send("__UPDATE_UI__".to_string());
120
121 // Split stream (tcp_stream was already extracted above)
122 let (mut read_half, write_half) = tokio::io::split(tcp_stream);
123 let write_half_arc = Arc::new(Mutex::new(write_half));
124
125 // Initialize client data
126 let client_data = Arc::new(Mutex::new(ClientData {
127 state: ConnectionState::Idle,
128 queued_data: Vec::new(),
129 memory: String::new(),
130 }));
131
132 // Call LLM with connected event
133 let protocol = Arc::new(crate::client::socks5::actions::Socks5ClientProtocol::new());
134 let connected_event = Event::new(
135 &SOCKS5_CLIENT_CONNECTED_EVENT,
136 serde_json::json!({
137 "proxy_addr": proxy_addr,
138 "target_addr": target_addr,
139 }),
140 );
141
142 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
143 // Copy the memory out under its own guard. Passing
144 // `&client_data.lock().await.memory` straight into the call would keep the guard
145 // alive for the whole `match` -- temporaries in a match scrutinee live until the
146 // match ends -- and the `Ok` arm below locks the same mutex again to store the
147 // model's memory update. `tokio::sync::Mutex` is not reentrant, so that is a
148 // permanent deadlock, reached only when the model happens to return memory.
149 let memory = { client_data.lock().await.memory.clone() };
150 match call_llm_for_client(
151 &llm_client,
152 &app_state,
153 client_id.to_string(),
154 &instruction,
155 &memory,
156 Some(&connected_event),
157 protocol.as_ref(),
158 &status_tx,
159 )
160 .await
161 {
162 Ok(ClientLlmResult {
163 actions,
164 memory_updates,
165 }) => {
166 // Update memory
167 if let Some(mem) = memory_updates {
168 client_data.lock().await.memory = mem;
169 }
170
171 // Execute initial actions
172 for action in actions {
173 use crate::llm::actions::client_trait::Client;
174 match protocol.as_ref().execute_action(action) {
175 Ok(
176 crate::llm::actions::client_trait::ClientActionResult::SendData(
177 bytes,
178 ),
179 ) => {
180 // The `Err` half used to be discarded outright, so a failed
181 // write to the tunnel produced no log line at any level, no
182 // status message and no state change -- the client reported
183 // success having put nothing on the wire. The protocol's own
184 // CLAUDE.md claimed these were "logged but not fatal"; only
185 // the second half was true.
186 match write_half_arc.lock().await.write_all(&bytes).await {
187 Ok(()) => trace!(
188 "SOCKS5 client {} sent {} bytes through tunnel",
189 client_id,
190 bytes.len()
191 ),
192 Err(e) => error!(
193 "SOCKS5 client {} failed to write {} bytes to the tunnel: {}",
194 client_id,
195 bytes.len(),
196 e
197 ),
198 }
199 }
200 Ok(
201 crate::llm::actions::client_trait::ClientActionResult::Disconnect,
202 ) => {
203 warn!("SOCKS5 client {} requested disconnect immediately after connect", client_id);
204 }
205 _ => {}
206 }
207 }
208 }
209 Err(e) => {
210 error!(
211 "LLM error for SOCKS5 client {} on connect: {}",
212 client_id, e
213 );
214 }
215 }
216 }
217
218 // Spawn read loop
219 // Registered with AppState so stop_client can abort this task —
220 // dropping a JoinHandle only detaches it in Tokio.
221 let task_registrar = app_state.clone();
222 // Command channel: lets the dashboard inject actions into this loop
223 // via AppState::send_to_client (see client/command_support.rs).
224 let mut command_rx =
225 crate::client::command_support::register_command_channel(&app_state, client_id).await;
226
227 let task_handle = tokio::spawn(async move {
228 let mut buffer = vec![0u8; 8192];
229
230 loop {
231 let read_result = tokio::select! {
232 read = read_half.read(&mut buffer) => read,
233 Some(cmd) = command_rx.recv() => {
234 let disconnect = crate::client::command_support::handle_stream_client_command(
235 &crate::client::socks5::actions::Socks5ClientProtocol,
236 &write_half_arc,
237 cmd,
238 client_id,
239 &app_state,
240 &status_tx,
241 )
242 .await;
243 if disconnect {
244 app_state
245 .update_client_status(client_id, ClientStatus::Disconnected)
246 .await;
247 let _ = status_tx.send(format!(
248 "[CLIENT] SOCKS5 client {} disconnected (injected action)",
249 client_id
250 ));
251 let _ = status_tx.send("__UPDATE_UI__".to_string());
252 break;
253 }
254 continue;
255 }
256 };
257 match read_result {
258 Ok(0) => {
259 info!("SOCKS5 client {} disconnected", client_id);
260 app_state
261 .update_client_status(client_id, ClientStatus::Disconnected)
262 .await;
263 let _ = status_tx
264 .send(format!("[CLIENT] SOCKS5 client {} disconnected", client_id));
265 let _ = status_tx.send("__UPDATE_UI__".to_string());
266 break;
267 }
268 Ok(n) => {
269 let data = buffer[..n].to_vec();
270 trace!(
271 "SOCKS5 client {} received {} bytes from target",
272 client_id,
273 n
274 );
275
276 // Handle data with LLM
277 let mut client_data_lock = client_data.lock().await;
278
279 match client_data_lock.state {
280 ConnectionState::Idle => {
281 // Process immediately
282 client_data_lock.state = ConnectionState::Processing;
283 drop(client_data_lock);
284
285 // Call LLM
286 if let Some(instruction) =
287 app_state.get_instruction_for_client(client_id).await
288 {
289 let protocol = Arc::new(
290 crate::client::socks5::actions::Socks5ClientProtocol::new(),
291 );
292 let event = Event::new(
293 &SOCKS5_CLIENT_DATA_RECEIVED_EVENT,
294 crate::client::socks5::actions::inbound_event_fields(&data),
295 );
296
297 // Same reasoning as the connected-event call above: the
298 // guard must not survive into the match arms.
299 let memory = { client_data.lock().await.memory.clone() };
300 match call_llm_for_client(
301 &llm_client,
302 &app_state,
303 client_id.to_string(),
304 &instruction,
305 &memory,
306 Some(&event),
307 protocol.as_ref(),
308 &status_tx,
309 )
310 .await
311 {
312 Ok(ClientLlmResult {
313 actions,
314 memory_updates,
315 }) => {
316 // Update memory
317 if let Some(mem) = memory_updates {
318 client_data.lock().await.memory = mem;
319 }
320
321 // Execute actions
322 for action in actions {
323 use crate::llm::actions::client_trait::Client;
324 match protocol.as_ref().execute_action(action) {
325 Ok(crate::llm::actions::client_trait::ClientActionResult::SendData(bytes)) => {
326 match write_half_arc.lock().await.write_all(&bytes).await {
327 Ok(()) => trace!("SOCKS5 client {} sent {} bytes", client_id, bytes.len()),
328 Err(e) => error!("SOCKS5 client {} failed to write {} bytes to the tunnel: {}", client_id, bytes.len(), e),
329 }
330 }
331 Ok(crate::llm::actions::client_trait::ClientActionResult::Disconnect) => {
332 info!("SOCKS5 client {} disconnecting", client_id);
333 break;
334 }
335 _ => {}
336 }
337 }
338 }
339 Err(e) => {
340 error!(
341 "LLM error for SOCKS5 client {}: {}",
342 client_id, e
343 );
344 }
345 }
346 }
347
348 // Process queued data if any
349 let mut client_data_lock = client_data.lock().await;
350 if !client_data_lock.queued_data.is_empty() {
351 client_data_lock.queued_data.clear();
352 }
353 client_data_lock.state = ConnectionState::Idle;
354 }
355 ConnectionState::Processing => {
356 // Queue data
357 client_data_lock.queued_data.extend_from_slice(&data);
358 client_data_lock.state = ConnectionState::Accumulating;
359 }
360 ConnectionState::Accumulating => {
361 // Continue queuing
362 client_data_lock.queued_data.extend_from_slice(&data);
363 }
364 }
365 }
366 Err(e) => {
367 error!("SOCKS5 client {} read error: {}", client_id, e);
368 app_state
369 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
370 .await;
371 let _ = status_tx.send("__UPDATE_UI__".to_string());
372 break;
373 }
374 }
375 }
376
377 // The loop owns the only receiver; dropping the registered handle
378 // makes later send_to_client calls fail fast instead of timing out.
379 app_state.remove_client_handle(client_id).await;
380 });
381 task_registrar
382 .register_client_task(client_id, task_handle)
383 .await;
384
385 Ok(local_addr)
386 }
387}