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;
13
14pub struct TlsProtocol {}
16
17impl TlsProtocol {
18 pub fn new() -> Self {
19 Self {}
20 }
21}
22
23impl Protocol for TlsProtocol {
25 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
26 vec![
27 crate::llm::actions::ParameterDefinition {
28 name: "send_first".to_string(),
29 type_hint: "boolean".to_string(),
30 description: "Set true when the server speaks first after the handshake (a banner or prompt). It is the only way tls_connection_opened is raised: then nothing the peer sends is read until that event has been answered, a silent answer is logged as a missing greeting, and a failed one closes the connection. Left false (the default), no connect event is raised and a connection costs no model call until the client sends something.".to_string(),
31 required: false,
32 example: serde_json::json!(false),
33 default: None,
34 },
35 crate::llm::actions::ParameterDefinition {
36 name: "cert_path".to_string(),
37 type_hint: "string".to_string(),
38 description: "Path to TLS certificate file (PEM format). If not provided, a self-signed certificate will be generated.".to_string(),
39 required: false,
40 example: serde_json::json!("/path/to/cert.pem"),
41 default: None,
42 },
43 crate::llm::actions::ParameterDefinition {
44 name: "key_path".to_string(),
45 type_hint: "string".to_string(),
46 description: "Path to TLS private key file (PEM format). Required if cert_path is provided.".to_string(),
47 required: false,
48 example: serde_json::json!("/path/to/key.pem"),
49 default: None,
50 },
51 crate::llm::actions::ParameterDefinition {
55 name: "handshake_timeout_secs".to_string(),
56 type_hint: "number".to_string(),
57 description: "Seconds a peer that has opened a TCP socket may take to \
58 complete the TLS handshake. Default 60. Every real client \
59 sends ClientHello immediately and finishes in one \
60 round-trip, NetGet's own TLS client included, and nothing in \
61 this phase involves the model - so this one does not need to \
62 be generous."
63 .to_string(),
64 required: false,
65 example: serde_json::json!(60),
66 default: Some(serde_json::json!(super::HANDSHAKE_READ_TIMEOUT.as_secs())),
67 },
68 crate::llm::actions::ParameterDefinition {
69 name: "first_byte_timeout_secs".to_string(),
70 type_hint: "number".to_string(),
71 description: "Seconds a peer that has COMPLETED the handshake may send no \
72 application record before the server closes it. Default 300, \
73 matching the window a `manual` rule gives a human to answer \
74 one event. The peer is often NetGet's own TLS client, which \
75 handshakes inside connect() and then writes no application \
76 bytes until an action or [ send message ] says to. Lower it \
77 (60 was the old value) for a listener exposed to strangers."
78 .to_string(),
79 required: false,
80 example: serde_json::json!(300),
81 default: Some(serde_json::json!(super::FIRST_RECORD_READ_TIMEOUT.as_secs())),
82 },
83 crate::llm::actions::ParameterDefinition {
84 name: "idle_timeout_secs".to_string(),
85 type_hint: "number".to_string(),
86 description: "Seconds a connection that has already carried application \
87 data may go without a further record before the server \
88 closes it. Default 300. TLS is a carrier, so what counts as \
89 idle is a property of whatever rides on it - which is why \
90 this is yours to set. Time spent waiting on an answer of \
91 ours is not counted against the peer."
92 .to_string(),
93 required: false,
94 example: serde_json::json!(300),
95 default: Some(serde_json::json!(super::IDLE_AFTER_DATA_TIMEOUT.as_secs())),
96 },
97 ]
98 }
99 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
100 vec![]
101 }
102 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
103 vec![
104 send_tls_data_action(),
105 wait_for_more_action(),
106 close_this_connection_action(),
107 ]
108 }
109 fn protocol_name(&self) -> &'static str {
110 "TLS"
111 }
112 fn get_event_types(&self) -> Vec<EventType> {
113 get_tls_event_types()
114 }
115 fn stack_name(&self) -> &'static str {
116 "ETH>IP>TCP>TLS"
117 }
118 fn keywords(&self) -> Vec<&'static str> {
119 vec!["tls", "ssl", "secure", "encrypted"]
120 }
121 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
122 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
123
124 ProtocolMetadataV2::builder()
125 .state(DevelopmentState::Experimental)
126 .implementation("TLS transport layer using tokio-rustls; self-signed certificate by default, or cert_path/key_path")
127 .max_inbound_bytes(crate::server::tls::MAX_QUEUED_BYTES)
131 .llm_control("Full control over application protocol on top of TLS; text or hex payloads")
132 .e2e_testing("openssl s_client / native TLS client")
133 .notes("Generic TLS server for custom protocols - LLM implements application layer. No client certificate authentication (no mTLS)")
134 .build()
135 }
136 fn description(&self) -> &'static str {
137 "Generic TLS server for implementing custom encrypted protocols"
138 }
139 fn example_prompt(&self) -> &'static str {
140 "Listen on port 8443 via TLS; implement a simple chat protocol over encrypted connection"
141 }
142 fn group_name(&self) -> &'static str {
143 "Core"
144 }
145
146 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
147 use crate::llm::actions::StartupExamples;
148
149 let script = r#"import json, sys
151data = json.load(sys.stdin)
152event = data["event"]
153if data["event_type_id"] == "tls_data_received":
154 actions = [{"type": "send_tls_data", "data": "OK\r\n"}]
155else:
156 actions = []
157print(json.dumps({"actions": actions}))"#;
158
159 StartupExamples::new(
160 json!({
161 "type": "open_server",
162 "port": 8443,
163 "base_stack": "tls",
164 "instruction": "Secure TLS server for custom encrypted protocols"
165 }),
166 json!({
167 "type": "open_server",
168 "port": 8443,
169 "base_stack": "tls",
170 "event_handlers": [{
171 "event_pattern": "tls_data_received",
172 "handler": {
173 "type": "script",
174 "language": "python",
175 "code": script
176 }
177 }]
178 }),
179 json!({
180 "type": "open_server",
181 "port": 8443,
182 "base_stack": "tls",
183 "event_handlers": [
184 {
185 "event_pattern": "tls_connection_opened",
186 "handler": {
187 "type": "static",
188 "actions": [{
189 "type": "send_tls_data",
190 "data": "220 Welcome to secure server\r\n"
191 }]
192 }
193 },
194 {
195 "event_pattern": "tls_data_received",
196 "handler": {
197 "type": "static",
198 "actions": [{
199 "type": "send_tls_data",
200 "data": "OK\r\n"
201 }]
202 }
203 }
204 ]
205 }),
206 )
207 }
208}
209
210impl Server for TlsProtocol {
212 fn spawn(
213 &self,
214 ctx: crate::protocol::SpawnContext,
215 ) -> std::pin::Pin<
216 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
217 > {
218 Box::pin(async move {
219 let send_first = ctx
221 .startup_params
222 .as_ref()
223 .map(|p| p.get_optional_bool("send_first"))
224 .transpose()?
225 .flatten()
226 .unwrap_or(false);
227
228 let tls_config = if let Some(ref params) = ctx.startup_params {
230 let cert_path = params.get_optional_string("cert_path")?;
231 let key_path = params.get_optional_string("key_path")?;
232
233 match (cert_path, key_path) {
234 (Some(cert), Some(key)) => {
235 Some(crate::server::tls_cert_manager::load_tls_config_from_files(
237 &cert, &key,
238 )?)
239 }
240 (Some(_), None) | (None, Some(_)) => {
241 return Err(anyhow::anyhow!(
242 "Both cert_path and key_path must be provided together"
243 ));
244 }
245 (None, None) => None, }
247 } else {
248 None
249 };
250
251 let secs = |name: &str| -> anyhow::Result<Option<u64>> {
255 Ok(ctx
256 .startup_params
257 .as_ref()
258 .map(|p| p.get_optional_u64(name))
259 .transpose()?
260 .flatten())
261 };
262 let handshake_timeout_secs = secs("handshake_timeout_secs")?;
263 let first_byte_timeout_secs = secs("first_byte_timeout_secs")?;
264 let idle_timeout_secs = secs("idle_timeout_secs")?;
265
266 use crate::server::tls::TlsServer;
267 TlsServer::spawn_with_llm_actions(
268 ctx.legacy_listen_addr(),
269 ctx.llm_client,
270 ctx.state,
271 ctx.status_tx,
272 send_first,
273 ctx.server_id,
274 tls_config,
275 handshake_timeout_secs,
276 first_byte_timeout_secs,
277 idle_timeout_secs,
278 )
279 .await
280 })
281 }
282 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
283 let action_type = action
284 .get("type")
285 .and_then(|v| v.as_str())
286 .context("Missing 'type' field in action")?;
287
288 match action_type {
289 "send_tls_data" => self.execute_send_tls_data(action),
290 "wait_for_more" => Ok(ActionResult::WaitForMore),
291 "close_this_connection" => Ok(ActionResult::CloseConnection),
292 "close_connection" => Ok(ActionResult::CloseConnection),
298 _ => Err(anyhow::anyhow!("Unknown TLS action: {action_type}")),
299 }
300 }
301}
302
303impl TlsProtocol {
304 fn execute_send_tls_data(&self, action: serde_json::Value) -> Result<ActionResult> {
306 let data = action
307 .get("data")
308 .and_then(|v| v.as_str())
309 .context("Missing 'data' parameter")?;
310
311 Ok(ActionResult::Output(decode_outbound_data(data, &action)?))
312 }
313}
314
315fn decode_outbound_data(data: &str, action: &serde_json::Value) -> Result<Vec<u8>> {
329 let encoding = action
330 .get("encoding")
331 .and_then(|v| v.as_str())
332 .unwrap_or("utf8");
333
334 match encoding {
335 "utf8" => Ok(data.as_bytes().to_vec()),
336 "hex" => {
337 let cleaned: String = data
339 .chars()
340 .filter(|c| !c.is_ascii_whitespace() && *c != ':')
341 .collect();
342 let cleaned = cleaned.strip_prefix("0x").unwrap_or(&cleaned);
343
344 if cleaned.len() % 2 != 0 {
345 return Err(anyhow::anyhow!(
346 "Invalid hex in 'data': expected an even number of hex digits, got {} \
347 ({data:?}). Each byte is two hex digits, e.g. \"48656c6c6f\" = \"Hello\".",
348 cleaned.len()
349 ));
350 }
351
352 hex::decode(cleaned).map_err(|e| {
353 anyhow::anyhow!(
354 "Invalid hex in 'data' ({data:?}): {e}. Use only 0-9/a-f, two digits per \
355 byte, e.g. \"48656c6c6f\" = \"Hello\". To send this string as literal \
356 text instead, omit 'encoding' or set it to \"utf8\"."
357 )
358 })
359 }
360 other => Err(anyhow::anyhow!(
361 "Unknown 'encoding' value {other:?}. Valid values are \"utf8\" (default, send the \
362 string's bytes as-is) and \"hex\" (decode the string as hex first)."
363 )),
364 }
365}
366
367fn send_tls_data_action() -> ActionDefinition {
369 ActionDefinition {
370 name: "send_tls_data".to_string(),
371 description: "Send data over the current TLS connection (TLS encryption is applied automatically)".to_string(),
372 parameters: vec![
373 Parameter {
374 name: "data".to_string(),
375 type_hint: "string".to_string(),
376 description: "Payload to send. Interpreted according to 'encoding'.".to_string(),
377 required: true,
378 },
379 Parameter {
380 name: "encoding".to_string(),
381 type_hint: "string".to_string(),
382 description: "How to interpret 'data': \"utf8\" (default) sends the string's bytes verbatim; \"hex\" decodes it as hex first, so \"48656c6c6f\" sends the 5 bytes \"Hello\". Use \"hex\" for binary protocols - tls_data_received reports binary payloads as hex.".to_string(),
383 required: false,
384 },
385 ],
386 example: json!({
387 "type": "send_tls_data",
388 "data": "Hello over TLS\r\n"
389 }),
390 log_template: Some(
391 LogTemplate::new()
392 .with_info("-> TLS {data_len}B")
393 .with_debug("TLS send: {data_len} bytes"),
394 ),
395 }
396}
397
398fn wait_for_more_action() -> ActionDefinition {
400 ActionDefinition {
401 name: "wait_for_more".to_string(),
402 description: "Wait for more data before responding (accumulate incomplete protocol data)"
403 .to_string(),
404 parameters: vec![],
405 example: json!({
406 "type": "wait_for_more"
407 }),
408 log_template: Some(LogTemplate::new().with_debug("TLS waiting for more data")),
409 }
410}
411
412fn close_this_connection_action() -> ActionDefinition {
414 ActionDefinition {
415 name: "close_this_connection".to_string(),
416 description: "Close the current TLS connection".to_string(),
417 parameters: vec![],
418 example: json!({
419 "type": "close_this_connection"
420 }),
421 log_template: Some(LogTemplate::new().with_info("-> TLS connection closed")),
422 }
423}
424
425pub static SEND_TLS_DATA_ACTION: LazyLock<ActionDefinition> =
430 LazyLock::new(|| send_tls_data_action());
431pub static WAIT_FOR_MORE_ACTION: LazyLock<ActionDefinition> =
432 LazyLock::new(|| wait_for_more_action());
433pub static CLOSE_THIS_CONNECTION_ACTION: LazyLock<ActionDefinition> =
434 LazyLock::new(|| close_this_connection_action());
435
436pub static TLS_CONNECTION_OPENED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
442 EventType::new(
443 "tls_connection_opened",
444 "TLS handshake complete; the client has sent nothing yet. Raised only when the \
445 server was started with send_first, which means the client is owed a greeting: send \
446 the banner or prompt now with send_tls_data. Nothing the client sends is read until \
447 this is answered.",
448 json!({
449 "type": "send_tls_data",
450 "data": "220 Welcome to secure server\r\n"
451 }),
452 )
453 .with_actions(vec![
455 SEND_TLS_DATA_ACTION.clone(),
456 CLOSE_THIS_CONNECTION_ACTION.clone(),
457 ])
458 .with_parameters(crate::protocol::event_type::connect_event_parameters())
459 .with_log_template(
460 LogTemplate::new()
461 .with_info("{client_ip} TLS connected")
462 .with_debug("TLS handshake complete from {client_ip}"),
463 )
464});
465
466pub static TLS_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
468 EventType::new(
469 "tls_data_received",
470 "Data received on TLS connection (implement your application protocol here)",
471 json!({
472 "type": "send_tls_data",
473 "data": "OK Data received\r\n"
474 }),
475 )
476 .with_parameters(vec![
477 Parameter {
478 name: "data".to_string(),
479 type_hint: "string".to_string(),
480 description: "The data received. Printable payloads arrive as text; anything else arrives hex-encoded. Check 'encoding' to tell which.".to_string(),
481 required: true,
482 },
483 Parameter {
484 name: "encoding".to_string(),
485 type_hint: "string".to_string(),
486 description: "How 'data' is encoded: \"utf8\" (printable text) or \"hex\" (binary). To echo a hex payload back, pass the same encoding to send_tls_data.".to_string(),
487 required: true,
488 },
489 ])
490 .with_actions(vec![
491 SEND_TLS_DATA_ACTION.clone(),
492 WAIT_FOR_MORE_ACTION.clone(),
493 CLOSE_THIS_CONNECTION_ACTION.clone(),
494 ])
495 .with_log_template(
496 LogTemplate::new()
497 .with_info("{client_ip} TLS <- {data_len}B ({duration_ms}ms)")
498 .with_debug("TLS data from {client_ip}: {data_len} bytes")
499 .with_trace("TLS data: {data}"),
500 )
501});
502
503pub fn get_tls_event_types() -> Vec<EventType> {
505 vec![
506 TLS_CONNECTION_OPENED_EVENT.clone(),
507 TLS_DATA_RECEIVED_EVENT.clone(),
508 ]
509}