1pub mod actions;
11pub mod filter;
12
13use crate::server::connection::ConnectionId;
14use anyhow::{bail, Context, Result};
15use filter::{FilterMode, Socks5FilterConfig};
16use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
17use std::sync::Arc;
18use tokio::io::{AsyncReadExt, AsyncWriteExt};
19use tokio::net::TcpStream;
20use tokio::sync::mpsc;
21use tracing::{error, info, warn};
22
23use crate::llm::action_helper::call_llm;
24use crate::llm::actions::protocol_trait::ActionResult;
25use crate::llm::ollama_client::OllamaClient;
26use crate::logging::emit::Log;
27use crate::protocol::Event;
28use crate::server::Socks5Protocol;
29use crate::state::app_state::AppState;
30use crate::state::ServerId;
31use actions::{SOCKS5_AUTH_REQUEST_EVENT, SOCKS5_CONNECT_REQUEST_EVENT};
32
33const SOCKS5_VERSION: u8 = 0x05;
35const AUTH_METHOD_NO_AUTH: u8 = 0x00;
36const AUTH_METHOD_USERNAME_PASSWORD: u8 = 0x02;
37const AUTH_METHOD_NO_ACCEPTABLE: u8 = 0xFF;
38
39const CMD_CONNECT: u8 = 0x01;
40const ATYP_IPV4: u8 = 0x01;
41const ATYP_DOMAIN: u8 = 0x03;
42const ATYP_IPV6: u8 = 0x04;
43
44const REPLY_SUCCESS: u8 = 0x00;
45const REPLY_GENERAL_FAILURE: u8 = 0x01;
46const REPLY_CONNECTION_NOT_ALLOWED: u8 = 0x02;
47const REPLY_NETWORK_UNREACHABLE: u8 = 0x03;
48const REPLY_HOST_UNREACHABLE: u8 = 0x04;
49const REPLY_CONNECTION_REFUSED: u8 = 0x05;
50
51const _REPLY_COMMAND_NOT_SUPPORTED: u8 = 0x07;
52const _REPLY_ADDRESS_TYPE_NOT_SUPPORTED: u8 = 0x08;
53
54const HANDSHAKE_TIMEOUT_SECS: u64 = 30;
61
62pub const RELAY_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3600);
85
86struct TouchOnRead<'a> {
90 inner: &'a mut TcpStream,
91 activity: Arc<crate::server::accept_bounded::ConnectionActivity>,
92 bytes_read: u64,
93}
94
95impl<'a> TouchOnRead<'a> {
96 fn new(
97 inner: &'a mut TcpStream,
98 activity: Arc<crate::server::accept_bounded::ConnectionActivity>,
99 ) -> Self {
100 Self {
101 inner,
102 activity,
103 bytes_read: 0,
104 }
105 }
106}
107
108impl tokio::io::AsyncRead for TouchOnRead<'_> {
109 fn poll_read(
110 self: std::pin::Pin<&mut Self>,
111 cx: &mut std::task::Context<'_>,
112 buf: &mut tokio::io::ReadBuf<'_>,
113 ) -> std::task::Poll<std::io::Result<()>> {
114 let this = self.get_mut();
115 let before = buf.filled().len();
116 let poll = std::pin::Pin::new(&mut *this.inner).poll_read(cx, buf);
117 if let std::task::Poll::Ready(Ok(())) = &poll {
118 let n = buf.filled().len() - before;
119 if n > 0 {
120 this.bytes_read += n as u64;
121 this.activity.touch();
122 }
123 }
124 poll
125 }
126}
127
128impl tokio::io::AsyncWrite for TouchOnRead<'_> {
129 fn poll_write(
130 self: std::pin::Pin<&mut Self>,
131 cx: &mut std::task::Context<'_>,
132 buf: &[u8],
133 ) -> std::task::Poll<std::io::Result<usize>> {
134 std::pin::Pin::new(&mut *self.get_mut().inner).poll_write(cx, buf)
135 }
136
137 fn poll_flush(
138 self: std::pin::Pin<&mut Self>,
139 cx: &mut std::task::Context<'_>,
140 ) -> std::task::Poll<std::io::Result<()>> {
141 std::pin::Pin::new(&mut *self.get_mut().inner).poll_flush(cx)
142 }
143
144 fn poll_shutdown(
145 self: std::pin::Pin<&mut Self>,
146 cx: &mut std::task::Context<'_>,
147 ) -> std::task::Poll<std::io::Result<()>> {
148 std::pin::Pin::new(&mut *self.get_mut().inner).poll_shutdown(cx)
149 }
150}
151
152const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
167
168const CONNECTION_CAP_REFUSAL: &[u8] = &[SOCKS5_VERSION, AUTH_METHOD_NO_ACCEPTABLE];
192
193fn encode_relay_data(data: &[u8]) -> (String, &'static str) {
197 if data
198 .iter()
199 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
200 {
201 (String::from_utf8_lossy(data).to_string(), "utf8")
202 } else {
203 (hex::encode(data), "hex")
204 }
205}
206
207fn socks5_reply_for_connect_error(err: &anyhow::Error) -> u8 {
210 for cause in err.chain() {
211 if let Some(io_err) = cause.downcast_ref::<std::io::Error>() {
212 return match io_err.kind() {
213 std::io::ErrorKind::ConnectionRefused => REPLY_CONNECTION_REFUSED,
214 std::io::ErrorKind::TimedOut => REPLY_HOST_UNREACHABLE,
215 std::io::ErrorKind::NetworkUnreachable => REPLY_NETWORK_UNREACHABLE,
216 std::io::ErrorKind::HostUnreachable => REPLY_HOST_UNREACHABLE,
217 _ => REPLY_GENERAL_FAILURE,
218 };
219 }
220 }
221 REPLY_GENERAL_FAILURE
222}
223
224#[derive(Debug, Clone)]
226pub enum TargetAddr {
227 Ipv4(Ipv4Addr, u16),
228 Ipv6(Ipv6Addr, u16),
229 Domain(String, u16),
230}
231
232impl std::fmt::Display for TargetAddr {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 match self {
235 TargetAddr::Ipv4(ip, port) => write!(f, "{}:{}", ip, port),
236 TargetAddr::Ipv6(ip, port) => write!(f, "[{}]:{}", ip, port),
237 TargetAddr::Domain(domain, port) => write!(f, "{}:{}", domain, port),
238 }
239 }
240}
241
242pub struct Socks5Server;
244
245impl Socks5Server {
246 pub async fn spawn_with_llm_actions(
248 listen_addr: SocketAddr,
249 llm_client: OllamaClient,
250 app_state: Arc<AppState>,
251 status_tx: mpsc::UnboundedSender<String>,
252 server_id: ServerId,
253 startup_params: Option<crate::protocol::StartupParams>,
254 ) -> Result<SocketAddr> {
255 Log::new(Some(&status_tx)).info(format!("SOCKS5 starting on {}", listen_addr));
256
257 let mut config = app_state
259 .get_socks5_filter_config(server_id)
260 .await
261 .unwrap_or_else(|| {
262 info!("No SOCKS5 filter config found, using defaults");
263 Socks5FilterConfig::default()
264 });
265
266 let mut relay_idle = RELAY_IDLE_TIMEOUT;
267
268 if let Some(ref params) = startup_params {
270 Log::new(Some(&status_tx)).info("Applying SOCKS5 startup parameters");
271
272 if let Some(methods) = params.get_optional_array("auth_methods")? {
274 config.auth_methods.clear();
275 for method in methods {
276 if let Some(method_str) = method.as_str() {
277 match method_str {
278 "none" => config.auth_methods.push(AUTH_METHOD_NO_AUTH),
279 "username_password" => {
280 config.auth_methods.push(AUTH_METHOD_USERNAME_PASSWORD)
281 }
282 _ => warn!("Unknown auth method: {}", method_str),
283 }
284 }
285 }
286 Log::new(Some(&status_tx)).info(format!("Auth methods: {:?}", config.auth_methods));
287 }
288
289 if let Some(action_str) = params.get_optional_string("default_action")? {
291 config.default_action = action_str;
292 Log::new(Some(&status_tx))
293 .info(format!("Default action: {}", config.default_action));
294 }
295
296 if let Some(filter) = params.get_optional_object("filter")? {
298 if let Some(patterns) = filter
299 .get("target_host_patterns")
300 .and_then(|v| v.as_array())
301 {
302 config.target_host_patterns = patterns
303 .iter()
304 .filter_map(|v| v.as_str())
305 .map(|s| s.to_string())
306 .collect();
307 }
308 if let Some(ranges) = filter.get("target_port_ranges").and_then(|v| v.as_array()) {
309 config.target_port_ranges = ranges
310 .iter()
311 .filter_map(|v| v.as_array())
312 .filter_map(|arr| {
313 if arr.len() == 2 {
314 let start = arr[0].as_u64()? as u16;
315 let end = arr[1].as_u64()? as u16;
316 Some((start, end))
317 } else {
318 None
319 }
320 })
321 .collect();
322 }
323 }
324
325 if let Some(mode_str) = params.get_optional_string("filter_mode")? {
327 config.filter_mode = match mode_str.as_str() {
328 "allow_all" => FilterMode::AllowAll,
329 "deny_all" => FilterMode::DenyAll,
330 "ask_llm" => FilterMode::AskLlm,
331 "selective" => FilterMode::Selective,
332 _ => {
333 warn!("Unknown filter mode: {}, using default", mode_str);
334 config.filter_mode
335 }
336 };
337 Log::new(Some(&status_tx)).info(format!("Filter mode: {:?}", config.filter_mode));
338 }
339
340 if let Some(secs) = params.get_optional_u64("idle_timeout_secs")? {
342 relay_idle = std::time::Duration::from_secs(secs);
343 }
344
345 if let Some(mitm) = params.get_optional_bool("mitm_by_default")? {
347 config.mitm_by_default = mitm;
348 Log::new(Some(&status_tx))
349 .info(format!("MITM by default: {}", config.mitm_by_default));
350 }
351 }
352
353 app_state
355 .set_socks5_filter_config(server_id, config.clone())
356 .await;
357
358 let listener =
359 crate::server::socket_helpers::create_reusable_tcp_listener(listen_addr).await?;
360 let local_addr = listener.local_addr()?;
361 Log::new(Some(&status_tx)).info(format!("SOCKS5 proxy ready on {}", local_addr));
362
363 let protocol = Arc::new(Socks5Protocol::new());
364
365 let task_registrar = app_state.clone();
366 let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
367 let accept_handle = tokio::spawn(async move {
368 loop {
369 match crate::server::accept_bounded::accept_bounded(
370 &listener,
371 &limiter,
372 CONNECTION_CAP_REFUSAL,
373 "SOCKS5",
374 Some(&status_tx),
375 )
376 .await
377 {
378 Ok((stream, remote_addr, permit)) => {
379 let connection_id =
380 ConnectionId::new(app_state.get_next_unified_id().await);
381 let local_addr_conn = stream.local_addr().unwrap_or(local_addr);
382 let llm_clone = llm_client.clone();
383 let state_clone = app_state.clone();
384 let status_clone = status_tx.clone();
385 let protocol_clone = protocol.clone();
386 let config_clone = config.clone();
387
388 let task_owner = app_state.clone();
390 task_owner
391 .spawn_server_task(server_id, async move {
392 let _permit = permit;
398 Log::new(Some(&status_clone)).info(format!(
399 "SOCKS5 connection {} from {}",
400 connection_id, remote_addr
401 ));
402
403 if let Err(e) = Self::handle_connection(
404 stream,
405 connection_id,
406 remote_addr,
407 local_addr_conn,
408 llm_clone,
409 state_clone.clone(),
410 status_clone.clone(),
411 protocol_clone,
412 server_id,
413 config_clone,
414 relay_idle,
415 )
416 .await
417 {
418 Log::new(Some(&status_clone)).error(format!(
419 "SOCKS5 connection {} error: {}",
420 connection_id, e
421 ));
422 }
423
424 state_clone
426 .close_connection_on_server(server_id, connection_id)
427 .await;
428 let _ = status_clone.send("__UPDATE_UI__".to_string());
429 })
430 .await;
431 }
432 Err(e) => {
433 error!("Failed to accept SOCKS5 connection: {}", e);
434 break;
435 }
436 }
437 }
438 });
439
440 task_registrar
441 .register_server_task(server_id, accept_handle)
442 .await;
443
444 Ok(local_addr)
445 }
446
447 async fn handle_connection(
449 mut client_stream: TcpStream,
450 connection_id: ConnectionId,
451 remote_addr: SocketAddr,
452 local_addr: SocketAddr,
453 llm_client: OllamaClient,
454 app_state: Arc<AppState>,
455 status_tx: mpsc::UnboundedSender<String>,
456 protocol: Arc<Socks5Protocol>,
457 server_id: ServerId,
458 config: Socks5FilterConfig,
459 relay_idle: std::time::Duration,
460 ) -> Result<()> {
461 use crate::state::server::{
463 ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
464 };
465 let now = crate::utils::clock::Instant::now();
466 let conn_state = ServerConnectionState {
467 id: connection_id,
468 remote_addr,
469 local_addr,
470 bytes_sent: 0,
471 bytes_received: 0,
472 packets_sent: 0,
473 packets_received: 0,
474 last_activity: now,
475 status: ConnectionStatus::Active,
476 status_changed_at: now,
477 protocol_info: ProtocolConnectionInfo::empty(),
478 };
479 app_state
480 .add_connection_to_server(server_id, conn_state)
481 .await;
482 let _ = status_tx.send("__UPDATE_UI__".to_string());
483
484 Log::new(Some(&status_tx)).debug(format!("SOCKS5 {} phase 1: handshake", connection_id));
486
487 let selected_method = tokio::time::timeout(
488 std::time::Duration::from_secs(HANDSHAKE_TIMEOUT_SECS),
489 Self::negotiate_auth(&mut client_stream, &config, connection_id, &status_tx),
490 )
491 .await
492 .map_err(|_| anyhow::anyhow!("timed out waiting for the SOCKS5 greeting"))??;
493
494 Log::new(Some(&status_tx)).debug(format!(
495 "SOCKS5 {} selected auth method: 0x{:02x}",
496 connection_id, selected_method
497 ));
498
499 let username = if selected_method == AUTH_METHOD_USERNAME_PASSWORD {
501 Log::new(Some(&status_tx))
502 .debug(format!("SOCKS5 {} phase 2: authentication", connection_id));
503
504 let auth_result = Self::authenticate_username_password(
505 &mut client_stream,
506 connection_id,
507 &llm_client,
508 &app_state,
509 &status_tx,
510 &protocol,
511 server_id,
512 )
513 .await?;
514
515 Some(auth_result)
516 } else {
517 None
518 };
519
520 Log::new(Some(&status_tx))
522 .debug(format!("SOCKS5 {} phase 3: CONNECT request", connection_id));
523
524 let target_addr = tokio::time::timeout(
525 std::time::Duration::from_secs(HANDSHAKE_TIMEOUT_SECS),
526 Self::parse_connect_request(&mut client_stream, connection_id, &status_tx),
527 )
528 .await
529 .map_err(|_| anyhow::anyhow!("timed out waiting for the SOCKS5 CONNECT request"))??;
530
531 Log::new(Some(&status_tx)).info(format!(
532 "SOCKS5 {} CONNECT to {}",
533 connection_id, target_addr
534 ));
535
536 app_state
538 .update_socks5_target(
539 server_id,
540 connection_id,
541 Some(target_addr.to_string()),
542 username.clone(),
543 )
544 .await;
545
546 let matches_filter = Self::check_filter_match(&target_addr, &config);
548
549 Log::new(Some(&status_tx)).debug(format!(
550 "SOCKS5 {} filter match: {}",
551 connection_id, matches_filter
552 ));
553
554 let (should_allow, mitm_enabled) = match (&config.filter_mode, matches_filter) {
556 (FilterMode::AllowAll, _) => (true, config.mitm_by_default),
557 (FilterMode::DenyAll, _) => (false, false),
558 (FilterMode::Selective, true) | (FilterMode::AskLlm, _) => {
559 match Self::ask_llm_for_decision(
564 &target_addr,
565 username.as_deref(),
566 connection_id,
567 &llm_client,
568 &app_state,
569 &status_tx,
570 &protocol,
571 server_id,
572 )
573 .await
574 {
575 Ok(decision) => decision,
576 Err(e) => {
577 Log::new(Some(&status_tx)).warn(format!(
582 "SOCKS5 {} decision=fail_closed_llm_error target={} - denying: {}",
583 connection_id, target_addr, e
584 ));
585 (false, false)
586 }
587 }
588 }
589 (FilterMode::Selective, false) => {
590 let allow = config.default_action == "allow";
592 (allow, allow && config.mitm_by_default)
593 }
594 };
595
596 if !should_allow {
597 Log::new(Some(&status_tx)).warn(format!(
598 "SOCKS5 {} decision=deny target={} connection denied",
599 connection_id, target_addr
600 ));
601
602 Self::send_connect_reply(
604 &mut client_stream,
605 REPLY_CONNECTION_NOT_ALLOWED,
606 &target_addr,
607 )
608 .await?;
609 return Ok(());
610 }
611
612 let mut target_stream =
619 match Self::connect_to_target(&target_addr, connection_id, &status_tx).await {
620 Ok(stream) => stream,
621 Err(e) => {
622 let reply = socks5_reply_for_connect_error(&e);
623 Log::new(Some(&status_tx)).warn(format!(
624 "SOCKS5 {} connect to {} failed: {} (reply 0x{:02x})",
625 connection_id, target_addr, e, reply
626 ));
627 Self::send_connect_reply(&mut client_stream, reply, &target_addr).await?;
628 return Ok(());
629 }
630 };
631
632 Log::new(Some(&status_tx)).info(format!(
633 "SOCKS5 {} connected to {}",
634 connection_id, target_addr
635 ));
636
637 Self::send_connect_reply(&mut client_stream, REPLY_SUCCESS, &target_addr).await?;
639
640 Log::new(Some(&status_tx)).debug(format!(
642 "SOCKS5 {} phase 4: relay (MITM: {})",
643 connection_id, mitm_enabled
644 ));
645
646 if mitm_enabled {
647 Self::relay_with_mitm(
649 client_stream,
650 target_stream,
651 connection_id,
652 &target_addr,
653 username.as_deref(),
654 &llm_client,
655 &app_state,
656 &status_tx,
657 &protocol,
658 server_id,
659 relay_idle,
660 )
661 .await?;
662 } else {
663 let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
666 let mut client = TouchOnRead::new(&mut client_stream, Arc::clone(&activity));
667 let mut target = TouchOnRead::new(&mut target_stream, Arc::clone(&activity));
668 let outcome = tokio::select! {
669 result = tokio::io::copy_bidirectional(&mut client, &mut target) => Some(result),
670 _ = crate::server::accept_bounded::watch_idle(Arc::clone(&activity), relay_idle) => None,
671 };
672 let (client_to_target_bytes, target_to_client_bytes) =
675 (client.bytes_read, target.bytes_read);
676 match outcome {
677 Some(Ok(_)) => {
678 Log::new(Some(&status_tx)).info(format!(
679 "SOCKS5 {} relay complete: {}↑ {}↓",
680 connection_id, client_to_target_bytes, target_to_client_bytes
681 ));
682 }
683 Some(Err(e)) => {
684 Log::new(Some(&status_tx))
685 .warn(format!("SOCKS5 {} relay error: {}", connection_id, e));
686 }
687 None => {
688 Log::new(Some(&status_tx)).info(format!(
689 "SOCKS5 {} tunnel to {} moved nothing either way for {}s; closing \
690 decision=idle_timeout ({}↑ {}↓)",
691 connection_id,
692 target_addr,
693 relay_idle.as_secs(),
694 client_to_target_bytes,
695 target_to_client_bytes
696 ));
697 }
698 }
699 app_state
702 .update_connection_stats(
703 server_id,
704 connection_id,
705 Some(client_to_target_bytes),
706 Some(target_to_client_bytes),
707 None,
708 None,
709 )
710 .await;
711 }
712
713 Ok(())
714 }
715
716 async fn relay_with_mitm(
718 mut client_stream: TcpStream,
719 mut target_stream: TcpStream,
720 connection_id: ConnectionId,
721 target_addr: &TargetAddr,
722 username: Option<&str>,
723 llm_client: &OllamaClient,
724 app_state: &Arc<AppState>,
725 status_tx: &mpsc::UnboundedSender<String>,
726 protocol: &Arc<Socks5Protocol>,
727 server_id: ServerId,
728 relay_idle: std::time::Duration,
729 ) -> Result<()> {
730 use actions::{SOCKS5_DATA_FROM_TARGET_EVENT, SOCKS5_DATA_TO_TARGET_EVENT};
731
732 Log::new(Some(status_tx)).info(format!("SOCKS5 {} MITM relay active", connection_id));
733
734 let mut client_buf = vec![0u8; 8192];
735 let mut target_buf = vec![0u8; 8192];
736 let mut client_to_target_total = 0u64;
737 let mut target_to_client_total = 0u64;
738
739 let activity = Arc::new(crate::server::accept_bounded::ConnectionActivity::new());
742
743 loop {
744 tokio::select! {
745 _ = crate::server::accept_bounded::watch_idle(Arc::clone(&activity), relay_idle) => {
746 Log::new(Some(status_tx)).info(format!(
747 "SOCKS5 {} MITM tunnel to {} moved nothing either way for {}s; closing \
748 decision=idle_timeout",
749 connection_id,
750 target_addr,
751 relay_idle.as_secs()
752 ));
753 break;
754 }
755
756 result = client_stream.read(&mut client_buf) => {
758 match result {
759 Ok(0) => {
760 Log::new(Some(status_tx)).debug(format!("SOCKS5 {} client closed", connection_id));
761 break;
762 }
763 Ok(n) => {
764 let _busy = activity.busy();
765 let data = &client_buf[..n];
766 Log::new(Some(status_tx)).trace(format!("SOCKS5 {} client→target {} bytes: {:?}", connection_id, n, data));
767
768 let (data_str, encoding) = encode_relay_data(data);
773 let event = Event::new(&SOCKS5_DATA_TO_TARGET_EVENT, serde_json::json!({
774 "data": data_str,
775 "encoding": encoding,
776 "target": target_addr.to_string(),
777 "username": username,
778 }));
779
780 let execution_result = call_llm(
781 llm_client,
782 app_state,
783 server_id,
784 Some(connection_id),
785 &event,
786 protocol.as_ref(),
787 ).await?;
788
789 let mut should_close = false;
791 let mut data_to_send: Option<Vec<u8>> = Some(data.to_vec());
792
793 for result in &execution_result.protocol_results {
794 match result {
795 ActionResult::NoAction => {
796 }
798 ActionResult::Output(modified_data) => {
799 data_to_send = Some(modified_data.clone());
801 Log::new(Some(status_tx)).debug(format!("SOCKS5 {} data modified: {} → {} bytes",
802 connection_id, n, modified_data.len()));
803 }
804 ActionResult::CloseConnection => {
805 should_close = true;
806 data_to_send = None;
807 Log::new(Some(status_tx)).warn(format!("SOCKS5 {} LLM close request", connection_id));
808 }
809 _ => {}
810 }
811 }
812
813 if should_close {
814 break;
815 }
816
817 if let Some(data) = data_to_send {
819 target_stream.write_all(&data).await?;
820 target_stream.flush().await?;
821 client_to_target_total += data.len() as u64;
822 }
823 }
824 Err(e) => {
825 Log::new(Some(status_tx)).error(format!("SOCKS5 {} client read error: {}", connection_id, e));
826 break;
827 }
828 }
829 }
830
831 result = target_stream.read(&mut target_buf) => {
833 match result {
834 Ok(0) => {
835 Log::new(Some(status_tx)).debug(format!("SOCKS5 {} target closed", connection_id));
836 break;
837 }
838 Ok(n) => {
839 let _busy = activity.busy();
840 let data = &target_buf[..n];
841 Log::new(Some(status_tx)).trace(format!("SOCKS5 {} target→client {} bytes: {:?}", connection_id, n, data));
842
843 let (data_str, encoding) = encode_relay_data(data);
846 let event = Event::new(&SOCKS5_DATA_FROM_TARGET_EVENT, serde_json::json!({
847 "data": data_str,
848 "encoding": encoding,
849 "target": target_addr.to_string(),
850 "username": username,
851 }));
852
853 let execution_result = call_llm(
854 llm_client,
855 app_state,
856 server_id,
857 Some(connection_id),
858 &event,
859 protocol.as_ref(),
860 ).await?;
861
862 let mut should_close = false;
864 let mut data_to_send: Option<Vec<u8>> = Some(data.to_vec());
865
866 for result in &execution_result.protocol_results {
867 match result {
868 ActionResult::NoAction => {
869 }
871 ActionResult::Output(modified_data) => {
872 data_to_send = Some(modified_data.clone());
874 Log::new(Some(status_tx)).debug(format!("SOCKS5 {} data modified: {} → {} bytes",
875 connection_id, n, modified_data.len()));
876 }
877 ActionResult::CloseConnection => {
878 should_close = true;
879 data_to_send = None;
880 Log::new(Some(status_tx)).warn(format!("SOCKS5 {} LLM close request", connection_id));
881 }
882 _ => {}
883 }
884 }
885
886 if should_close {
887 break;
888 }
889
890 if let Some(data) = data_to_send {
892 client_stream.write_all(&data).await?;
893 client_stream.flush().await?;
894 target_to_client_total += data.len() as u64;
895 }
896 }
897 Err(e) => {
898 Log::new(Some(status_tx)).error(format!("SOCKS5 {} target read error: {}", connection_id, e));
899 break;
900 }
901 }
902 }
903 }
904 }
905
906 Log::new(Some(status_tx)).info(format!(
907 "SOCKS5 {} MITM relay complete: {}↑ {}↓",
908 connection_id, client_to_target_total, target_to_client_total
909 ));
910
911 Ok(())
912 }
913
914 async fn negotiate_auth(
916 stream: &mut TcpStream,
917 config: &Socks5FilterConfig,
918 connection_id: ConnectionId,
919 status_tx: &mpsc::UnboundedSender<String>,
920 ) -> Result<u8> {
921 let mut buf = [0u8; 2];
923 stream.read_exact(&mut buf).await?;
924
925 let version = buf[0];
926 let nmethods = buf[1];
927
928 Log::new(Some(status_tx)).trace(format!(
929 "SOCKS5 {} handshake: version={}, nmethods={}",
930 connection_id, version, nmethods
931 ));
932
933 if version != SOCKS5_VERSION {
934 bail!("Unsupported SOCKS version: {}", version);
935 }
936
937 if nmethods == 0 {
938 bail!("No authentication methods provided");
939 }
940
941 let mut methods = vec![0u8; nmethods as usize];
943 stream.read_exact(&mut methods).await?;
944
945 Log::new(Some(status_tx)).trace(format!(
946 "SOCKS5 {} client methods: {:?}",
947 connection_id, methods
948 ));
949
950 let selected_method = config
952 .auth_methods
953 .iter()
954 .find(|&&method| methods.contains(&method))
955 .copied()
956 .unwrap_or(AUTH_METHOD_NO_ACCEPTABLE);
957
958 let response = [SOCKS5_VERSION, selected_method];
960 stream.write_all(&response).await?;
961 stream.flush().await?;
962
963 if selected_method == AUTH_METHOD_NO_ACCEPTABLE {
964 bail!("No acceptable authentication methods");
965 }
966
967 Ok(selected_method)
968 }
969
970 async fn authenticate_username_password(
972 stream: &mut TcpStream,
973 connection_id: ConnectionId,
974 llm_client: &OllamaClient,
975 app_state: &Arc<AppState>,
976 status_tx: &mpsc::UnboundedSender<String>,
977 protocol: &Arc<Socks5Protocol>,
978 server_id: ServerId,
979 ) -> Result<String> {
980 let mut buf = [0u8; 1];
982 stream.read_exact(&mut buf).await?;
983
984 let auth_version = buf[0];
985 if auth_version != 0x01 {
986 bail!(
987 "Unsupported username/password auth version: {}",
988 auth_version
989 );
990 }
991
992 stream.read_exact(&mut buf).await?;
994 let ulen = buf[0] as usize;
995 let mut username_bytes = vec![0u8; ulen];
996 stream.read_exact(&mut username_bytes).await?;
997 let username = String::from_utf8_lossy(&username_bytes).to_string();
998
999 stream.read_exact(&mut buf).await?;
1001 let plen = buf[0] as usize;
1002 let mut password_bytes = vec![0u8; plen];
1003 stream.read_exact(&mut password_bytes).await?;
1004 let password = String::from_utf8_lossy(&password_bytes).to_string();
1005
1006 Log::new(Some(status_tx)).debug(format!(
1007 "SOCKS5 {} auth request: username={}",
1008 connection_id, username
1009 ));
1010
1011 let event = Event::new(
1013 &SOCKS5_AUTH_REQUEST_EVENT,
1014 serde_json::json!({
1015 "username": username,
1016 "password": password,
1017 }),
1018 );
1019
1020 let execution_result = match call_llm(
1023 llm_client,
1024 app_state,
1025 server_id,
1026 Some(connection_id),
1027 &event,
1028 protocol.as_ref(),
1029 )
1030 .await
1031 {
1032 Ok(result) => result,
1033 Err(e) => {
1034 Log::new(Some(status_tx)).warn(format!(
1035 "SOCKS5 {} decision=fail_closed_llm_error user={} auth rejected: {}",
1036 connection_id, username, e
1037 ));
1038 let _ = stream.write_all(&[0x01, 0x01]).await;
1039 let _ = stream.flush().await;
1040 bail!(
1041 "Authentication decision failed for user {}: {}",
1042 username,
1043 e
1044 );
1045 }
1046 };
1047
1048 let allowed_action = execution_result
1052 .raw_actions
1053 .iter()
1054 .any(|action| action.get("type").and_then(|v| v.as_str()) == Some("allow_socks5_auth"));
1055 let denied_action = execution_result
1056 .raw_actions
1057 .iter()
1058 .any(|action| action.get("type").and_then(|v| v.as_str()) == Some("deny_socks5_auth"));
1059 let auth_allowed = allowed_action && !denied_action;
1060
1061 let status = if auth_allowed { 0x00 } else { 0x01 };
1063 let response = [0x01, status];
1064 stream.write_all(&response).await?;
1065 stream.flush().await?;
1066
1067 if !auth_allowed {
1068 let decision = if denied_action {
1069 "model_reject"
1070 } else {
1071 "model_silent"
1072 };
1073 Log::new(Some(status_tx)).warn(format!(
1074 "SOCKS5 {} decision={} user={} auth rejected",
1075 connection_id, decision, username
1076 ));
1077 bail!("Authentication failed for user: {}", username);
1078 }
1079
1080 Log::new(Some(status_tx)).info(format!(
1081 "SOCKS5 {} authenticated as {}",
1082 connection_id, username
1083 ));
1084
1085 Ok(username)
1086 }
1087
1088 async fn parse_connect_request(
1090 stream: &mut TcpStream,
1091 connection_id: ConnectionId,
1092 status_tx: &mpsc::UnboundedSender<String>,
1093 ) -> Result<TargetAddr> {
1094 let mut buf = [0u8; 4];
1096 stream.read_exact(&mut buf).await?;
1097
1098 let version = buf[0];
1099 let cmd = buf[1];
1100 let _rsv = buf[2];
1101 let atyp = buf[3];
1102
1103 Log::new(Some(status_tx)).trace(format!(
1104 "SOCKS5 {} request: version={}, cmd=0x{:02x}, atyp=0x{:02x}",
1105 connection_id, version, cmd, atyp
1106 ));
1107
1108 if version != SOCKS5_VERSION {
1109 bail!("Unsupported SOCKS version: {}", version);
1110 }
1111
1112 if cmd != CMD_CONNECT {
1113 bail!(
1114 "Unsupported command: 0x{:02x} (only CONNECT supported)",
1115 cmd
1116 );
1117 }
1118
1119 let target_addr = match atyp {
1121 ATYP_IPV4 => {
1122 let mut addr = [0u8; 4];
1123 stream.read_exact(&mut addr).await?;
1124 let ip = Ipv4Addr::from(addr);
1125 let mut port_buf = [0u8; 2];
1126 stream.read_exact(&mut port_buf).await?;
1127 let port = u16::from_be_bytes(port_buf);
1128 TargetAddr::Ipv4(ip, port)
1129 }
1130 ATYP_DOMAIN => {
1131 let mut len_buf = [0u8; 1];
1132 stream.read_exact(&mut len_buf).await?;
1133 let len = len_buf[0] as usize;
1134 let mut domain_bytes = vec![0u8; len];
1135 stream.read_exact(&mut domain_bytes).await?;
1136 let domain = String::from_utf8_lossy(&domain_bytes).to_string();
1137 let mut port_buf = [0u8; 2];
1138 stream.read_exact(&mut port_buf).await?;
1139 let port = u16::from_be_bytes(port_buf);
1140 TargetAddr::Domain(domain, port)
1141 }
1142 ATYP_IPV6 => {
1143 let mut addr = [0u8; 16];
1144 stream.read_exact(&mut addr).await?;
1145 let ip = Ipv6Addr::from(addr);
1146 let mut port_buf = [0u8; 2];
1147 stream.read_exact(&mut port_buf).await?;
1148 let port = u16::from_be_bytes(port_buf);
1149 TargetAddr::Ipv6(ip, port)
1150 }
1151 _ => bail!("Unsupported address type: 0x{:02x}", atyp),
1152 };
1153
1154 Ok(target_addr)
1155 }
1156
1157 async fn send_connect_reply(
1159 stream: &mut TcpStream,
1160 reply_code: u8,
1161 target_addr: &TargetAddr,
1162 ) -> Result<()> {
1163 let mut response = vec![SOCKS5_VERSION, reply_code, 0x00];
1165
1166 match target_addr {
1168 TargetAddr::Ipv4(ip, port) => {
1169 response.push(ATYP_IPV4);
1170 response.extend_from_slice(&ip.octets());
1171 response.extend_from_slice(&port.to_be_bytes());
1172 }
1173 TargetAddr::Ipv6(ip, port) => {
1174 response.push(ATYP_IPV6);
1175 response.extend_from_slice(&ip.octets());
1176 response.extend_from_slice(&port.to_be_bytes());
1177 }
1178 TargetAddr::Domain(domain, port) => {
1179 response.push(ATYP_DOMAIN);
1180 response.push(domain.len() as u8);
1181 response.extend_from_slice(domain.as_bytes());
1182 response.extend_from_slice(&port.to_be_bytes());
1183 }
1184 }
1185
1186 stream.write_all(&response).await?;
1187 stream.flush().await?;
1188
1189 Ok(())
1190 }
1191
1192 async fn connect_to_target(
1194 target_addr: &TargetAddr,
1195 connection_id: ConnectionId,
1196 status_tx: &mpsc::UnboundedSender<String>,
1197 ) -> Result<TcpStream> {
1198 let target_str = target_addr.to_string();
1199
1200 Log::new(Some(status_tx)).debug(format!(
1201 "SOCKS5 {} connecting to {}",
1202 connection_id, target_str
1203 ));
1204
1205 let stream = TcpStream::connect(&target_str)
1206 .await
1207 .context(format!("Failed to connect to {}", target_str))?;
1208
1209 Ok(stream)
1210 }
1211
1212 fn check_filter_match(target_addr: &TargetAddr, config: &Socks5FilterConfig) -> bool {
1214 let target_host = match target_addr {
1215 TargetAddr::Ipv4(ip, _) => ip.to_string(),
1216 TargetAddr::Ipv6(ip, _) => ip.to_string(),
1217 TargetAddr::Domain(domain, _) => domain.clone(),
1218 };
1219
1220 let target_port = match target_addr {
1221 TargetAddr::Ipv4(_, port) => *port,
1222 TargetAddr::Ipv6(_, port) => *port,
1223 TargetAddr::Domain(_, port) => *port,
1224 };
1225
1226 let host_matches = if config.target_host_patterns.is_empty() {
1228 true
1229 } else {
1230 config.target_host_patterns.iter().any(|pattern| {
1231 if let Ok(re) = regex::Regex::new(pattern) {
1232 re.is_match(&target_host)
1233 } else {
1234 false
1235 }
1236 })
1237 };
1238
1239 let port_matches = if config.target_port_ranges.is_empty() {
1241 true
1242 } else {
1243 config
1244 .target_port_ranges
1245 .iter()
1246 .any(|(start, end)| target_port >= *start && target_port <= *end)
1247 };
1248
1249 host_matches && port_matches
1250 }
1251
1252 async fn ask_llm_for_decision(
1254 target_addr: &TargetAddr,
1255 username: Option<&str>,
1256 connection_id: ConnectionId,
1257 llm_client: &OllamaClient,
1258 app_state: &Arc<AppState>,
1259 status_tx: &mpsc::UnboundedSender<String>,
1260 protocol: &Arc<Socks5Protocol>,
1261 server_id: ServerId,
1262 ) -> Result<(bool, bool)> {
1263 Log::new(Some(status_tx))
1264 .debug(format!("SOCKS5 {} asking LLM for decision", connection_id));
1265
1266 let event = Event::new(
1267 &SOCKS5_CONNECT_REQUEST_EVENT,
1268 serde_json::json!({
1269 "target": target_addr.to_string(),
1270 "username": username,
1271 }),
1272 );
1273
1274 let execution_result = call_llm(
1275 llm_client,
1276 app_state,
1277 server_id,
1278 Some(connection_id),
1279 &event,
1280 protocol.as_ref(),
1281 )
1282 .await?;
1283
1284 let allow_action = execution_result.raw_actions.iter().find(|action| {
1291 action.get("type").and_then(|v| v.as_str()) == Some("allow_socks5_connect")
1292 });
1293 let denied = execution_result.raw_actions.iter().any(|action| {
1294 action.get("type").and_then(|v| v.as_str()) == Some("deny_socks5_connect")
1295 });
1296
1297 let allowed = allow_action.is_some() && !denied;
1298
1299 let mitm_enabled = allowed
1301 && allow_action
1302 .and_then(|action| action.get("mitm"))
1303 .and_then(|v| v.as_bool())
1304 .unwrap_or(false);
1305
1306 let decision = if denied {
1310 "model_reject"
1311 } else if allow_action.is_some() {
1312 "model_allow"
1313 } else {
1314 "model_silent"
1315 };
1316 Log::new(Some(status_tx)).info(format!(
1317 "SOCKS5 {} decision={} target={} allowed={} mitm={}",
1318 connection_id, decision, target_addr, allowed, mitm_enabled
1319 ));
1320
1321 Ok((allowed, mitm_enabled))
1322 }
1323}