Skip to main content

netget/server/inetd/
mod.rs

1//! The classic inetd services (RFC 862-868) over TCP and UDP, sharing one engine. Rust owns
2//! the framing, the clock arithmetic, the chargen pattern and every bound; handlers decide
3//! what each request receives. No handler failure ever produces fabricated output.
4pub mod actions;
5pub mod wire;
6
7use crate::llm::action_helper::call_llm;
8use crate::llm::actions::protocol_trait::ActionResult;
9use crate::logging::emit::Log;
10use crate::protocol::{Event, SpawnContext};
11use crate::server::connection::ConnectionId;
12use crate::state::server::{ConnectionState, ConnectionStatus, ProtocolConnectionInfo};
13use actions::Service;
14use anyhow::{Context, Result};
15use serde_json::{json, Value};
16use std::{net::SocketAddr, sync::Arc, time::Duration};
17use tokio::{
18    io::{AsyncReadExt, AsyncWriteExt},
19    net::{TcpListener, TcpStream, UdpSocket},
20};
21
22/// How long Echo and Discard wait for the client's next bytes.
23pub const IDLE_TIMEOUT: Duration = Duration::from_secs(300);
24pub const MAX_IDLE_TIMEOUT_SECS: u64 = 3600;
25pub const DEFAULT_TRANSPORT: &str = "both";
26/// RFC 864's customary line: 72 characters and CRLF.
27pub const DEFAULT_CHARGEN_LINE: usize = 72;
28/// UDP requests handled at once; datagrams beyond this are dropped.
29pub const MAX_UDP_IN_FLIGHT: usize = 64;
30/// Attempts to find one port number free on both TCP and UDP.
31const BOTH_BIND_ATTEMPTS: usize = 16;
32
33#[derive(Clone, Copy, PartialEq)]
34enum Transport {
35    Tcp,
36    Udp,
37    Both,
38}
39
40fn config(ctx: &SpawnContext, service: Service) -> Result<(Transport, Duration)> {
41    let params = ctx.startup_params.as_ref();
42    let transport = match params
43        .map(|p| p.get_optional_string("transport"))
44        .transpose()?
45        .flatten()
46        .as_deref()
47        .unwrap_or(DEFAULT_TRANSPORT)
48    {
49        "tcp" => Transport::Tcp,
50        "udp" => Transport::Udp,
51        "both" => Transport::Both,
52        other => anyhow::bail!("transport must be tcp, udp or both, not {other}"),
53    };
54    let idle = if service.reads() {
55        let secs = params
56            .map(|p| p.get_optional_u64("idle_timeout_secs"))
57            .transpose()?
58            .flatten()
59            .unwrap_or(IDLE_TIMEOUT.as_secs());
60        anyhow::ensure!(
61            (1..=MAX_IDLE_TIMEOUT_SECS).contains(&secs),
62            "idle_timeout_secs must be between 1 and {MAX_IDLE_TIMEOUT_SECS}"
63        );
64        Duration::from_secs(secs)
65    } else {
66        IDLE_TIMEOUT
67    };
68    Ok((transport, idle))
69}
70
71async fn bind(
72    requested: SocketAddr,
73    transport: Transport,
74) -> Result<(Option<TcpListener>, Option<UdpSocket>)> {
75    match transport {
76        Transport::Tcp => Ok((Some(TcpListener::bind(requested).await?), None)),
77        Transport::Udp => Ok((None, Some(UdpSocket::bind(requested).await?))),
78        Transport::Both => {
79            for _ in 0..BOTH_BIND_ATTEMPTS {
80                let tcp = TcpListener::bind(requested).await?;
81                let port = tcp.local_addr()?.port();
82                match UdpSocket::bind(SocketAddr::new(requested.ip(), port)).await {
83                    Ok(udp) => return Ok((Some(tcp), Some(udp))),
84                    // An explicit port is never replaced; an OS-assigned one is tried again.
85                    Err(e) if requested.port() != 0 => return Err(e.into()),
86                    Err(_) => continue,
87                }
88            }
89            anyhow::bail!(
90                "no port number was free on both TCP and UDP after {BOTH_BIND_ATTEMPTS} attempts"
91            )
92        }
93    }
94}
95
96pub async fn spawn(ctx: SpawnContext, service: Service) -> Result<SocketAddr> {
97    let (transport, idle) = config(&ctx, service)?;
98    let (tcp, udp) = bind(ctx.legacy_listen_addr(), transport).await?;
99    let addr = match (&tcp, &udp) {
100        (Some(tcp), _) => tcp.local_addr()?,
101        (None, Some(udp)) => udp.local_addr()?,
102        _ => unreachable!("bind returns at least one socket"),
103    };
104    Log::new(Some(&ctx.status_tx)).info(format!(
105        "{} listening on {addr} ({})",
106        service.name(),
107        match transport {
108            Transport::Tcp => "tcp",
109            Transport::Udp => "udp",
110            Transport::Both => "tcp and udp",
111        }
112    ));
113    if let Some(listener) = tcp {
114        let child = ctx.clone();
115        let accept = tokio::spawn(accept_loop(child, service, listener, addr, idle));
116        ctx.state.register_server_task(ctx.server_id, accept).await;
117    }
118    if let Some(socket) = udp {
119        let child = ctx.clone();
120        let datagrams = tokio::spawn(udp_loop(child, service, Arc::new(socket)));
121        ctx.state
122            .register_server_task(ctx.server_id, datagrams)
123            .await;
124    }
125    Ok(addr)
126}
127
128async fn accept_loop(
129    ctx: SpawnContext,
130    service: Service,
131    listener: TcpListener,
132    addr: SocketAddr,
133    idle: Duration,
134) {
135    let limiter = crate::server::accept_bounded::ConnectionLimiter::new(
136        crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS,
137    );
138    let server_id = ctx.server_id;
139    loop {
140        let (socket, peer, permit) = match crate::server::accept_bounded::accept_bounded(
141            &listener,
142            &limiter,
143            b"",
144            service.name(),
145            Some(&ctx.status_tx),
146        )
147        .await
148        {
149            Ok(v) => v,
150            Err(_) => break,
151        };
152        let id = ConnectionId::new(ctx.state.get_next_unified_id().await);
153        let now = crate::utils::clock::Instant::now();
154        ctx.state
155            .add_connection_to_server(
156                server_id,
157                ConnectionState {
158                    id,
159                    remote_addr: peer,
160                    local_addr: addr,
161                    bytes_sent: 0,
162                    bytes_received: 0,
163                    packets_sent: 0,
164                    packets_received: 0,
165                    last_activity: now,
166                    status: ConnectionStatus::Active,
167                    status_changed_at: now,
168                    protocol_info: ProtocolConnectionInfo::empty(),
169                },
170            )
171            .await;
172        let child = ctx.clone();
173        ctx.state
174            .spawn_server_task(server_id, async move {
175                let _permit = permit;
176                if let Err(e) = tcp_session(&child, id, service, socket, peer, idle).await {
177                    Log::new(Some(&child.status_tx))
178                        .warn(format!("{} connection {id} ended: {e}", service.name()));
179                }
180                child
181                    .state
182                    .update_connection_status(server_id, id, ConnectionStatus::Closed)
183                    .await;
184                let _ = child.status_tx.send("__UPDATE_UI__".into());
185            })
186            .await;
187    }
188}
189
190fn outcome(ctx: &SpawnContext, service: Service, id: Option<ConnectionId>, decision: &str) {
191    let who = id
192        .map(|id| format!("connection {id}"))
193        .unwrap_or_else(|| "datagram".into());
194    let summary = format!(
195        "{} {who} operation={} decision={decision}",
196        service.name(),
197        service.event().id
198    );
199    let log = Log::new(Some(&ctx.status_tx));
200    if decision.starts_with("fail_closed_") || decision == "model_silent" {
201        log.error(summary);
202    } else {
203        log.info(summary);
204    }
205}
206
207/// `Some(answer)` for the service's reply, `None` for a deliberate refusal, `Err` for a failure
208/// or silence. Both of the last two send nothing; only the log tells them apart.
209async fn decision(
210    ctx: &SpawnContext,
211    service: Service,
212    id: Option<ConnectionId>,
213    data: Value,
214) -> Result<Option<Value>> {
215    let event = Event::new(service.event_type(), data);
216    let result = match call_llm(
217        &ctx.llm_client,
218        &ctx.state,
219        ctx.server_id,
220        id,
221        &event,
222        actions::protocol(service),
223    )
224    .await
225    {
226        Ok(result) => result,
227        Err(error) => {
228            outcome(ctx, service, id, "fail_closed_llm_error");
229            return Err(error);
230        }
231    };
232    if !result.failures.is_empty() {
233        outcome(ctx, service, id, "fail_closed_invalid_reply");
234        anyhow::bail!("{} handler supplied an invalid action", service.name());
235    }
236    let mut found = None;
237    let mut pending = result.protocol_results;
238    while let Some(result) = pending.pop() {
239        match result {
240            ActionResult::Custom { name, data }
241                if name == service.action_name() || name == service.refuse_name() =>
242            {
243                if found.is_some() {
244                    outcome(ctx, service, id, "fail_closed_invalid_reply");
245                    anyhow::bail!("Multiple {} answers to one request", service.name());
246                }
247                found = Some((name == service.refuse_name(), data));
248            }
249            ActionResult::Multiple(items) => pending.extend(items),
250            _ => {}
251        }
252    }
253    match found {
254        Some((true, _)) => {
255            outcome(ctx, service, id, "model_reject");
256            Ok(None)
257        }
258        Some((false, answer)) => {
259            outcome(ctx, service, id, "model_answer");
260            Ok(Some(answer))
261        }
262        None => {
263            outcome(ctx, service, id, "model_silent");
264            anyhow::bail!("Handler did not answer the {} request", service.name())
265        }
266    }
267}
268
269async fn send(
270    ctx: &SpawnContext,
271    id: ConnectionId,
272    w: &mut (impl tokio::io::AsyncWrite + Unpin),
273    bytes: &[u8],
274) -> Result<()> {
275    tokio::time::timeout(wire::IO_TIMEOUT, w.write_all(bytes))
276        .await
277        .context("write deadline")??;
278    ctx.state
279        .update_connection_stats(
280            ctx.server_id,
281            id,
282            None,
283            Some(bytes.len() as u64),
284            None,
285            Some(1),
286        )
287        .await;
288    Ok(())
289}
290
291/// The bytes a stream service (Daytime, QOTD, Time) sends for a handler's answer.
292fn single_reply(service: Service, answer: &Value) -> Result<Vec<u8>> {
293    Ok(match service {
294        Service::Daytime => {
295            let text = match answer["text"].as_str() {
296                Some(text) => wire::line(text, wire::MAX_DAYTIME_CHARS),
297                None => wire::daytime_now(),
298            };
299            format!("{text}\r\n").into_bytes()
300        }
301        Service::Qotd => {
302            let quote: String = answer["quote"]
303                .as_str()
304                .unwrap_or_default()
305                .lines()
306                .map(crate::utils::sanitize::strip_controls)
307                .collect::<Vec<_>>()
308                .join("\n")
309                .chars()
310                .take(wire::MAX_QUOTE_CHARS)
311                .collect();
312            format!("{}\r\n", quote.replace('\n', "\r\n")).into_bytes()
313        }
314        Service::Time => wire::time_bytes(wire::requested_time(answer)?).to_vec(),
315        _ => anyhow::bail!("{} is not a single-reply service", service.name()),
316    })
317}
318
319struct Chargen {
320    charset: Vec<u8>,
321    width: usize,
322    max_bytes: Option<u64>,
323}
324
325fn chargen(answer: &Value) -> Result<Chargen> {
326    Ok(Chargen {
327        charset: wire::charset(answer.get("charset"))?.into_bytes(),
328        width: answer["line_length"]
329            .as_u64()
330            .map(|w| w as usize)
331            .unwrap_or(DEFAULT_CHARGEN_LINE),
332        max_bytes: answer["max_bytes"].as_u64(),
333    })
334}
335
336async fn tcp_session(
337    ctx: &SpawnContext,
338    id: ConnectionId,
339    service: Service,
340    socket: TcpStream,
341    peer: SocketAddr,
342    idle: Duration,
343) -> Result<()> {
344    let (mut reader, mut writer) = tokio::io::split(socket);
345    match service {
346        Service::Echo => {
347            let mut buf = vec![0u8; wire::READ_CHUNK];
348            loop {
349                let n = tokio::time::timeout(idle, reader.read(&mut buf))
350                    .await
351                    .context("idle deadline")??;
352                if n == 0 {
353                    return Ok(());
354                }
355                ctx.state
356                    .update_connection_stats(ctx.server_id, id, Some(n as u64), None, Some(1), None)
357                    .await;
358                let (data, encoding) = wire::encode(&buf[..n]);
359                let answer = match decision(
360                    ctx,
361                    service,
362                    Some(id),
363                    json!({"transport":"tcp","data":data,"encoding":encoding,"bytes":n}),
364                )
365                .await
366                {
367                    Ok(Some(answer)) => answer,
368                    // Nothing is echoed that the handler did not approve: half-close instead.
369                    Ok(None) | Err(_) => {
370                        let _ = writer.shutdown().await;
371                        return Ok(());
372                    }
373                };
374                let reply = match answer["data"].as_str() {
375                    Some(data) => wire::decode(data, answer["encoding"].as_str())?,
376                    None => buf[..n].to_vec(),
377                };
378                send(ctx, id, &mut writer, &reply).await?;
379            }
380        }
381        Service::Discard => {
382            let Ok(Some(answer)) = decision(
383                ctx,
384                service,
385                Some(id),
386                json!({"transport":"tcp","remote_addr":peer.to_string()}),
387            )
388            .await
389            else {
390                return Ok(());
391            };
392            let limit = answer["max_bytes"].as_u64();
393            let mut buf = vec![0u8; wire::READ_CHUNK];
394            let mut total: u64 = 0;
395            loop {
396                let n = tokio::time::timeout(idle, reader.read(&mut buf))
397                    .await
398                    .context("idle deadline")??;
399                if n == 0 {
400                    return Ok(());
401                }
402                total += n as u64;
403                ctx.state
404                    .update_connection_stats(ctx.server_id, id, Some(n as u64), None, Some(1), None)
405                    .await;
406                if limit.is_some_and(|limit| total >= limit) {
407                    let _ = writer.shutdown().await;
408                    return Ok(());
409                }
410            }
411        }
412        Service::Daytime | Service::Qotd | Service::Time => {
413            // Silence on failure: closing without a line is the honest answer.
414            if let Ok(Some(answer)) =
415                decision(ctx, service, Some(id), json!({"transport":"tcp"})).await
416            {
417                send(ctx, id, &mut writer, &single_reply(service, &answer)?).await?;
418            }
419            let _ = writer.shutdown().await;
420            Ok(())
421        }
422        Service::Chargen => {
423            let Ok(Some(answer)) =
424                decision(ctx, service, Some(id), json!({"transport":"tcp"})).await
425            else {
426                return Ok(());
427            };
428            let pattern = chargen(&answer)?;
429            // RFC 864: anything the client sends is thrown away. A client that half-closes
430            // its side (netcat -N does at once) still wants the stream, so the end of its
431            // input never ends the session; only a failed write or the limit does.
432            let drain = async {
433                let mut sink = vec![0u8; wire::READ_CHUNK];
434                while let Ok(n) = reader.read(&mut sink).await {
435                    if n == 0 {
436                        break;
437                    }
438                }
439                std::future::pending::<()>().await
440            };
441            let generate = async {
442                let mut sent: u64 = 0;
443                let mut n = 0usize;
444                loop {
445                    let mut line = wire::chargen_line(&pattern.charset, pattern.width, n);
446                    if let Some(max) = pattern.max_bytes {
447                        let left = max.saturating_sub(sent) as usize;
448                        if left == 0 {
449                            break;
450                        }
451                        line.truncate(left);
452                    }
453                    if send(ctx, id, &mut writer, &line).await.is_err() {
454                        break;
455                    }
456                    sent += line.len() as u64;
457                    n += 1;
458                }
459                let _ = writer.shutdown().await;
460            };
461            tokio::pin!(drain);
462            tokio::select! {
463                _ = generate => {}
464                _ = &mut drain => {}
465            }
466            Ok(())
467        }
468    }
469}
470
471async fn udp_loop(ctx: SpawnContext, service: Service, socket: Arc<UdpSocket>) {
472    let in_flight = Arc::new(tokio::sync::Semaphore::new(MAX_UDP_IN_FLIGHT));
473    let mut buf = vec![0u8; wire::READ_CHUNK + 1];
474    let mut chargen_offset = 0usize;
475    loop {
476        let (n, peer) = match socket.recv_from(&mut buf).await {
477            Ok(v) => v,
478            Err(_) => continue,
479        };
480        if n > wire::READ_CHUNK {
481            Log::new(Some(&ctx.status_tx)).warn(format!(
482                "{} dropped an oversized datagram from {peer}",
483                service.name()
484            ));
485            continue;
486        }
487        if service == Service::Discard {
488            continue;
489        }
490        let Ok(permit) = in_flight.clone().try_acquire_owned() else {
491            Log::new(Some(&ctx.status_tx)).warn(format!(
492                "{} dropped a datagram from {peer}: too many in flight",
493                service.name()
494            ));
495            continue;
496        };
497        let datagram = buf[..n].to_vec();
498        let offset = chargen_offset;
499        chargen_offset = chargen_offset.wrapping_add(1);
500        let child = ctx.clone();
501        let reply_socket = socket.clone();
502        ctx.state
503            .spawn_server_task(ctx.server_id, async move {
504                let _permit = permit;
505                let data = match service {
506                    Service::Echo => {
507                        let (data, encoding) = wire::encode(&datagram);
508                        json!({"transport":"udp","data":data,"encoding":encoding,"bytes":datagram.len()})
509                    }
510                    _ => json!({"transport":"udp"}),
511                };
512                // A failed or silent handler leaves the datagram unanswered.
513                let Ok(Some(answer)) = decision(&child, service, None, data).await else {
514                    return;
515                };
516                let reply = match service {
517                    Service::Echo => match answer["data"].as_str() {
518                        Some(data) => wire::decode(data, answer["encoding"].as_str()),
519                        None => Ok(datagram),
520                    },
521                    Service::Chargen => chargen(&answer).map(|p| {
522                        let mut line = wire::chargen_line(&p.charset, p.width, offset);
523                        line.truncate(wire::MAX_CHARGEN_UDP);
524                        line
525                    }),
526                    other => single_reply(other, &answer),
527                };
528                match reply {
529                    Ok(reply) => {
530                        let _ = reply_socket.send_to(&reply, peer).await;
531                    }
532                    Err(e) => Log::new(Some(&child.status_tx)).warn(format!("{} reply to {peer} not sent: {e}", service.name())),
533                }
534            })
535            .await;
536    }
537}