1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13use tracing::debug;
14
15pub struct Socks5Protocol {}
17
18impl Socks5Protocol {
19 pub fn new() -> Self {
20 Self {}
21 }
22}
23
24impl Protocol for Socks5Protocol {
26 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
27 Vec::new()
32 }
33 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
34 vec![
35 allow_socks5_connect_action(),
36 deny_socks5_connect_action(),
37 allow_socks5_auth_action(),
38 deny_socks5_auth_action(),
39 forward_socks5_data_action(),
40 modify_socks5_data_action(),
41 close_connection_action(),
42 ]
43 }
44 fn protocol_name(&self) -> &'static str {
45 "SOCKS5"
46 }
47 fn get_event_types(&self) -> Vec<EventType> {
48 get_socks5_event_types()
49 }
50 fn stack_name(&self) -> &'static str {
51 "ETH>IP>TCP>SOCKS5"
52 }
53 fn keywords(&self) -> Vec<&'static str> {
54 vec!["socks", "socks5"]
55 }
56 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
57 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
58
59 ProtocolMetadataV2::builder()
60 .state(DevelopmentState::Experimental)
61 .well_known_port(1080)
62 .implementation("Manual SOCKS5 protocol (RFC 1928)")
63 .llm_control("Auth allow/deny, connection allow/deny, MITM data forward/modify/close")
64 .e2e_testing(
69 "A SOCKS5 client hand-written in tests/server/socks5/e2e_test.rs (raw TcpStream, \
70 RFC 1928/1929 by hand): no-auth and username/password handshakes, CONNECT by \
71 IPv4 and by domain, a refusal, and an HTTP exchange through a MITM tunnel. No \
72 third-party SOCKS5 client has been run against it.",
73 )
74 .notes(
75 "OPEN RELAY BY DESIGN, AND UNRESTRICTED: the destination of every connection is \
76 chosen by the peer, and there is no allow-list, deny-list or network \
77 restriction of any kind. Loopback, link-local (including 169.254.169.254, the \
78 cloud instance-metadata endpoint), and every RFC 1918 range are all reachable, \
79 so anyone who can reach this port can reach whatever this host can -- an SSRF \
80 pivot into the operator's private network, and a relay someone else's traffic \
81 can be laundered through. The only gate is the model: `filter_mode` decides \
82 whether it is consulted at all, and `allow_all` or a `selective` filter whose \
83 patterns miss (with `default_action: allow`) connects with no consultation \
84 whatsoever. `target_host_patterns` and `target_port_ranges` select what the \
85 model is ASKED about; they do not restrict anything on their own. Do not \
86 expose this to an untrusted network. \
87 CONNECT only (no BIND/UDP ASSOCIATE); a connect/auth event with no explicit \
88 allow action is denied; MITM inspection costs one LLM call per data chunk in \
89 each direction; the handshake reads time out after 30s but the number of \
90 concurrent connections is unbounded.",
91 )
92 .build()
93 }
94 fn description(&self) -> &'static str {
95 "SOCKS5 proxy server"
96 }
97 fn example_prompt(&self) -> &'static str {
98 "Start a SOCKS5 proxy on port 1080 that asks before connecting"
99 }
100 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
101 use crate::llm::actions::ParameterDefinition;
102 vec![
103 ParameterDefinition {
104 name: "auth_methods".to_string(),
105 type_hint: "array".to_string(),
106 description: "Array of allowed authentication methods: 'none' (no auth) or 'username_password' (RFC 1929)".to_string(),
107 required: false,
108 example: json!(["none", "username_password"]),
109 default: None,
110 },
111 ParameterDefinition {
112 name: "default_action".to_string(),
113 type_hint: "string".to_string(),
114 description: "Default action when no filter matches: 'allow' or 'deny'".to_string(),
115 required: false,
116 example: json!("allow"),
117 default: None,
118 },
119 ParameterDefinition {
120 name: "filter_mode".to_string(),
121 type_hint: "string".to_string(),
122 description: "Filter mode: 'allow_all', 'deny_all', 'ask_llm', or 'selective'".to_string(),
123 required: false,
124 example: json!("selective"),
125 default: None,
126 },
127 ParameterDefinition {
128 name: "filter".to_string(),
129 type_hint: "object".to_string(),
130 description: "Filter configuration object with 'target_host_patterns' (array of regex) and 'target_port_ranges' (array of [start, end])".to_string(),
131 required: false,
132 example: json!({
133 "target_host_patterns": [".*\\.example\\.com"],
134 "target_port_ranges": [[80, 80], [443, 443]]
135 }),
136 default: None,
137 },
138 ParameterDefinition {
139 name: "mitm_by_default".to_string(),
140 type_hint: "boolean".to_string(),
141 description: "Enable Man-in-the-Middle inspection for all allowed connections by default".to_string(),
142 required: false,
143 example: json!(false),
144 default: None,
145 },
146 ParameterDefinition {
147 name: "idle_timeout_secs".to_string(),
148 type_hint: "number".to_string(),
149 description: "Seconds an established tunnel may move no data in either \
150 direction before the proxy closes it (default 3600). Traffic \
151 either way keeps it open. Raise it for long-idle protocols \
152 such as SSH without keepalives."
153 .to_string(),
154 required: false,
155 example: json!(3600),
156 default: Some(serde_json::json!(super::RELAY_IDLE_TIMEOUT.as_secs())),
157 },
158 ]
159 }
160 fn group_name(&self) -> &'static str {
161 "Proxy & Network"
162 }
163
164 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
165 use crate::llm::actions::StartupExamples;
166
167 let script = r#"import json, sys
170data = json.load(sys.stdin)
171event = data["event"]
172et = data["event_type_id"]
173if et == "socks5_connect_request":
174 actions = [{"type": "allow_socks5_connect", "mitm": False}]
175elif et == "socks5_auth_request":
176 actions = [{"type": "allow_socks5_auth"}]
177else:
178 actions = []
179print(json.dumps({"actions": actions}))"#;
180
181 StartupExamples::new(
182 json!({
184 "type": "open_server",
185 "port": 1080,
186 "base_stack": "socks5",
187 "instruction": "SOCKS5 proxy server. Accept all connections without authentication. Allow all CONNECT requests to pass through."
188 }),
189 json!({
191 "type": "open_server",
192 "port": 1080,
193 "base_stack": "socks5",
194 "event_handlers": [{
195 "event_pattern": "socks5_connect_request",
196 "handler": {
197 "type": "script",
198 "language": "python",
199 "code": script
200 }
201 }, {
202 "event_pattern": "socks5_auth_request",
203 "handler": {
204 "type": "script",
205 "language": "python",
206 "code": script
207 }
208 }]
209 }),
210 json!({
212 "type": "open_server",
213 "port": 1080,
214 "base_stack": "socks5",
215 "event_handlers": [{
216 "event_pattern": "socks5_connect_request",
217 "handler": {
218 "type": "static",
219 "actions": [{
220 "type": "allow_socks5_connect",
221 "mitm": false
222 }]
223 }
224 }]
225 }),
226 )
227 }
228}
229
230impl Server for Socks5Protocol {
232 fn spawn(
233 &self,
234 ctx: crate::protocol::SpawnContext,
235 ) -> std::pin::Pin<
236 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
237 > {
238 Box::pin(async move {
239 use crate::server::socks5::Socks5Server;
240 Socks5Server::spawn_with_llm_actions(
241 ctx.legacy_listen_addr(),
242 ctx.llm_client,
243 ctx.state,
244 ctx.status_tx,
245 ctx.server_id,
246 ctx.startup_params,
247 )
248 .await
249 })
250 }
251 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
252 let action_type = action
253 .get("type")
254 .and_then(|v| v.as_str())
255 .context("Missing 'type' field in action")?;
256
257 match action_type {
258 "allow_socks5_connect" => self.execute_allow_connect(action),
259 "deny_socks5_connect" => self.execute_deny_connect(action),
260 "allow_socks5_auth" => self.execute_allow_auth(action),
261 "deny_socks5_auth" => self.execute_deny_auth(action),
262 "forward_socks5_data" => self.execute_forward_data(action),
263 "modify_socks5_data" => self.execute_modify_data(action),
264 "close_connection" => self.execute_close_connection(action),
265 _ => Err(anyhow::anyhow!("Unknown SOCKS5 action: {}", action_type)),
266 }
267 }
268}
269
270impl Socks5Protocol {
271 fn execute_allow_connect(&self, action: serde_json::Value) -> Result<ActionResult> {
272 let _mitm = action
273 .get("mitm")
274 .and_then(|v| v.as_bool())
275 .unwrap_or(false);
276
277 debug!("SOCKS5 allowing connection (MITM: {})", _mitm);
278
279 Ok(ActionResult::NoAction)
281 }
282
283 fn execute_deny_connect(&self, action: serde_json::Value) -> Result<ActionResult> {
284 let reason = action
285 .get("reason")
286 .and_then(|v| v.as_str())
287 .unwrap_or("Connection denied by policy");
288
289 debug!("SOCKS5 denying connection: {}", reason);
290
291 Ok(ActionResult::CloseConnection)
293 }
294
295 fn execute_allow_auth(&self, _action: serde_json::Value) -> Result<ActionResult> {
296 debug!("SOCKS5 allowing authentication");
297 Ok(ActionResult::NoAction)
298 }
299
300 fn execute_deny_auth(&self, action: serde_json::Value) -> Result<ActionResult> {
301 let reason = action
302 .get("reason")
303 .and_then(|v| v.as_str())
304 .unwrap_or("Authentication failed");
305
306 debug!("SOCKS5 denying authentication: {}", reason);
307 Ok(ActionResult::CloseConnection)
308 }
309
310 fn execute_forward_data(&self, _action: serde_json::Value) -> Result<ActionResult> {
311 debug!("SOCKS5 forwarding data as-is");
312 Ok(ActionResult::NoAction)
314 }
315
316 fn execute_modify_data(&self, action: serde_json::Value) -> Result<ActionResult> {
317 let modified_data = action
318 .get("data")
319 .and_then(|v| v.as_str())
320 .context("Missing 'data' field for modify_socks5_data action")?;
321
322 let bytes = decode_outbound_data(modified_data, &action)?;
326
327 debug!("SOCKS5 modifying data (new length: {} bytes)", bytes.len());
328
329 Ok(ActionResult::Output(bytes))
331 }
332
333 fn execute_close_connection(&self, action: serde_json::Value) -> Result<ActionResult> {
334 let reason = action
335 .get("reason")
336 .and_then(|v| v.as_str())
337 .unwrap_or("Connection closed by policy");
338
339 debug!("SOCKS5 closing connection: {}", reason);
340 Ok(ActionResult::CloseConnection)
341 }
342}
343
344fn decode_outbound_data(data: &str, action: &serde_json::Value) -> Result<Vec<u8>> {
353 let encoding = action
354 .get("encoding")
355 .and_then(|v| v.as_str())
356 .unwrap_or("utf8");
357
358 match encoding {
359 "utf8" => Ok(data.as_bytes().to_vec()),
360 "hex" => {
361 let cleaned: String = data
362 .chars()
363 .filter(|c| !c.is_ascii_whitespace() && *c != ':')
364 .collect();
365 let cleaned = cleaned.strip_prefix("0x").unwrap_or(&cleaned);
366
367 if cleaned.len() % 2 != 0 {
368 return Err(anyhow::anyhow!(
369 "Invalid hex in 'data': expected an even number of hex digits, got {} \
370 ({data:?}). Each byte is two hex digits, e.g. \"48656c6c6f\" = \"Hello\".",
371 cleaned.len()
372 ));
373 }
374
375 hex::decode(cleaned).map_err(|e| {
376 anyhow::anyhow!(
377 "Invalid hex in 'data' ({data:?}): {e}. Use only 0-9/a-f, two digits per \
378 byte. To relay this string as literal text instead, omit 'encoding' or \
379 set it to \"utf8\"."
380 )
381 })
382 }
383 other => Err(anyhow::anyhow!(
384 "Unknown 'encoding' value {other:?}. Valid values are \"utf8\" (default) and \"hex\"."
385 )),
386 }
387}
388
389fn allow_socks5_connect_action() -> ActionDefinition {
394 ActionDefinition {
395 name: "allow_socks5_connect".to_string(),
396 description: "Allow SOCKS5 CONNECT request to proceed".to_string(),
397 parameters: vec![Parameter {
398 name: "mitm".to_string(),
399 type_hint: "boolean".to_string(),
400 description: "Enable MITM inspection for this connection (default: false)".to_string(),
401 required: false,
402 }],
403 example: json!({
404 "type": "allow_socks5_connect",
405 "mitm": false
406 }),
407 log_template: Some(
408 LogTemplate::new()
409 .with_info("-> SOCKS5 allow (MITM={mitm})")
410 .with_debug("SOCKS5 allow_socks5_connect: mitm={mitm}"),
411 ),
412 }
413}
414
415fn deny_socks5_connect_action() -> ActionDefinition {
416 ActionDefinition {
417 name: "deny_socks5_connect".to_string(),
418 description: "Deny SOCKS5 CONNECT request".to_string(),
419 parameters: vec![Parameter {
420 name: "reason".to_string(),
421 type_hint: "string".to_string(),
422 description: "Reason for denial (for logging)".to_string(),
423 required: false,
424 }],
425 example: json!({
426 "type": "deny_socks5_connect",
427 "reason": "Blocked by security policy"
428 }),
429 log_template: Some(
430 LogTemplate::new()
431 .with_info("-> SOCKS5 deny: {reason}")
432 .with_debug("SOCKS5 deny_socks5_connect: reason={reason}"),
433 ),
434 }
435}
436
437fn allow_socks5_auth_action() -> ActionDefinition {
438 ActionDefinition {
439 name: "allow_socks5_auth".to_string(),
440 description: "Allow SOCKS5 username/password authentication".to_string(),
441 parameters: vec![],
442 example: json!({
443 "type": "allow_socks5_auth"
444 }),
445 log_template: Some(
446 LogTemplate::new()
447 .with_info("-> SOCKS5 auth allowed")
448 .with_debug("SOCKS5 allow_socks5_auth"),
449 ),
450 }
451}
452
453fn deny_socks5_auth_action() -> ActionDefinition {
454 ActionDefinition {
455 name: "deny_socks5_auth".to_string(),
456 description: "Deny SOCKS5 authentication".to_string(),
457 parameters: vec![Parameter {
458 name: "reason".to_string(),
459 type_hint: "string".to_string(),
460 description: "Reason for denial (for logging)".to_string(),
461 required: false,
462 }],
463 example: json!({
464 "type": "deny_socks5_auth",
465 "reason": "Invalid credentials"
466 }),
467 log_template: Some(
468 LogTemplate::new()
469 .with_info("-> SOCKS5 auth denied: {reason}")
470 .with_debug("SOCKS5 deny_socks5_auth: reason={reason}"),
471 ),
472 }
473}
474
475fn forward_socks5_data_action() -> ActionDefinition {
476 ActionDefinition {
477 name: "forward_socks5_data".to_string(),
478 description: "Forward SOCKS5 data without modification (MITM mode)".to_string(),
479 parameters: vec![],
480 example: json!({
481 "type": "forward_socks5_data"
482 }),
483 log_template: Some(
484 LogTemplate::new()
485 .with_info("-> SOCKS5 forward")
486 .with_debug("SOCKS5 forward_socks5_data"),
487 ),
488 }
489}
490
491fn modify_socks5_data_action() -> ActionDefinition {
492 ActionDefinition {
493 name: "modify_socks5_data".to_string(),
494 description: "Modify SOCKS5 data before forwarding (MITM mode)".to_string(),
495 parameters: vec![
496 Parameter {
497 name: "data".to_string(),
498 type_hint: "string".to_string(),
499 description: "Replacement payload. Interpreted according to 'encoding'.".to_string(),
500 required: true,
501 },
502 Parameter {
503 name: "encoding".to_string(),
504 type_hint: "string".to_string(),
505 description: "How to interpret 'data': \"utf8\" (default) relays the string's bytes verbatim; \"hex\" decodes it as hex first. Match the 'encoding' reported on the event to round-trip binary payloads.".to_string(),
506 required: false,
507 },
508 ],
509 example: json!({
510 "type": "modify_socks5_data",
511 "data": "Modified payload"
512 }),
513 log_template: Some(
514 LogTemplate::new()
515 .with_info("-> SOCKS5 modify data")
516 .with_debug("SOCKS5 modify_socks5_data"),
517 ),
518 }
519}
520
521fn close_connection_action() -> ActionDefinition {
522 ActionDefinition {
523 name: "close_connection".to_string(),
524 description: "Close the SOCKS5 connection (MITM mode)".to_string(),
525 parameters: vec![Parameter {
526 name: "reason".to_string(),
527 type_hint: "string".to_string(),
528 description: "Reason for closing (for logging)".to_string(),
529 required: false,
530 }],
531 example: json!({
532 "type": "close_connection",
533 "reason": "Suspicious data detected"
534 }),
535 log_template: Some(
536 LogTemplate::new()
537 .with_info("-> SOCKS5 close: {reason}")
538 .with_debug("SOCKS5 close_connection: reason={reason}"),
539 ),
540 }
541}
542
543pub static SOCKS5_AUTH_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
548 EventType::new(
549 "socks5_auth_request",
550 "SOCKS5 client authentication request (username/password)",
551 json!({
552 "type": "allow_socks5_auth"
553 }),
554 )
555 .with_parameters(vec![
556 Parameter {
557 name: "username".to_string(),
558 type_hint: "string".to_string(),
559 description: "Username provided by client".to_string(),
560 required: true,
561 },
562 Parameter {
563 name: "password".to_string(),
564 type_hint: "string".to_string(),
565 description: "Password provided by client".to_string(),
566 required: true,
567 },
568 ])
569 .with_actions(vec![allow_socks5_auth_action(), deny_socks5_auth_action()])
570});
571
572pub static SOCKS5_CONNECT_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
573 EventType::new(
574 "socks5_connect_request",
575 "SOCKS5 CONNECT request to target address",
576 json!({
577 "type": "allow_socks5_connect",
578 "mitm": false
579 }),
580 )
581 .with_parameters(vec![
582 Parameter {
583 name: "target".to_string(),
584 type_hint: "string".to_string(),
585 description: "Target address (IP or domain:port)".to_string(),
586 required: true,
587 },
588 Parameter {
589 name: "username".to_string(),
590 type_hint: "string".to_string(),
591 description: "Authenticated username (if any)".to_string(),
592 required: false,
593 },
594 ])
595 .with_actions(vec![
596 allow_socks5_connect_action(),
597 deny_socks5_connect_action(),
598 ])
599 .with_log_template(
600 LogTemplate::new()
601 .with_info("SOCKS5 {client_ip} -> {target}")
602 .with_debug("SOCKS5 connection from {client_ip}:{client_port} to {target}")
603 .with_trace("SOCKS5: {json_pretty(.)}"),
604 )
605});
606
607pub static SOCKS5_DATA_TO_TARGET_EVENT: LazyLock<EventType> = LazyLock::new(|| {
608 EventType::new(
609 "socks5_data_to_target",
610 "Data from client to target (MITM inspection mode)",
611 json!({
612 "type": "forward_socks5_data"
613 }),
614 )
615 .with_parameters(vec![
616 Parameter {
617 name: "data".to_string(),
618 type_hint: "string".to_string(),
619 description: "Data being sent from client to target. Printable payloads arrive as text, binary arrives hex-encoded; check 'encoding'.".to_string(),
620 required: true,
621 },
622 Parameter {
623 name: "encoding".to_string(),
624 type_hint: "string".to_string(),
625 description: "How 'data' is encoded: \"utf8\" or \"hex\". Pass the same value to modify_socks5_data when replacing the payload.".to_string(),
626 required: true,
627 },
628 Parameter {
629 name: "target".to_string(),
630 type_hint: "string".to_string(),
631 description: "Target address (IP or domain:port)".to_string(),
632 required: true,
633 },
634 Parameter {
635 name: "username".to_string(),
636 type_hint: "string".to_string(),
637 description: "Authenticated username (if any)".to_string(),
638 required: false,
639 },
640 ])
641 .with_actions(vec![
642 forward_socks5_data_action(),
643 modify_socks5_data_action(),
644 close_connection_action(),
645 ])
646});
647
648pub static SOCKS5_DATA_FROM_TARGET_EVENT: LazyLock<EventType> = LazyLock::new(|| {
649 EventType::new(
650 "socks5_data_from_target",
651 "Data from target to client (MITM inspection mode)",
652 json!({
653 "type": "forward_socks5_data"
654 }),
655 )
656 .with_parameters(vec![
657 Parameter {
658 name: "data".to_string(),
659 type_hint: "string".to_string(),
660 description: "Data being sent from target to client. Printable payloads arrive as text, binary arrives hex-encoded; check 'encoding'.".to_string(),
661 required: true,
662 },
663 Parameter {
664 name: "encoding".to_string(),
665 type_hint: "string".to_string(),
666 description: "How 'data' is encoded: \"utf8\" or \"hex\". Pass the same value to modify_socks5_data when replacing the payload.".to_string(),
667 required: true,
668 },
669 Parameter {
670 name: "target".to_string(),
671 type_hint: "string".to_string(),
672 description: "Target address (IP or domain:port)".to_string(),
673 required: true,
674 },
675 Parameter {
676 name: "username".to_string(),
677 type_hint: "string".to_string(),
678 description: "Authenticated username (if any)".to_string(),
679 required: false,
680 },
681 ])
682 .with_actions(vec![
683 forward_socks5_data_action(),
684 modify_socks5_data_action(),
685 close_connection_action(),
686 ])
687});
688
689pub fn get_socks5_event_types() -> Vec<EventType> {
690 vec![
691 SOCKS5_AUTH_REQUEST_EVENT.clone(),
692 SOCKS5_CONNECT_REQUEST_EVENT.clone(),
693 SOCKS5_DATA_TO_TARGET_EVENT.clone(),
694 SOCKS5_DATA_FROM_TARGET_EVENT.clone(),
695 ]
696}