inetd TCP/UDP Handling & Internal Services

 

Part 3: inetd TCP/UDP Handling & Internal Services
wait/nowait Flags, Echo Service & tcpd Wrapper | Chapter 60 – TLPI Series

Overview

In Part 2 we saw how inetd works at a high level. Now we look at the critical difference between how inetd handles TCP services versus UDP services, what the wait and nowait flags actually mean, how internal services work without a separate process, and we walk through the TCP echo service example end-to-end.

The wait / nowait Flag: What It Controls

The flags field in /etc/inetd.conf takes one of two values: wait or nowait. This controls whether inetd temporarily hands off socket management to the execed server or continues managing it itself.

nowait (for TCP servers)

With nowait, inetd:

  • Accepts the connection itself (calls accept())
  • Forks the server to handle that one connection
  • Immediately goes back to select() — it keeps monitoring the listening socket
  • Can accept the next connection right away, even while the first server is still running

The execed server handles exactly one connection and then terminates. inetd listens for the next one.

Result: Multiple concurrent TCP server instances can run simultaneously — one per active client.
wait (for UDP servers)

With wait, inetd:

  • Forks the server
  • Removes this socket from its select() set — inetd stops monitoring this service
  • Waits until the server process exits (detects via SIGCHLD handler)
  • Only then adds the socket back to its select() set

The execed server reads all outstanding datagrams, then terminates (usually using a read timeout to detect when no more data is coming).

Result: Only one instance of this UDP server runs at a time. inetd waits for it to finish before handling the next datagram.

Why Are the Rules Different for TCP and UDP?

TCP always uses nowait — Here’s Why

TCP is connection-oriented. When a client connects to port 21 (FTP), inetd accepts that connection — this creates a new connected socket. The listening socket (port 21) is still available for the next client to connect to. inetd can immediately go back to select() on the listening socket and accept another client’s connection while the first FTP server is still serving the first client. Multiple TCP connections are fully independent from each other.

UDP usually uses wait — Here’s Why

UDP is connectionless. There is only one socket (the bound port). All client datagrams arrive on the same socket. If inetd stays in select() on the same UDP socket while a UDP server is already running and reading from that socket, both inetd AND the server would be competing to read datagrams from the same socket. This is a race condition: inetd might wake up first on the next datagram and start a second server instance before the first one has finished. Using wait prevents this by having inetd remove the socket from its monitoring set until the running server exits.

Timeline: nowait (TCP) vs wait (UDP)

TCP nowait — Concurrent servers

Time →
inetd
select()
Client1 connects →
accept()→fork()
select()
Client2 →
accept()→fork()
select()…
server1
handling Client1 …
exit
server2
handling Client2 …
exit

UDP wait — Sequential servers

Time →
inetd
select()
dgram arrives →
fork()
REMOVED from select() — waiting
select()…
server
reads all datagrams (with timeout)
exit →
SIGCHLD
/* Pseudocode: UDP server launched by inetd with 'wait' flag
   Must read all datagrams and then terminate */

#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>

#define TIMEOUT_SEC 5    /* terminate if no datagram for 5 seconds */

int main(void) {
    /* fd 0 is the UDP socket (set up by inetd via dup2) */
    int sock = STDIN_FILENO;

    /* Set a read timeout so we eventually exit */
    struct timeval tv = { .tv_sec = TIMEOUT_SEC, .tv_usec = 0 };
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    char buf[4096];
    struct sockaddr_in client_addr;
    socklen_t addrlen;

    while (1) {
        addrlen = sizeof(client_addr);
        ssize_t n = recvfrom(sock, buf, sizeof(buf), 0,
                             (struct sockaddr *)&client_addr, &addrlen);
        if (n < 0) {
            /* EAGAIN or EWOULDBLOCK means timeout — no more datagrams */
            break;
        }
        /* Process the datagram and respond */
        sendto(sock, buf, n, 0,
               (struct sockaddr *)&client_addr, addrlen);
    }

    /* Exit so inetd gets SIGCHLD and re-adds socket to select() */
    return 0;
}

Internal Services: inetd as the Server

For a few very simple, well-known services, inetd does not bother exec()-ing a separate server process at all. It handles these services itself, internally. This is an efficiency optimization — no fork(), no exec(), no new process needed.

To configure a service as internal, set the server program field to internal and omit the server arguments field:

