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 TorRelayProtocol;
17
18impl TorRelayProtocol {
19 pub fn new() -> Self {
20 Self
21 }
22
23 fn execute_detect_relay_cell(&self, action: serde_json::Value) -> Result<ActionResult> {
24 let message = action
25 .get("message")
26 .and_then(|v| v.as_str())
27 .context("Missing 'message' parameter")?;
28
29 debug!("Tor Relay: {}", message);
30
31 Ok(ActionResult::Custom {
32 name: "tor_relay_log".to_string(),
33 data: json!({
34 "logged": true,
35 "message": message
36 }),
37 })
38 }
39
40 fn execute_send_destroy(&self, action: serde_json::Value) -> Result<ActionResult> {
46 let circuit_id = action
47 .get("circuit_id")
48 .and_then(|v| v.as_str())
49 .context("Missing 'circuit_id' parameter")?;
50
51 let circuit_id = u32::from_str_radix(circuit_id.trim_start_matches("0x"), 16)
52 .with_context(|| format!("circuit_id '{}' is not a hex circuit ID", circuit_id))?;
53
54 let reason = action
55 .get("reason")
56 .and_then(|v| v.as_u64())
57 .unwrap_or(1 ) as u8;
58
59 debug!(
60 "Tor Relay sending DESTROY for circuit 0x{:08x} (reason {})",
61 circuit_id, reason
62 );
63
64 let mut cell = Vec::with_capacity(CELL_LEN);
65 cell.extend_from_slice(&circuit_id.to_be_bytes());
66 cell.push(CELL_COMMAND_DESTROY);
67 cell.push(reason);
68 cell.resize(CELL_LEN, 0);
69
70 Ok(ActionResult::Output(cell))
71 }
72}
73
74impl Protocol for TorRelayProtocol {
76 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
85 vec![]
86 }
87 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
88 tor_relay_response_actions()
89 }
90 fn protocol_name(&self) -> &'static str {
91 "Tor Relay"
92 }
93 fn get_event_types(&self) -> Vec<EventType> {
94 get_tor_relay_event_types()
95 }
96 fn stack_name(&self) -> &'static str {
97 "ETH>IP>TCP>TLS>TorRelay"
98 }
99 fn description(&self) -> &'static str {
100 "Partial Tor OR-protocol relay (not interoperable with real Tor clients)"
101 }
102 fn example_prompt(&self) -> &'static str {
103 "Start a Tor exit relay on port 9001 allowing connections to localhost"
104 }
105 fn keywords(&self) -> Vec<&'static str> {
106 vec![
107 "tor_relay",
108 "tor-relay",
109 "onion router",
110 "guard",
111 "exit",
112 "middle",
113 "circuit",
114 ]
115 }
116 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
117 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
118
119 ProtocolMetadataV2::builder()
120 .state(DevelopmentState::Experimental)
121 .well_known_port(9001)
122 .implementation("Custom partial Tor OR protocol: TLS 1.3, ntor server handshake (Curve25519/HMAC-SHA256/HKDF), per-circuit AES-128-CTR, stream multiplexing to TCP targets, SENDME windows. Cells are framed by length, so VERSIONS and other variable-length cells no longer desynchronise the reader, and VERSIONS is answered - but the rest of the link handshake (CERTS/AUTH_CHALLENGE/NETINFO) is still neither sent nor parsed. The relay logs its identity fingerprint and onion public key at startup, which a peer needs to run ntor at all.")
123 .llm_control("Two events: circuit created, and RELAY commands the relay does not implement. Actions: log, DESTROY a circuit, close the connection. Everything on the data path - BEGIN, DATA, END, SENDME, exit policy - is decided in Rust with no LLM involvement.")
124 .e2e_testing("tests/server/tor_relay/e2e_test.rs, 2 LLM calls, not #[ignore]d. A Tor client written from tor-spec in the test file (independent of the server's own circuit.rs) runs VERSIONS, then a full ntor CREATE2/CREATED2 whose server AUTH value it recomputes and checks, then RELAY/BEGIN to a localhost HTTP server, then RELAY/DATA out and back - asserting the HTTP body decrypts correctly with the backward key. Not tested: a real tor or Arti binary, which still cannot use this relay because the link handshake stops after VERSIONS; cell digests; EXTEND; multi-hop.")
125 .notes("OPEN EXIT BY DESIGN, AND UNRESTRICTED: the destination of every RELAY/BEGIN is chosen by the peer, and there is no exit policy, allow-list, deny-list or network restriction of any kind. Loopback, link-local (including 169.254.169.254, the cloud instance-metadata endpoint) and every RFC 1918 range are all reachable, so anyone who completes a circuit can reach whatever this host can -- an SSRF pivot into the operator's private network, and an exit someone else's traffic can be laundered through. The model is not consulted about it either: BEGIN, DATA, END, SENDME and exit target selection are all decided in Rust, so `filter_mode`-style gating does not exist here even in the weak form socks5 and proxy have. Two bounds do apply, and they are on resources rather than on destination: a BEGIN's connect is abandoned after 10s (it is awaited inline in the cell loop, so an unbounded one froze the whole session for the OS timeout), and a circuit may hold at most 64 streams, answered past that with END/RESOURCE_LIMIT. Do not expose this to an untrusted network. Separately, it is NOT interoperable with real Tor and is not a usable relay: the link handshake stops after VERSIONS (no CERTS/AUTH_CHALLENGE/NETINFO); the relay cell digest field is never computed or verified (tor-spec wants a running SHA-1, and no SHA-1 dependency is available here); no EXTEND, so it can only ever be a single-hop endpoint. Was rated Stable and 'production-ready' - it is neither.")
126 .build()
127 }
128 fn group_name(&self) -> &'static str {
129 "Network Services"
130 }
131
132 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
133 use crate::llm::actions::StartupExamples;
134 use serde_json::json;
135
136 let script = r#"import json, sys
138data = json.load(sys.stdin)
139event = data["event"]
140if data["event_type_id"] == "tor_relay_relay_cell":
141 actions = [{"type": "detect_relay_cell", "message": "relay cell observed"}]
142else:
143 actions = []
144print(json.dumps({"actions": actions}))"#;
145
146 StartupExamples::new(
147 json!({
149 "type": "open_server",
150 "port": 9001,
151 "base_stack": "tor-relay",
152 "instruction": "Act as Tor exit relay allowing connections to localhost"
153 }),
154 json!({
156 "type": "open_server",
157 "port": 9001,
158 "base_stack": "tor-relay",
159 "event_handlers": [{
160 "event_pattern": "tor_relay_relay_cell",
161 "handler": {
162 "type": "script",
163 "language": "python",
164 "code": script
165 }
166 }]
167 }),
168 json!({
170 "type": "open_server",
171 "port": 9001,
172 "base_stack": "tor-relay",
173 "event_handlers": [{
174 "event_pattern": "tor_relay_relay_cell",
175 "handler": {
176 "type": "static",
177 "actions": [{
178 "type": "detect_relay_cell",
179 "message": "Cell detected and logged"
180 }]
181 }
182 }]
183 }),
184 )
185 }
186}
187
188impl Server for TorRelayProtocol {
190 fn spawn(
191 &self,
192 ctx: crate::protocol::SpawnContext,
193 ) -> std::pin::Pin<
194 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
195 > {
196 Box::pin(async move {
197 use crate::server::tor_relay::TorRelayServer;
198 TorRelayServer::spawn_with_llm_actions(
199 ctx.legacy_listen_addr(),
200 ctx.llm_client,
201 ctx.state,
202 ctx.status_tx,
203 ctx.server_id,
204 )
205 .await
206 })
207 }
208 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
209 let action_type = action
210 .get("type")
211 .and_then(|v| v.as_str())
212 .context("Missing 'type' field in action")?;
213
214 match action_type {
215 "detect_relay_cell" => self.execute_detect_relay_cell(action),
216 "send_destroy" => self.execute_send_destroy(action),
217 "close_connection" => Ok(ActionResult::CloseConnection),
218 "tor_relay_log" => Ok(ActionResult::NoAction),
224 _ => Err(anyhow::anyhow!("Unknown Tor Relay action: {}", action_type)),
225 }
226 }
227}
228
229pub const CELL_LEN: usize = 514;
235pub const CELL_COMMAND_DESTROY: u8 = 4;
237
238fn detect_relay_cell_action() -> ActionDefinition {
243 ActionDefinition {
244 name: "detect_relay_cell".to_string(),
245 description: "Record an observation about this cell. Logs only - sends nothing on the \
246 wire."
247 .to_string(),
248 parameters: vec![Parameter {
249 name: "message".to_string(),
250 type_hint: "string".to_string(),
251 description: "Log message describing the cell".to_string(),
252 required: true,
253 }],
254 example: json!({
255 "type": "detect_relay_cell",
256 "message": "RELAY cell detected from circuit 0x12345"
257 }),
258 log_template: Some(
259 LogTemplate::new()
260 .with_info("-> Tor RELAY cell")
261 .with_debug("Tor detect_relay_cell: {message}"),
262 ),
263 }
264}
265
266fn send_destroy_action() -> ActionDefinition {
267 ActionDefinition {
268 name: "send_destroy".to_string(),
269 description: "Tear down a circuit by sending it a DESTROY cell".to_string(),
270 parameters: vec![
271 Parameter {
272 name: "circuit_id".to_string(),
273 type_hint: "string".to_string(),
274 description: "Circuit ID to destroy, hex (e.g. '0x00000005'). Use the \
275 circuit_id from the event."
276 .to_string(),
277 required: true,
278 },
279 Parameter {
280 name: "reason".to_string(),
281 type_hint: "number".to_string(),
282 description: "tor-spec destroy reason: 1 PROTOCOL, 2 INTERNAL, 3 REQUESTED, \
283 5 HIBERNATING, 8 FINISHED, 9 TIMEOUT (default 1)"
284 .to_string(),
285 required: false,
286 },
287 ],
288 example: json!({
289 "type": "send_destroy",
290 "circuit_id": "0x00000005",
291 "reason": 3
292 }),
293 log_template: Some(
294 LogTemplate::new()
295 .with_info("-> Tor DESTROY {circuit_id}")
296 .with_debug("Tor send_destroy: circuit={circuit_id} reason={reason}"),
297 ),
298 }
299}
300
301fn close_connection_action() -> ActionDefinition {
302 ActionDefinition {
303 name: "close_connection".to_string(),
304 description: "Close the whole TLS connection immediately, dropping every circuit on it"
305 .to_string(),
306 parameters: vec![],
307 example: json!({
308 "type": "close_connection"
309 }),
310 log_template: Some(
311 LogTemplate::new()
312 .with_info("-> Tor close connection")
313 .with_debug("Tor close_connection: closing immediately"),
314 ),
315 }
316}
317
318fn tor_relay_response_actions() -> Vec<ActionDefinition> {
323 vec![
324 detect_relay_cell_action(),
325 send_destroy_action(),
326 close_connection_action(),
327 tor_relay_log_action(),
328 ]
329}
330
331fn tor_relay_log_action() -> ActionDefinition {
343 ActionDefinition {
344 name: "tor_relay_log".to_string(),
345 description: "Record an observation about the circuit without sending anything. \
346 The correct answer when a cell needs noting rather than answering."
347 .to_string(),
348 parameters: vec![Parameter {
349 name: "message".to_string(),
350 type_hint: "string".to_string(),
351 description: "What to record".to_string(),
352 required: true,
353 }],
354 example: json!({
355 "type": "tor_relay_log",
356 "message": "circuit created"
357 }),
358 log_template: None,
359 }
360}
361
362pub static TOR_RELAY_CIRCUIT_CREATED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
368 EventType::new(
369 "tor_relay_circuit_created",
370 "Tor circuit created via ntor handshake",
371 json!({
372 "type": "detect_relay_cell",
373 "message": "Circuit created successfully"
374 }),
375 )
376 .with_parameters(vec![
377 Parameter {
378 name: "circuit_id".to_string(),
379 type_hint: "string".to_string(),
380 description: "Circuit ID (hex format)".to_string(),
381 required: true,
382 },
383 Parameter {
384 name: "client_ip".to_string(),
385 type_hint: "string".to_string(),
386 description: "Client IP address".to_string(),
387 required: true,
388 },
389 ])
390 .with_actions(tor_relay_response_actions())
391});
392
393pub static TOR_RELAY_RELAY_CELL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
397 EventType::new(
398 "tor_relay_relay_cell",
399 "Tor RELAY cell received carrying a command the relay does not implement",
400 json!({
401 "type": "detect_relay_cell",
402 "message": "EXTEND cell on circuit 0x00000005 - not supported, ignoring"
403 }),
404 )
405 .with_parameters(vec![
406 Parameter {
407 name: "circuit_id".to_string(),
408 type_hint: "string".to_string(),
409 description: "Circuit ID (hex format)".to_string(),
410 required: true,
411 },
412 Parameter {
413 name: "relay_command".to_string(),
414 type_hint: "string".to_string(),
415 description: "RELAY command name (EXTEND, TRUNCATE, RESOLVE, DROP, UNKNOWN, ...)"
416 .to_string(),
417 required: true,
418 },
419 Parameter {
420 name: "stream_id".to_string(),
421 type_hint: "number".to_string(),
422 description: "Stream ID within the circuit".to_string(),
423 required: true,
424 },
425 Parameter {
426 name: "length".to_string(),
427 type_hint: "number".to_string(),
428 description: "Length of RELAY cell data".to_string(),
429 required: true,
430 },
431 Parameter {
432 name: "client_ip".to_string(),
433 type_hint: "string".to_string(),
434 description: "Client IP address".to_string(),
435 required: true,
436 },
437 ])
438 .with_actions(tor_relay_response_actions())
439});
440
441pub fn get_tor_relay_event_types() -> Vec<EventType> {
442 vec![
443 TOR_RELAY_CIRCUIT_CREATED_EVENT.clone(),
444 TOR_RELAY_RELAY_CELL_EVENT.clone(),
445 ]
446}