Skip to main content

netget/server/inetd/
actions.rs

1//! The six classic inetd services as protocols sharing one engine. Each protocol type is a
2//! thin wrapper over [`Service`], which carries its metadata, event and action.
3use 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    /// Echo and Discard read from the client, so they have an idle deadline.
320    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
426/// The protocol surface every service shares, parameterised by the service.
427pub 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    // Literal ports, privilege first, so a source scan reads the declaration it checks.
454    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
639/// The protocol object for a service, for the shared engine's model calls.
640pub 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}