# These are echo service entries — commented out by default
# To enable, remove the '#' at the start of each line
# echo stream tcp nowait root internal
# echo dgram  udp wait   root internal

# Other common internal services:
# discard stream tcp nowait root internal   (discards all input)
# daytime stream tcp nowait root internal   (returns current date/time)
# chargen stream tcp nowait root internal   (character generator)
Common internal services:

  • echo – Sends back exactly what it receives (port 7). Used for testing.
  • discard – Reads and silently discards all input. A /dev/null for the network.
  • daytime – Returns current date and time as a human-readable string.
  • chargen – Continuously generates and sends a stream of characters. Used for bandwidth testing.

The TCP Echo Service: End-to-End Example

The echo service is the simplest possible network service: whatever data the client sends, the server sends back exactly the same data. Port 7 is the standard echo port.

What inetd Does For Us (TCP echo server scenario)

Normally, a TCP server must perform all of these steps before it can even start reading data:

  1. Call socket() to create a socket
  2. Call bind() to assign a port
  3. Call listen() to allow incoming connections
  4. Call accept() to accept a client connection
  5. Then read/write data

When invoked by inetd, the server can skip ALL of steps 1–4. inetd has already done them. The execed server simply reads from fd 0 and writes to fd 1.

A Minimal inetd-Compatible TCP Echo Server

/* echo_server.c
   A minimal echo server designed to be invoked by inetd.
   inetd handles socket(), bind(), listen(), accept() for us.
   The connection is already on fd 0 (stdin) / fd 1 (stdout).
   Compile: gcc -o echo_server echo_server.c
   /etc/inetd.conf entry:
   echo stream tcp nowait root /usr/local/bin/echo_server echo_server
*/

#include <stdio.h>
#include <unistd.h>

int main(void) {
    char buf[4096];
    ssize_t nread;

    /* Read from fd 0 (= the connected TCP socket, set up by inetd) */
    while ((nread = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
        /* Write the same bytes back to fd 1 (= same socket) */
        write(STDOUT_FILENO, buf, nread);
    }
    /* When read returns 0, client closed the connection */
    return 0;
}

Testing the Echo Service with netcat

# First, enable the echo service in /etc/inetd.conf:
# Remove the '#' from this line:
#   echo stream tcp nowait root internal
# Then reload inetd:
sudo killall -HUP inetd

# Test with netcat:
echo "Hello inetd!" | nc localhost 7

# Expected output:
# Hello inetd!

# Test with telnet:
telnet localhost 7
# Type anything and it echoes back. Press Ctrl+] then quit to exit.

A Minimal inetd-Compatible UDP Echo Server

/* udp_echo_server.c
   UDP echo server for inetd with 'wait' flag.
   Must read datagrams until a timeout, then exit.
   /etc/inetd.conf entry:
   echo dgram udp wait root /usr/local/bin/udp_echo udp_echo
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/time.h>
#include <errno.h>

int main(void) {
    int sock = STDIN_FILENO;  /* inetd puts UDP socket on fd 0 */
    char buf[4096];
    struct sockaddr_storage client;
    socklen_t clen;

    /* Set 5-second read timeout — exit when no datagrams arrive */
    struct timeval tv = { .tv_sec = 5, .tv_usec = 0 };
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));

    for (;;) {
        clen = sizeof(client);
        ssize_t n = recvfrom(sock, buf, sizeof(buf), 0,
                             (struct sockaddr *)&client, &clen);
        if (n < 0) {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;  /* timeout — no more datagrams, exit cleanly */
            break;
        }
        /* Echo datagram back to sender */
        sendto(sock, buf, n, 0, (struct sockaddr *)&client, clen);
    }

    /* Exit sends SIGCHLD to inetd, which re-adds socket to select() */
    return 0;
}

The tcpd TCP Wrapper

Looking back at the example /etc/inetd.conf lines from Part 2:

ftp    stream tcp nowait root /usr/sbin/tcpd in.ftpd
telnet stream tcp nowait root /usr/sbin/tcpd in.telnetd
login  stream tcp nowait root /usr/sbin/tcpd in.rlogind

Notice that for all three services, the server program is /usr/sbin/tcpd, not the actual FTP or Telnet server. The actual server name (in.ftpd, in.telnetd, in.rlogind) appears as the first server argument.

