1use super::wire;
4use crate::llm::actions::{
5 protocol_trait::{ActionResult, Protocol, Server},
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::{EventType, SpawnContext};
10use crate::state::app_state::AppState;
11use anyhow::{bail, ensure, Result};
12use serde_json::{json, Value};
13use std::sync::LazyLock;
14
15pub fn parameter(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
16 Parameter {
17 name: name.into(),
18 type_hint: kind.into(),
19 description: description.into(),
20 required,
21 }
22}
23
24pub fn action(
25 name: &str,
26 description: &str,
27 parameters: Vec<Parameter>,
28 example: Value,
29) -> ActionDefinition {
30 ActionDefinition {
31 name: name.into(),
32 description: description.into(),
33 parameters,
34 example,
35 log_template: Some(LogTemplate::new().with_info(format!("-> {name}"))),
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Service {
41 Echo,
42 Discard,
43 Daytime,
44 Qotd,
45 Chargen,
46 Time,
47}
48
49fn transport_parameter() -> Parameter {
50 parameter(
51 "transport",
52 "string",
53 "tcp or udp: how the request arrived",
54 true,
55 )
56}
57
58fn echo_action() -> ActionDefinition {
59 action(
60 "echo_reply",
61 "Send the echo. Omit data to echo the received bytes exactly, as RFC 862 defines; supply data to send something else.",
62 vec![
63 parameter("data", "string", "Bytes to send instead of the received ones", false),
64 parameter("encoding", "string", "Encoding of data: utf8 (default) or hex", false),
65 ],
66 json!({"type":"echo_reply"}),
67 )
68}
69fn discard_action() -> ActionDefinition {
70 action(
71 "discard_reply",
72 "Accept the connection and silently discard what it sends. To turn it away, use discard_refuse.",
73 vec![parameter("max_bytes", "number", "Close after discarding this many bytes; omit for no limit", false)],
74 json!({"type":"discard_reply"}),
75 )
76}
77fn daytime_action() -> ActionDefinition {
78 action(
79 "daytime_reply",
80 "Answer a daytime request with one line of human-readable date and time. Omit text to send the server clock.",
81 vec![parameter("text", "string", "The date and time line to send (at most 256 characters)", false)],
82 json!({"type":"daytime_reply"}),
83 )
84}
85fn qotd_action() -> ActionDefinition {
86 action(
87 "qotd_reply",
88 "Answer a quote-of-the-day request with a short message of at most 512 characters.",
89 vec![parameter(
90 "quote",
91 "string",
92 "The quote to send; newlines are kept",
93 true,
94 )],
95 json!({"type":"qotd_reply","quote":"Simplicity is prerequisite for reliability. - Dijkstra"}),
96 )
97}
98fn chargen_action() -> ActionDefinition {
99 action(
100 "chargen_reply",
101 "Start generating characters: the rotating RFC 864 pattern over the given character set, until the client closes or max_bytes is reached.",
102 vec![
103 parameter("charset", "string", "Printable ASCII characters to rotate through; default all 95", false),
104 parameter("line_length", "number", "Characters per line before CRLF, 1 to 512; default 72", false),
105 parameter("max_bytes", "number", "Stop and close after this many bytes; omit to stream until the client closes", false),
106 ],
107 json!({"type":"chargen_reply"}),
108 )
109}
110fn time_action() -> ActionDefinition {
111 action(
112 "time_reply",
113 "Answer a time request. Rust sends it as RFC 868's 32-bit seconds since 1900. Omit both fields to send the server clock.",
114 vec![
115 parameter("unix_seconds", "number", "The time to report, as seconds since 1970", false),
116 parameter("iso8601", "string", "The time to report, as an RFC 3339 timestamp", false),
117 ],
118 json!({"type":"time_reply"}),
119 )
120}
121
122fn refuse_action(service: Service) -> ActionDefinition {
123 let name = service.refuse_name();
124 action(
125 name,
126 "Deliberately send nothing: close the TCP connection, or leave the UDP datagram unanswered. Logged as a refusal, unlike silence.",
127 vec![parameter("reason", "string", "Why the request is refused, for the log only", false)],
128 json!({"type": name}),
129 )
130}
131
132pub static ECHO_EVENT: LazyLock<EventType> = LazyLock::new(|| {
133 EventType::new(
134 "echo_request",
135 "Data arrived for the echo service: one TCP read or one UDP datagram.",
136 echo_action().example.clone(),
137 )
138 .with_parameters(vec![
139 transport_parameter(),
140 parameter(
141 "data",
142 "string",
143 "The received bytes, as text or hex per encoding",
144 true,
145 ),
146 parameter(
147 "encoding",
148 "string",
149 "utf8 when the bytes are printable text, otherwise hex",
150 true,
151 ),
152 parameter("bytes", "number", "Number of bytes received", true),
153 ])
154 .with_actions(vec![echo_action(), refuse_action(Service::Echo)])
155});
156pub static DISCARD_EVENT: LazyLock<EventType> = LazyLock::new(|| {
157 EventType::new(
158 "discard_request",
159 "A client connected to the discard service.",
160 discard_action().example.clone(),
161 )
162 .with_parameters(vec![
163 transport_parameter(),
164 parameter(
165 "remote_addr",
166 "string",
167 "Address of the connecting client",
168 true,
169 ),
170 ])
171 .with_actions(vec![discard_action(), refuse_action(Service::Discard)])
172});
173pub static DAYTIME_EVENT: LazyLock<EventType> = LazyLock::new(|| {
174 EventType::new(
175 "daytime_request",
176 "A client asked for the date and time (a TCP connection or a UDP datagram).",
177 daytime_action().example.clone(),
178 )
179 .with_parameters(vec![transport_parameter()])
180 .with_actions(vec![daytime_action(), refuse_action(Service::Daytime)])
181});
182pub static QOTD_EVENT: LazyLock<EventType> = LazyLock::new(|| {
183 EventType::new(
184 "qotd_request",
185 "A client asked for the quote of the day (a TCP connection or a UDP datagram).",
186 qotd_action().example.clone(),
187 )
188 .with_parameters(vec![transport_parameter()])
189 .with_actions(vec![qotd_action(), refuse_action(Service::Qotd)])
190});
191pub static CHARGEN_EVENT: LazyLock<EventType> = LazyLock::new(|| {
192 EventType::new(
193 "chargen_request",
194 "A client asked for generated characters (a TCP connection or a UDP datagram).",
195 chargen_action().example.clone(),
196 )
197 .with_parameters(vec![transport_parameter()])
198 .with_actions(vec![chargen_action(), refuse_action(Service::Chargen)])
199});
200pub static TIME_EVENT: LazyLock<EventType> = LazyLock::new(|| {
201 EventType::new(
202 "time_request",
203 "A client asked for the machine-readable time (a TCP connection or a UDP datagram).",
204 time_action().example.clone(),
205 )
206 .with_parameters(vec![transport_parameter()])
207 .with_actions(vec![time_action(), refuse_action(Service::Time)])
208});
209
210impl Service {
211 pub const ALL: [Service; 6] = [
212 Service::Echo,
213 Service::Discard,
214 Service::Daytime,
215 Service::Qotd,
216 Service::Chargen,
217 Service::Time,
218 ];
219 pub fn name(self) -> &'static str {
220 match self {
221 Service::Echo => "Echo",
222 Service::Discard => "Discard",
223 Service::Daytime => "Daytime",
224 Service::Qotd => "QOTD",
225 Service::Chargen => "Chargen",
226 Service::Time => "Time",
227 }
228 }
229 pub fn feature_name(self) -> &'static str {
230 match self {
231 Service::Echo => "echo",
232 Service::Discard => "discard",
233 Service::Daytime => "daytime",
234 Service::Qotd => "qotd",
235 Service::Chargen => "chargen",
236 Service::Time => "time",
237 }
238 }
239 pub fn port(self) -> u16 {
240 match self {
241 Service::Echo => 7,
242 Service::Discard => 9,
243 Service::Daytime => 13,
244 Service::Qotd => 17,
245 Service::Chargen => 19,
246 Service::Time => 37,
247 }
248 }
249 pub fn stack_name(self) -> &'static str {
250 match self {
251 Service::Echo => "ETH>IP>TCP>ECHO",
252 Service::Discard => "ETH>IP>TCP>DISCARD",
253 Service::Daytime => "ETH>IP>TCP>DAYTIME",
254 Service::Qotd => "ETH>IP>TCP>QOTD",
255 Service::Chargen => "ETH>IP>TCP>CHARGEN",
256 Service::Time => "ETH>IP>TCP>TIME",
257 }
258 }
259 pub fn rfc(self) -> &'static str {
260 match self {
261 Service::Echo => "RFC 862",
262 Service::Discard => "RFC 863",
263 Service::Daytime => "RFC 867",
264 Service::Qotd => "RFC 865",
265 Service::Chargen => "RFC 864",
266 Service::Time => "RFC 868",
267 }
268 }
269 pub fn event_type(self) -> &'static EventType {
270 match self {
271 Service::Echo => &ECHO_EVENT,
272 Service::Discard => &DISCARD_EVENT,
273 Service::Daytime => &DAYTIME_EVENT,
274 Service::Qotd => &QOTD_EVENT,
275 Service::Chargen => &CHARGEN_EVENT,
276 Service::Time => &TIME_EVENT,
277 }
278 }
279 pub fn event(self) -> EventType {
280 match self {
281 Service::Echo => ECHO_EVENT.clone(),
282 Service::Discard => DISCARD_EVENT.clone(),
283 Service::Daytime => DAYTIME_EVENT.clone(),
284 Service::Qotd => QOTD_EVENT.clone(),
285 Service::Chargen => CHARGEN_EVENT.clone(),
286 Service::Time => TIME_EVENT.clone(),
287 }
288 }
289 pub fn refuse_name(self) -> &'static str {
290 match self {
291 Service::Echo => "echo_refuse",
292 Service::Discard => "discard_refuse",
293 Service::Daytime => "daytime_refuse",
294 Service::Qotd => "qotd_refuse",
295 Service::Chargen => "chargen_refuse",
296 Service::Time => "time_refuse",
297 }
298 }
299 pub fn reply_definition(self) -> ActionDefinition {
300 match self {
301 Service::Echo => echo_action(),
302 Service::Discard => discard_action(),
303 Service::Daytime => daytime_action(),
304 Service::Qotd => qotd_action(),
305 Service::Chargen => chargen_action(),
306 Service::Time => time_action(),
307 }
308 }
309 pub fn action_name(self) -> &'static str {
310 match self {
311 Service::Echo => "echo_reply",
312 Service::Discard => "discard_reply",
313 Service::Daytime => "daytime_reply",
314 Service::Qotd => "qotd_reply",
315 Service::Chargen => "chargen_reply",
316 Service::Time => "time_reply",
317 }
318 }
319 pub fn reads(self) -> bool {
321 matches!(self, Service::Echo | Service::Discard)
322 }
323 pub fn description(self) -> &'static str {
324 match self {
325 Service::Echo => "Echo service (RFC 862) over TCP and UDP; the handler may echo verbatim or answer differently",
326 Service::Discard => "Discard service (RFC 863) over TCP and UDP; the handler decides which TCP connections it keeps",
327 Service::Daytime => "Daytime service (RFC 867) over TCP and UDP; the handler chooses the date line",
328 Service::Qotd => "Quote of the Day service (RFC 865) over TCP and UDP; the handler writes the quote",
329 Service::Chargen => "Character Generator service (RFC 864) over TCP and UDP; the handler shapes the pattern",
330 Service::Time => "Time service (RFC 868) over TCP and UDP; the handler chooses the time reported",
331 }
332 }
333 pub fn keywords(self) -> Vec<&'static str> {
334 match self {
335 Service::Echo => vec!["echo service", "rfc862", "inetd echo"],
336 Service::Discard => vec!["discard", "rfc863", "inetd discard"],
337 Service::Daytime => vec!["daytime", "rfc867"],
338 Service::Qotd => vec!["qotd", "quote of the day", "rfc865"],
339 Service::Chargen => vec!["chargen", "character generator", "rfc864"],
340 Service::Time => vec!["time protocol", "rfc868", "rdate"],
341 }
342 }
343 pub fn llm_control(self) -> &'static str {
344 match self {
345 Service::Echo => {
346 "What is echoed for each TCP read or UDP datagram (verbatim by default)"
347 }
348 Service::Discard => {
349 "Whether each TCP connection is kept, and after how many bytes it is closed"
350 }
351 Service::Daytime => "The date and time line each request receives",
352 Service::Qotd => "The quote each request receives",
353 Service::Chargen => {
354 "The character set, line length and byte limit of the generated stream"
355 }
356 Service::Time => "The time each request is told",
357 }
358 }
359 pub fn validate(self, v: &Value) -> Result<()> {
360 match self {
361 Service::Echo => {
362 ensure!(
363 v.get("data").is_none_or(|d| d.is_null() || d.is_string()),
364 "data must be a string"
365 );
366 if let Some(data) = v["data"].as_str() {
367 let bytes = wire::decode(data, v["encoding"].as_str())?;
368 ensure!(
369 bytes.len() <= 4 * wire::READ_CHUNK,
370 "data exceeds {} bytes",
371 4 * wire::READ_CHUNK
372 );
373 }
374 }
375 Service::Discard => {
376 ensure!(
377 v.get("max_bytes").is_none_or(|m| m.is_null() || m.is_u64()),
378 "max_bytes must be a non-negative integer"
379 );
380 }
381 Service::Daytime => {
382 ensure!(
383 v.get("text").is_none_or(|t| t.is_null() || t.is_string()),
384 "text must be a string"
385 );
386 ensure!(
387 v["text"]
388 .as_str()
389 .is_none_or(|t| t.chars().count() <= wire::MAX_DAYTIME_CHARS),
390 "text exceeds 256 characters"
391 );
392 }
393 Service::Qotd => {
394 let quote = v["quote"]
395 .as_str()
396 .ok_or_else(|| anyhow::anyhow!("quote must be a string"))?;
397 ensure!(
398 quote.chars().count() <= wire::MAX_QUOTE_CHARS,
399 "quote exceeds 512 characters (RFC 865)"
400 );
401 }
402 Service::Chargen => {
403 wire::charset(v.get("charset"))?;
404 if let Some(length) = v.get("line_length").filter(|l| !l.is_null()) {
405 let length = length
406 .as_u64()
407 .ok_or_else(|| anyhow::anyhow!("line_length must be an integer"))?;
408 ensure!(
409 (1..=wire::MAX_CHARGEN_LINE as u64).contains(&length),
410 "line_length must be 1 to 512"
411 );
412 }
413 ensure!(
414 v.get("max_bytes").is_none_or(|m| m.is_null() || m.is_u64()),
415 "max_bytes must be a non-negative integer"
416 );
417 }
418 Service::Time => {
419 wire::requested_time(v)?;
420 }
421 }
422 Ok(())
423 }
424}
425
426pub fn startup_parameters(service: Service) -> Vec<ParameterDefinition> {
428 let mut params = vec![ParameterDefinition {
429 name: "transport".into(),
430 type_hint: "string".into(),
431 description: "tcp, udp, or both on the same port number".into(),
432 required: false,
433 example: json!("tcp"),
434 default: Some(json!(super::DEFAULT_TRANSPORT)),
435 }];
436 if service.reads() {
437 params.push(ParameterDefinition {
438 name: "idle_timeout_secs".into(),
439 type_hint: "number".into(),
440 description: "Seconds a TCP connection may stay silent before it is closed (1..=3600)"
441 .into(),
442 required: false,
443 example: json!(60),
444 default: Some(json!(super::IDLE_TIMEOUT.as_secs())),
445 });
446 }
447 params
448}
449
450pub fn metadata(service: Service) -> crate::protocol::metadata::ProtocolMetadataV2 {
451 use crate::protocol::metadata::*;
452 let builder = ProtocolMetadataV2::builder().state(DevelopmentState::Experimental);
453 let builder = match service {
455 Service::Echo => builder
456 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(7))
457 .well_known_port(7),
458 Service::Discard => builder
459 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(9))
460 .well_known_port(9),
461 Service::Daytime => builder
462 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(13))
463 .well_known_port(13),
464 Service::Qotd => builder
465 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(17))
466 .well_known_port(17),
467 Service::Chargen => builder
468 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(19))
469 .well_known_port(19),
470 Service::Time => builder
471 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(37))
472 .well_known_port(37),
473 };
474 let builder = builder
475 .implementation(match service {
476 Service::Echo => "RFC 862 over Tokio TCP and UDP: each TCP read or UDP datagram is one request",
477 Service::Discard => "RFC 863 over Tokio TCP and UDP: TCP data is read and dropped, UDP datagrams are dropped unanswered",
478 Service::Daytime => "RFC 867 over Tokio TCP and UDP: one line per TCP connection or UDP datagram",
479 Service::Qotd => "RFC 865 over Tokio TCP and UDP: one quote per TCP connection or UDP datagram",
480 Service::Chargen => "RFC 864 over Tokio TCP and UDP: the rotating 72-column pattern streamed on TCP, one line per UDP datagram",
481 Service::Time => "RFC 868 over Tokio TCP and UDP: a 32-bit big-endian count of seconds since 1900",
482 })
483 .llm_control(service.llm_control())
484 .e2e_testing("tests/server/inetd: raw TCP and UDP exchanges for all six; Perl Net::Ping and Net::Time, rdate and nc as independent clients")
485 .notes(match service {
486 Service::Echo => "A handler failure closes the TCP connection or leaves the datagram unanswered; it never echoes something it was not told to. 8 KiB per read or datagram; idle deadline; UDP handling is bounded to 64 requests in flight.",
487 Service::Discard => "UDP discard raises no event: nothing is decided and nothing is sent. A refusal or a handler failure closes the TCP connection.",
488 Service::Daytime => "A handler failure closes the TCP connection without a line and leaves a UDP request unanswered: no fabricated time.",
489 Service::Qotd => "A handler failure closes the TCP connection without a quote and leaves a UDP request unanswered.",
490 Service::Chargen => "TCP output streams until the client closes, a write stalls past 30 s, or max_bytes; client input is ignored. A UDP request gets one line of at most 512 characters. A handler failure sends nothing.",
491 Service::Time => "The 32-bit value wraps in 2036, as RFC 868's does. A handler failure sends nothing.",
492 })
493 .max_inbound_bytes(wire::READ_CHUNK);
494 let builder = if service == Service::Echo {
495 builder.request_only("Echo answers the data it received; it never sends first")
496 } else {
497 builder
498 };
499 builder.build()
500}
501
502pub fn startup_examples(service: Service) -> crate::llm::actions::StartupExamples {
503 let name = service.feature_name();
504 let llm = json!({"type":"open_server","base_stack":name,"port":10000 + service.port(),"instruction":format!("Run the {} service", service.name())});
505 let static_action = match service {
506 Service::Qotd => {
507 json!({"type":"qotd_reply","quote":"Simplicity is prerequisite for reliability."})
508 }
509 other => json!({"type": other.action_name()}),
510 };
511 let mut static_example = llm.clone();
512 static_example["event_handlers"] = json!([{"event_pattern": service.event().id, "handler": {"type":"static","actions":[static_action.clone()]}}]);
513 let mut scripted = llm.clone();
514 let code = format!(
515 "import json\nprint(json.dumps({{'actions':[{}]}}))",
516 static_action.to_string().replace("true", "True")
517 );
518 scripted["event_handlers"] = json!([{"event_pattern": service.event().id, "handler": {"type":"script","language":"python","code":code}}]);
519 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
520}
521
522pub fn execute(service: Service, v: Value) -> Result<ActionResult> {
523 match v["type"].as_str() {
524 Some(name) if name == service.action_name() => {
525 service.validate(&v)?;
526 Ok(ActionResult::Custom {
527 name: name.to_string(),
528 data: v,
529 })
530 }
531 Some(name) if name == service.refuse_name() => {
532 ensure!(
533 v.get("reason").is_none_or(|r| r.is_null() || r.is_string()),
534 "reason must be a string"
535 );
536 Ok(ActionResult::Custom {
537 name: name.to_string(),
538 data: v,
539 })
540 }
541 _ => bail!("Unknown {} server action", service.name()),
542 }
543}
544
545macro_rules! inetd_protocol {
546 ($ty:ident, $service:expr, $prompt:literal) => {
547 #[derive(Default)]
548 pub struct $ty;
549 impl $ty {
550 pub fn new() -> Self {
551 Self
552 }
553 }
554 impl Protocol for $ty {
555 fn protocol_name(&self) -> &'static str {
556 $service.name()
557 }
558 fn stack_name(&self) -> &'static str {
559 $service.stack_name()
560 }
561 fn description(&self) -> &'static str {
562 $service.description()
563 }
564 fn keywords(&self) -> Vec<&'static str> {
565 $service.keywords()
566 }
567 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
568 vec![]
569 }
570 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
571 vec![$service.reply_definition(), refuse_action($service)]
572 }
573 fn get_event_types(&self) -> Vec<EventType> {
574 vec![$service.event()]
575 }
576 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
577 startup_parameters($service)
578 }
579 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
580 metadata($service)
581 }
582 fn example_prompt(&self) -> &'static str {
583 $prompt
584 }
585 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
586 startup_examples($service)
587 }
588 fn group_name(&self) -> &'static str {
589 "Network Services"
590 }
591 }
592 impl Server for $ty {
593 fn spawn(
594 &self,
595 ctx: SpawnContext,
596 ) -> std::pin::Pin<
597 Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>,
598 > {
599 Box::pin(super::spawn(ctx, $service))
600 }
601 fn execute_action(&self, v: Value) -> Result<ActionResult> {
602 execute($service, v)
603 }
604 }
605 };
606}
607
608inetd_protocol!(
609 EchoProtocol,
610 Service::Echo,
611 "Echo service on port 10007 over TCP and UDP"
612);
613inetd_protocol!(
614 DiscardProtocol,
615 Service::Discard,
616 "Discard service on port 10009 that accepts every connection"
617);
618inetd_protocol!(
619 DaytimeProtocol,
620 Service::Daytime,
621 "Daytime service on port 10013 that always reports noon on 1 January 2000"
622);
623inetd_protocol!(
624 QotdProtocol,
625 Service::Qotd,
626 "Quote of the Day service on port 10017 with a different programming quote each time"
627);
628inetd_protocol!(
629 ChargenProtocol,
630 Service::Chargen,
631 "Character generator on port 10019 that stops after 10 KB"
632);
633inetd_protocol!(
634 TimeProtocol,
635 Service::Time,
636 "RFC 868 time server on port 10037"
637);
638
639pub fn protocol(service: Service) -> &'static dyn Server {
641 match service {
642 Service::Echo => &EchoProtocol,
643 Service::Discard => &DiscardProtocol,
644 Service::Daytime => &DaytimeProtocol,
645 Service::Qotd => &QotdProtocol,
646 Service::Chargen => &ChargenProtocol,
647 Service::Time => &TimeProtocol,
648 }
649}