1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub fn decode_outbound_data(action: &serde_json::Value) -> Result<Vec<u8>> {
34 let data = action.get("data").and_then(|v| v.as_str());
35 let legacy = action.get("data_hex").and_then(|v| v.as_str());
36
37 match (data, legacy) {
38 (Some(_), Some(_)) => Err(anyhow::anyhow!(
39 "Both 'data' and 'data_hex' were supplied. Send exactly one: 'data' with an \
40 optional 'encoding' (\"utf8\" by default, or \"hex\"), or the deprecated \
41 'data_hex'. They are not combined and neither takes precedence."
42 )),
43 (Some(text), None) => decode_with_encoding(text, action),
44 (None, Some(h)) => decode_hex_field(h, "data_hex"),
45 (None, None) => Err(anyhow::anyhow!(
46 "Missing 'data' field. Put the payload in 'data' and, when it is binary, add \
47 \"encoding\": \"hex\"."
48 )),
49 }
50}
51
52fn decode_with_encoding(data: &str, action: &serde_json::Value) -> Result<Vec<u8>> {
53 match action
54 .get("encoding")
55 .and_then(|v| v.as_str())
56 .unwrap_or("utf8")
57 {
58 "utf8" => Ok(data.as_bytes().to_vec()),
59 "hex" => decode_hex_field(data, "data"),
60 other => Err(anyhow::anyhow!(
61 "Invalid 'encoding' value {other:?}. Valid values are \"utf8\" (default, send the \
62 string's characters as-is) and \"hex\" (decode the string as hex-encoded bytes)."
63 )),
64 }
65}
66
67fn decode_hex_field(value: &str, field: &str) -> Result<Vec<u8>> {
70 let cleaned: String = value
71 .chars()
72 .filter(|c| !c.is_ascii_whitespace() && *c != ':')
73 .collect();
74 let cleaned = cleaned.strip_prefix("0x").unwrap_or(&cleaned);
75
76 if cleaned.len() % 2 != 0 {
77 return Err(anyhow::anyhow!(
78 "Invalid hex in '{field}': expected an even number of hex digits, got {} \
79 ({value:?}). Each byte is two hex digits, e.g. \"48656c6c6f\" = \"Hello\".",
80 cleaned.len()
81 ));
82 }
83
84 hex::decode(cleaned).map_err(|e| {
85 anyhow::anyhow!(
86 "Invalid hex in '{field}' ({value:?}): {e}. Use only 0-9/a-f, two digits per byte, \
87 e.g. \"48656c6c6f\" = \"Hello\". To send this string as literal text, put it in \
88 'data' and omit 'encoding' (or set it to \"utf8\")."
89 )
90 })
91}
92
93pub fn inbound_event_fields(data: &[u8]) -> serde_json::Value {
101 let (text, encoding) = if data
102 .iter()
103 .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
104 {
105 (String::from_utf8_lossy(data).to_string(), "utf8")
106 } else {
107 (hex::encode(data), "hex")
108 };
109
110 json!({
111 "data": text,
112 "encoding": encoding,
113 "data_length": data.len(),
114 })
115}
116
117const SEND_DATA_DESCRIPTION: &str = "Send a payload through the established SOCKS5 tunnel to \
122 the target server. The 'data' field holds the payload and the optional 'encoding' field \
123 says how to turn it into bytes: omit 'encoding' (or use \"utf8\") to send the string's \
124 characters as-is, or set \"encoding\": \"hex\" to send 'data' decoded from hex. There \
125 is no auto-detection - a string like \"48656c6c6f\" is sent literally unless you set \
126 \"encoding\": \"hex\".";
127
128fn data_parameter() -> Parameter {
130 Parameter {
131 name: "data".to_string(),
132 type_hint: "string".to_string(),
133 description: "Payload to send through the SOCKS5 tunnel to the target. Interpreted \
134 according to 'encoding': as literal text by default, or as hex-encoded \
135 bytes when \"encoding\": \"hex\"."
136 .to_string(),
137 required: true,
138 }
139}
140
141fn encoding_parameter() -> Parameter {
143 Parameter {
144 name: "encoding".to_string(),
145 type_hint: "string".to_string(),
146 description: "How to convert 'data' into the bytes put on the wire. \"utf8\" (the \
147 default when omitted) sends the characters of 'data' unchanged - use it \
148 for text protocols such as HTTP. \"hex\" decodes 'data' as hex-encoded \
149 bytes, two hex digits per byte. There is no auto-detection: \
150 {\"data\": \"48656c6c6f\"} sends the 10 characters 4-8-6-5-6-c-6-c-6-f, \
151 and only {\"data\": \"48656c6c6f\", \"encoding\": \"hex\"} sends the \
152 5 bytes 'Hello'. No other values are accepted."
153 .to_string(),
154 required: false,
155 }
156 .with_choices(["utf8", "hex"])
157}
158
159pub static SOCKS5_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
161 EventType::new(
162 "socks5_connected",
163 "SOCKS5 client successfully connected through proxy",
164 json!({
165 "type": "send_socks5_data",
166 "data": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
167 "encoding": "utf8"
168 }),
169 )
170 .with_parameters(vec![
171 Parameter {
172 name: "proxy_addr".to_string(),
173 type_hint: "string".to_string(),
174 description: "SOCKS5 proxy server address".to_string(),
175 required: true,
176 },
177 Parameter {
178 name: "target_addr".to_string(),
179 type_hint: "string".to_string(),
180 description: "Target server address through proxy".to_string(),
181 required: true,
182 },
183 ])
184});
185
186pub static SOCKS5_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
188 EventType::new(
189 "socks5_data_received",
190 "Data received from target server through SOCKS5 proxy",
191 json!({
192 "type": "send_socks5_data",
193 "data": "GET /next HTTP/1.1\r\nHost: example.com\r\n\r\n",
194 "encoding": "utf8"
195 }),
196 )
197 .with_parameters(vec![
198 Parameter {
199 name: "data".to_string(),
200 type_hint: "string".to_string(),
201 description: "The data received from the target. Read it according to the \
202 'encoding' field of this event."
203 .to_string(),
204 required: true,
205 },
206 Parameter {
207 name: "encoding".to_string(),
208 type_hint: "string".to_string(),
209 description: "How to read 'data': \"utf8\" means 'data' is the received bytes as \
210 literal text, \"hex\" means it is those bytes hex-encoded (two hex \
211 digits per byte, used whenever they are not all printable ASCII). To \
212 send the same bytes back unchanged, pass this 'data' and 'encoding' \
213 straight to send_socks5_data."
214 .to_string(),
215 required: true,
216 },
217 Parameter {
218 name: "data_length".to_string(),
219 type_hint: "number".to_string(),
220 description: "Length of the received data in bytes, before any encoding".to_string(),
221 required: true,
222 },
223 ])
224});
225
226pub struct Socks5ClientProtocol;
228
229impl Socks5ClientProtocol {
230 pub fn new() -> Self {
231 Self
232 }
233}
234
235impl Protocol for Socks5ClientProtocol {
237 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
238 vec![
239 ParameterDefinition {
240 name: "target_addr".to_string(),
241 description: "Target server address to connect through SOCKS5 proxy (host:port)"
242 .to_string(),
243 type_hint: "string".to_string(),
244 required: true,
245 example: json!("example.com:80"),
246 default: None,
247 },
248 ParameterDefinition {
249 name: "auth_username".to_string(),
250 description: "Username for SOCKS5 authentication (optional)".to_string(),
251 type_hint: "string".to_string(),
252 required: false,
253 example: json!("user"),
254 default: None,
255 },
256 ParameterDefinition {
257 name: "auth_password".to_string(),
258 description: "Password for SOCKS5 authentication (optional)".to_string(),
259 type_hint: "string".to_string(),
260 required: false,
261 example: json!("password"),
262 default: None,
263 },
264 ]
265 }
266 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
267 vec![
268 ActionDefinition {
269 name: "send_socks5_data".to_string(),
270 description: SEND_DATA_DESCRIPTION.to_string(),
271 parameters: vec![data_parameter(), encoding_parameter()],
272 example: json!({
273 "type": "send_socks5_data",
274 "data": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
275 "encoding": "utf8"
276 }),
277 log_template: None,
278 },
279 ActionDefinition {
280 name: "disconnect".to_string(),
281 description: "Disconnect from SOCKS5 proxy and target server".to_string(),
282 parameters: vec![],
283 example: json!({
284 "type": "disconnect"
285 }),
286 log_template: None,
287 },
288 ]
289 }
290 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
291 vec![
292 ActionDefinition {
293 name: "send_socks5_data".to_string(),
294 description: format!(
295 "Send data through the SOCKS5 tunnel in response to data received from the \
296 target. {SEND_DATA_DESCRIPTION}"
297 ),
298 parameters: vec![data_parameter(), encoding_parameter()],
299 example: json!({
300 "type": "send_socks5_data",
301 "data": "GET /next HTTP/1.1\r\nHost: example.com\r\n\r\n",
302 "encoding": "utf8"
303 }),
304 log_template: None,
305 },
306 ActionDefinition {
307 name: "wait_for_more".to_string(),
308 description: "Wait for more data before responding".to_string(),
309 parameters: vec![],
310 example: json!({
311 "type": "wait_for_more"
312 }),
313 log_template: None,
314 },
315 ]
316 }
317 fn protocol_name(&self) -> &'static str {
318 "SOCKS5"
319 }
320 fn get_event_types(&self) -> Vec<EventType> {
329 vec![
330 SOCKS5_CLIENT_CONNECTED_EVENT.clone(),
331 SOCKS5_CLIENT_DATA_RECEIVED_EVENT.clone(),
332 ]
333 }
334 fn stack_name(&self) -> &'static str {
335 "ETH>IP>TCP>SOCKS5"
336 }
337 fn keywords(&self) -> Vec<&'static str> {
338 vec![
339 "socks5",
340 "socks",
341 "proxy",
342 "socks5 client",
343 "connect via socks5",
344 ]
345 }
346 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
347 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
348
349 ProtocolMetadataV2::builder()
350 .state(DevelopmentState::Experimental)
351 .implementation("tokio-socks library for SOCKS5 protocol")
352 .llm_control(
353 "Full control over target address, authentication, and data flow through proxy",
354 )
355 .e2e_testing("Dante or SS5 SOCKS5 server for testing")
356 .build()
357 }
358 fn description(&self) -> &'static str {
359 "SOCKS5 client for connecting to servers through a SOCKS5 proxy"
360 }
361 fn example_prompt(&self) -> &'static str {
362 "Connect to example.com:80 through SOCKS5 proxy at localhost:1080"
363 }
364 fn group_name(&self) -> &'static str {
365 "Proxy"
366 }
367 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
368 use crate::llm::actions::StartupExamples;
369 use serde_json::json;
370
371 StartupExamples::new(
372 json!({
374 "type": "open_client",
375 "remote_addr": "localhost:1080",
376 "base_stack": "socks5",
377 "startup_params": {
378 "target_addr": "example.com:80"
379 },
380 "instruction": "Send an HTTP GET request through the SOCKS5 tunnel"
381 }),
382 json!({
384 "type": "open_client",
385 "remote_addr": "localhost:1080",
386 "base_stack": "socks5",
387 "startup_params": {
388 "target_addr": "example.com:80"
389 },
390 "event_handlers": [{
391 "event_pattern": "socks5_data_received",
392 "handler": {
393 "type": "script",
394 "language": "python",
395 "code": "<socks5_client_handler>"
396 }
397 }]
398 }),
399 json!({
401 "type": "open_client",
402 "remote_addr": "localhost:1080",
403 "base_stack": "socks5",
404 "startup_params": {
405 "target_addr": "example.com:80"
406 },
407 "event_handlers": [
408 {
409 "event_pattern": "socks5_connected",
410 "handler": {
411 "type": "static",
412 "actions": [{
413 "type": "send_socks5_data",
414 "data": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
415 "encoding": "utf8"
416 }]
417 }
418 },
419 {
420 "event_pattern": "socks5_data_received",
421 "handler": {
422 "type": "static",
423 "actions": [{
424 "type": "disconnect"
425 }]
426 }
427 }
428 ]
429 }),
430 )
431 }
432}
433
434impl Client for Socks5ClientProtocol {
436 fn connect(
437 &self,
438 ctx: crate::protocol::ConnectContext,
439 ) -> std::pin::Pin<
440 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
441 > {
442 Box::pin(async move {
443 use crate::client::socks5::Socks5Client;
444 Socks5Client::connect_with_llm_actions(
445 ctx.remote_addr,
446 ctx.llm_client,
447 ctx.state,
448 ctx.status_tx,
449 ctx.client_id,
450 ctx.startup_params,
451 )
452 .await
453 })
454 }
455 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
456 let action_type = action
457 .get("type")
458 .and_then(|v| v.as_str())
459 .context("Missing 'type' field in action")?;
460
461 match action_type {
462 "send_socks5_data" => Ok(ClientActionResult::SendData(decode_outbound_data(&action)?)),
463 "disconnect" => Ok(ClientActionResult::Disconnect),
464 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
465 _ => Err(anyhow::anyhow!(
466 "Unknown SOCKS5 client action: {}",
467 action_type
468 )),
469 }
470 }
471}