tcpd is the TCP daemon wrapper. When inetd exec()s tcpd, tcpd does the following before invoking the real server:

  1. Logs the connection — records which host connected, to which service, and when
  2. Checks access control — consults /etc/hosts.allow and /etc/hosts.deny to decide whether to permit or deny the connection
  3. If the connection is permitted, tcpd exec()s the real server (e.g., in.ftpd) with the remaining arguments
How tcpd uses argv[0]: When inetd exec()s tcpd, it sets argv[0] to the first server argument — e.g., in.ftpd. tcpd reads argv[0] to know which real server to invoke and also uses it for logging and access control lookups. This is a clever UNIX trick where a program changes behavior based on what name it was called with.
/* /etc/hosts.allow example — allow FTP from local network only */
in.ftpd : 192.168.1.

/* /etc/hosts.deny — deny everything else */
in.ftpd : ALL

/* Check current tcpd rules for a specific host */
tcpdmatch in.ftpd 203.0.113.5

Quick Reference: inetd Behavior Summary
Scenario Flag inetd accepts()? inetd keeps select()-ing? Concurrent?
TCP server (typical) nowait Yes Yes — immediately Yes
TCP server (accepts own) wait No — passes fd No — waits for exit No
UDP server (typical) wait N/A (UDP) No — waits for exit No
Internal service either Yes (TCP) Yes N/A — no child

Interview Questions & Answers
Q1. What do the ‘wait’ and ‘nowait’ flags mean in /etc/inetd.conf?
nowait means inetd continues monitoring the socket after launching the server. The server handles one connection and exits; inetd immediately handles the next. wait means inetd removes the socket from its select() set after launching the server, and waits (via SIGCHLD) until the server exits before resuming monitoring. nowait allows concurrent servers; wait enforces sequential handling.
Q2. Why should most UDP servers use ‘wait’ and not ‘nowait’?
Because UDP is connectionless, all datagrams for a service arrive on the same single socket. If inetd used nowait, it would keep select()-ing on the same UDP socket while the server is also reading from it. This creates a race condition where inetd could receive a datagram first and start a second server instance before the first has finished. Using wait removes this race by ensuring only one server reads from the UDP socket at a time.
Q3. What are inetd internal services? Give examples.
Internal services are implemented directly inside inetd itself, without exec()-ing any external server process. They are specified by putting “internal” in the server program field and omitting arguments. Examples include: echo (port 7, reflects data back), discard (port 9, discards input), daytime (port 13, returns current time as text), and chargen (port 19, generates a character stream). These are so simple that the overhead of fork+exec for each request is not justified.
Q4. How does an inetd-launched server know what file descriptor to read/write on?
inetd’s child process uses dup2() to copy the socket onto file descriptors 0 (stdin), 1 (stdout), and 2 (stderr) before calling exec(). The server simply uses read(0,…) and write(1,…) or even standard C I/O functions like fgets()/fputs() on stdin/stdout. The server does not need to create a socket, bind, listen, or accept — all that was done by inetd.
Q5. What is tcpd and what role does it play with inetd?
tcpd (TCP daemon wrapper) is an intermediary invoked by inetd instead of the real server. It adds logging and access control checks (using /etc/hosts.allow and /etc/hosts.deny) before exec()-ing the actual server. The real server name is passed as argv[0] to tcpd, which uses it to determine what to log and which rules to check. If the connection is permitted, tcpd exec()s the real server.
Q6. How does a UDP server invoked by inetd know when to exit?
The UDP server sets a read timeout on the socket (using SO_RCVTIMEO via setsockopt or a select() with timeout). It processes all datagrams that arrive. When a recvfrom() call times out with EAGAIN/EWOULDBLOCK, the server knows no more datagrams are arriving in the current window and exits. This exit triggers SIGCHLD in inetd, which then re-adds the UDP socket to its select() monitoring set for future datagrams.
Q7. A TCP server configured with ‘wait’ in inetd.conf — is that ever valid?
Yes, in a special case. When wait is specified for a TCP service, inetd does NOT call accept() — instead it passes the listening socket file descriptor directly to the server (as fd 0). The server itself is responsible for calling accept() on it. This means the server can handle multiple connections in sequence (calling accept() in a loop). inetd stops monitoring that port until the server exits. This is useful for servers that do their own multiplexing but want inetd to handle the initial socket setup.

Leave a Reply

Your email address will not be published. Required fields are marked *