Skip to main content

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}