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 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.
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.
TCP nowait — Concurrent servers
UDP wait — Sequential servers
/* 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;
}
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)
- 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 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:
- Call
socket()to create a socket - Call
bind()to assign a port - Call
listen()to allow incoming connections - Call
accept()to accept a client connection - 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;
}
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:
- Logs the connection — records which host connected, to which service, and when
- Checks access control — consults /etc/hosts.allow and /etc/hosts.deny to decide whether to permit or deny the connection
- If the connection is permitted, tcpd exec()s the real server (e.g., in.ftpd) with the remaining arguments
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
| 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 |
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.