The Problem inetd Solves
Imagine a Linux server that needs to offer 20 different network services — FTP, telnet, finger, time, daytime, echo, discard, and more. The traditional approach would be to run 20 separate daemon processes, all started at boot time, all consuming memory and CPU even when no client is using them. Most of the time, these daemons sit completely idle.
inetd (Internet Daemon) solves this. It is a single daemon that listens on all those ports simultaneously. When a client connects to any of those ports, inetd wakes up, figures out which service it is, and launches the appropriate server program on demand. When the client disconnects, the launched server exits. No permanent idle processes.
The Core Idea: One Listener, Many Services
Without inetd vs With inetd
How inetd Works Internally
Understanding inetd’s internal mechanics is important because it uses the same system calls you have already learned, just in a clever way.
inetd Internal Steps
/etc/inetd.conf to find which services to manage. For each service, it creates a socket, binds it to the service port, and calls listen() (for TCP) or just bind() (for UDP). Then it calls select() on ALL those sockets at once.select() returns indicating that socket is ready. inetd knows which service corresponds to port 7.fork() to create a child process. The child will run the actual echo server program.- For TCP: calls
accept()to get the connected socket - Duplicates the socket to
stdin(fd 0),stdout(fd 1), andstderr(fd 2) usingdup2() - Closes all other file descriptors
- Calls
execve()to replace itself with the echo server program - The echo server reads from stdin and writes to stdout — but these ARE the socket!
- Closes the connected socket
- Goes back to
select()on all service sockets - Ready to handle the next connection on any port
This is the clever part: inetd makes the connected socket become stdin, stdout, and stderr in the child process. Then it execs the server program. The server program does not need to know anything about sockets — it just reads from stdin and writes to stdout, like any normal Unix program. inetd has silently replaced those standard streams with the socket.
/* Inside inetd's child process (simplified): */
/* cfd = connected socket (for TCP), or the UDP socket for UDP */
dup2(cfd, STDIN_FILENO); /* stdin (fd 0) now points to the socket */
dup2(cfd, STDOUT_FILENO); /* stdout (fd 1) now points to the socket */
dup2(cfd, STDERR_FILENO); /* stderr (fd 2) now points to the socket */
/* Close original cfd since we have copies as 0, 1, 2 */
if (cfd > STDERR_FILENO)
close(cfd);
/* Close all other inherited file descriptors from inetd */
/* (inetd had many sockets open for other services) */
for (int fd = 3; fd < max_fd; fd++)
close(fd);
/* Now exec the server program.
* It reads from stdin (=socket) and writes to stdout (=socket).
* It doesn't even need to know it's talking over a network! */
execve(server_program_path, server_argv, environ);
The Configuration File: /etc/inetd.conf
inetd’s behavior is controlled entirely by /etc/inetd.conf. Each line describes one service. The format is:
# Format:
# service socket-type protocol wait/nowait user server-path server-args
#
# service = name from /etc/services (or a port number)
# socket-type = stream (TCP) or dgram (UDP)
# protocol = tcp or udp
# wait/nowait = wait: inetd waits for server to exit before accepting more (UDP)
# nowait: inetd accepts the next connection immediately (TCP)
# user = username the server runs as
# server-path = full path to the server executable
# server-args = arguments passed to the server (argv[0] is program name)
# Example lines from a real /etc/inetd.conf:
# Echo service (TCP) - nowait: inetd doesn't wait, serves next client
echo stream tcp nowait root /usr/sbin/echod echod
# Echo service (UDP) - wait: inetd waits for this datagram exchange to finish
echo dgram udp wait root /usr/sbin/echod echod
# Daytime service - server is built-in to inetd (no external program)
daytime stream tcp nowait root internal
# FTP service
ftp stream tcp nowait root /usr/sbin/ftpd ftpd
# Telnet service
telnet stream tcp nowait root /usr/sbin/telnetd telnetd -a
The wait/nowait field controls how inetd behaves after launching a server:
inetd does NOT wait for the launched server to finish before accepting the next connection. This is the normal mode for TCP services. Each client gets its own process, and inetd immediately goes back to select() to handle the next client. The forked server handles the TCP connection independently.
inetd WAITS for the launched server to finish before accepting the next connection on that UDP port. This is required for UDP because inetd passes the server the actual UDP socket (not a connected copy). The server uses recvfrom() directly on it and may handle multiple datagrams. inetd must wait to avoid two servers competing for the same UDP socket.
Some very simple services (daytime, time, echo, discard, chargen) are implemented directly inside inetd itself, without forking an external program. These are called internal services. In inetd.conf you specify internal as the server path.
This is even more efficient — no fork(), no exec(), just inetd handling the request inline. For example, the daytime service just formats the current date/time and sends it. No external daemon is needed for something that trivial.
Writing an inetd-Compatible Server
Here is the key insight: because inetd sets up stdin and stdout to be the socket, your server program does not need any socket code at all! It just reads from stdin and writes to stdout. This is a beautifully simple design.
/*
* simple_echo_inetd.c
*
* An echo server designed to be launched by inetd.
* inetd connects stdin/stdout to the client socket before exec-ing us.
* We just read from stdin and write to stdout — no socket code needed!
*
* In /etc/inetd.conf:
* echo stream tcp nowait root /usr/local/bin/simple_echo_inetd simple_echo_inetd
*
* Compile: gcc -o simple_echo_inetd simple_echo_inetd.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUF_SIZE 4096
int main(void)
{
char buf[BUF_SIZE];
ssize_t numRead;
/*
* stdin (fd 0) = the client's connected TCP socket (set up by inetd)
* stdout (fd 1) = the same socket
*
* We read data and write it back. That is the entire echo protocol.
* No socket(), bind(), listen(), accept() needed!
*/
while ((numRead = read(STDIN_FILENO, buf, BUF_SIZE)) > 0) {
/* Write all bytes back to stdout (=socket) */
ssize_t written = 0;
while (written < numRead) {
ssize_t n = write(STDOUT_FILENO,
buf + written,
numRead - written);
if (n <= 0)
return EXIT_FAILURE;
written += n;
}
}
/*
* numRead == 0: client closed connection — normal exit.
* numRead == -1: read error.
*/
return (numRead == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
}
/etc/inetd.conf. No socket code required in the program itself.xinetd — The Modern Replacement
The original inetd is simple but lacks security features. Most modern Linux distributions use xinetd (extended inetd) instead. xinetd adds:
Restrict connections by source IP address or hostname. Reject unauthorized clients before they even reach the server program.
Limit how many connections per second a service accepts. Protects against DoS attacks and runaway clients.
Set a maximum number of simultaneous instances of each service. Prevents resource exhaustion.
Detailed logging of connections, including who connected, when, and from where. Integrates with syslog.
Enable services only during certain hours. For example, allow FTP only during business hours.
Supports hosts.allow / hosts.deny for additional access control at the TCP wrapper layer.
xinetd Configuration Format
xinetd uses a different configuration format from inetd — each service gets its own block in /etc/xinetd.d/:
# /etc/xinetd.d/echo-stream
# xinetd configuration for TCP echo service
service echo
{
type = INTERNAL # handled inside xinetd
id = echo-stream # unique ID
socket_type = stream # TCP
protocol = tcp
user = root
wait = no # nowait for TCP
disable = no # enable this service
}
# Example of an external service with access control:
# /etc/xinetd.d/my-service
service my_service
{
socket_type = stream
protocol = tcp
wait = no
user = nobody # run as unprivileged user
server = /usr/local/bin/my_server
server_args = -v
only_from = 192.168.1.0/24 # only allow local subnet
instances = 10 # max 10 simultaneous connections
per_source = 3 # max 3 connections per source IP
log_on_success = HOST DURATION # log on successful connection
log_on_failure = HOST # log on rejected connection
disable = no
}
Modern Alternative: systemd Socket Activation
On modern Linux systems (virtually all major distributions since ~2012), systemd has replaced both inetd and xinetd with a feature called socket activation. The concept is the same — listen on a port, launch the server on demand — but integrated into systemd’s service management framework.
How systemd socket activation works:
- You create a
.socketunit file telling systemd which port to listen on - You create a corresponding
.serviceunit file for the actual server - systemd listens on the socket. When a connection arrives, it starts the service
- systemd passes the ready socket to the service via a file descriptor (not dup2 to stdin, but via
SD_LISTEN_FDS) - If no connections for a while, the service can be stopped, saving resources
# /etc/systemd/system/echo.socket
[Unit]
Description=Echo Service Socket
[Socket]
ListenStream=7 # TCP port 7
Accept=yes # spawn a new service instance per connection
[Install]
WantedBy=sockets.target
# /etc/systemd/system/echo@.service
[Unit]
Description=Echo Service Instance
Requires=echo.socket
[Service]
Type=simple
ExecStart=/usr/local/bin/simple_echo_inetd # same inetd-style program!
StandardInput=socket # stdin is the accepted socket
StandardOutput=socket # stdout is the accepted socket
Accept=yes, systemd passes stdin/stdout as the socket, exactly like inetd does. Programs written for inetd work with systemd socket activation without any changes. The principle introduced by inetd in the 1980s is still alive today.inetd vs xinetd vs systemd — Quick Comparison
| Feature | inetd | xinetd | systemd |
|---|---|---|---|
| Config location | /etc/inetd.conf | /etc/xinetd.d/ | /etc/systemd/system/ |
| Access control | No (needs tcpwrappers) | Yes (built-in) | Via firewall / IPFilter |
| Rate limiting | No | Yes | Limited |
| Internal services | Yes | Yes | No |
| Modern distro support | Rare | Some | Universal |
| stdin/stdout trick | Yes (dup2) | Yes (dup2) | Yes (Accept=yes) |
Interview Questions & Answers
inetd (Internet Daemon) is a super-daemon that listens on multiple network ports simultaneously. Instead of running a separate daemon process for each network service (FTP, telnet, echo, etc.) — each of which sits idle most of the time — inetd listens on all their ports at once using select(). When a client connects on a particular port, inetd forks and execs the appropriate server. This saves significant system resources, especially on systems that offer many infrequently-used services.
inetd uses dup2() in the child process to duplicate the connected socket onto file descriptors 0, 1, and 2 (stdin, stdout, stderr). It then closes the original socket fd and all other inherited file descriptors, and calls execve() to replace the child with the server program. The server program reads client data from stdin and sends responses via stdout — both of which are actually the network socket. The server doesn’t need any socket-related code itself.
nowait (used with TCP): after forking the server, inetd immediately returns to select() and is ready to accept the next connection. The forked server handles the TCP connection independently. wait (used with UDP): after forking the server, inetd stops monitoring that UDP port and waits for the server to exit before accepting more connections on it. This is necessary for UDP because the server is given the original UDP socket (not a separate connected socket like TCP), so two concurrent servers would compete for the same socket.
Internal services are very simple network services (echo, discard, daytime, time, chargen) that inetd implements directly within its own code, without forking an external program. When a connection arrives on an internal service port, inetd handles the entire request in-process and responds immediately. In /etc/inetd.conf, these are marked with internal as the server path. This is even more efficient than forking because there is no process creation overhead at all.
At startup, inetd creates one socket for each service listed in /etc/inetd.conf and binds each to the corresponding port. It collects all these socket file descriptors into an fd_set and calls select() with all of them. select() blocks until any one of those sockets becomes readable (a new connection or datagram arrives). inetd then identifies which socket triggered, looks up the corresponding service, and forks the appropriate server. This allows a single process to efficiently monitor any number of ports without spinning in a busy loop.
xinetd adds several important features: (1) built-in access control by source IP/hostname (no need for external TCP wrappers), (2) rate limiting — limits connections per second to prevent DoS attacks, (3) per-service connection limits, (4) time-of-day access restrictions, (5) enhanced logging of connections and refusals, and (6) per-source connection limits. These features make xinetd much more suitable for production Internet-facing servers than the original inetd, which has minimal security controls.
systemd socket activation achieves the same goal — listen on a port and launch a service on demand — but is integrated into systemd’s service management. Key differences: (1) systemd uses .socket unit files instead of /etc/inetd.conf, (2) with Accept=yes, systemd sets up stdin/stdout as the socket (just like inetd), so existing inetd-compatible programs work unchanged, (3) systemd has better integration with modern service lifecycle management, cgroups, and security features, (4) systemd has no internal services — all services run external programs. On virtually all modern Linux distributions, systemd socket activation is the preferred approach.
It follows the classic Unix philosophy of composability. A program that reads stdin and writes stdout can be: (1) tested directly from the command line by a human, (2) connected to a network via inetd, xinetd, or systemd socket activation, (3) connected to a file via shell redirection, (4) piped to/from other programs. The program has no socket-specific code, making it simpler, easier to test, and reusable in multiple contexts. This separation of concerns — transport mechanism separate from application logic — is a core Unix design principle.
The original inetd has several security weaknesses: (1) no built-in access control — any host on the network can connect, (2) no rate limiting — a flooding attack can exhaust server resources by causing rapid fork() calls, (3) services run as root unless configured otherwise, (4) no logging of connection attempts or refusals, (5) no per-service connection limits. Because of these weaknesses, inetd is generally considered unsuitable for Internet-facing systems today. xinetd or systemd with appropriate firewall rules are used instead.
Chapter 60 Complete!
You have covered iterative servers, concurrent servers, UDP and TCP echo services, and the inetd super-daemon. These are the core patterns for all Linux network servers.
