Understanding the TCP Wrapper Concept
Figure 16.1 shows how you can visualize the role of tcpd as it interacts with inetd and the resulting server.
This graphical representation of the TCP wrapper concept
illustrates the relationship of the processes involved.
Let's review the process of a remote client connecting to your in.telnetd server:
- The client uses his telnet client command to issue a connect request to your machine's telnet daemon.
- Your Linux host is using inetd, which has been configured to listen on port 23 for telnet requests. It accepts the connection request from step 1.
- The /etc/inetd.conf configuration file directs your inetd server to fork(2) a new process. The parent process goes back to listening for more connects.
- The child process from step 3 now calls exec(2) to execute the /usr/sbin/tcpd TCP wrapper program.
- The tcpd program determines whether the client should be given access or not. This is determined by the combination of the socket addresses involved and the configuration files /etc/hosts.deny and /etc/hosts.allow.
- If access is to be denied, tcpd simply terminates (this causes file units 0, 1, and 2 to be closed, which are the socket file descriptors).
- If access is to be granted, the executable that is to be started is determined by tcpd's argv  value. In this example, the name is in.telnetd. This specifies the executable pathname /usr/sbin/in.telnetd, which is passed to the exec(2) function to load and execute.
- The server now runs in place of tcpd with the same process ID that tcpd formerly had. The server now performs input and output on the sockets (file units 0, 1, and 2).
Step 7 is important— it is where the server process is started by the exec(2) function call from within tcpd. This maintains the important parent/child relationship between inetd and the (child)
server process. When the wait flag word is used, the inetd daemon can start the next server only when it detects that the current child process has ended. This works correctly only when the server process is a direct child process of the parent inetd. Numbers might help make this easier to digest:
- The inetd daemon has process ID 124 for this example.
- The inetd daemon calls fork(2) to start a child process. This child process ID is now 1243 for this example.
- The inetd child process (PID 1243) now calls exec(2) to start /usr/sbin/tcpd.
- Note that tcpd is now running as PID 1243 (recall that exec(2) uses the same process resources to start a new program, while discarding the original program that called exec(2)).
- The tcpd eventually calls exec(2) again, when access is to be granted. This starts the new server, which is /usr/sbin/in.telnetd in this example.
- Note that the server /usr/sbin/in.telnetd still is PID 1243 because exec(2) does not create a new process (see notes in step 4).
- Server in.telnetd eventually exits (PID 1243 terminates).
- Parent process inetd (PID 124) receives a SIGCHLD signal to indicate that its child process ID 1243 has terminated. This will cause inetd to call upon wait (2) to determine which child process has terminated.
From this list of steps, you can see how cleverly inserted the tcpd wrapper program is. This program never actually performs I/O on the sockets— this would disturb the protocol being used (telnet or otherwise).
You might still have two questions at this point:
- How does the TCP wrapper program determine what service it is securing (telnet, ftp, and so on)?
- How does it determine who the client is?
Determining the Service
The tcpd program can determine the service it is protecting by calling upon the getsockname (2) function. Remember that function? It not only returns the socket address that the client was connecting to, but it indicates the port number of the service. In the previous examples, the port number was 23 (the telnet service).
Determining the Client Identity
Because the tcpd program was not the one that executed the accept(2) function call (this was done by inetd), it must determine who the client is. As you've probably guessed, this is done with the getpeername(2) function. You will recall that this function retrieves the address and port number of the remote client, in the same manner as getsockname(2).
Determining the Datagram Client Identity
Determining the identity of a datagram client is a bit trickier. The astute reader might have wondered about this in the previous section, because datagrams do not use the accept(2) function call. It is also not possible to use getpeername(2) on datagram sockets because each datagram can potentially come from different clients. The client's address is returned by the recvfrom(2) function call. How, then, can tcpd determine the client's identity without actually reading the server's datagram?
It turns out that tcpd is able to cheat. The client's address and port number can be determined by calling recvfrom(2) using the flag option MSG_PEEK. Example code is shown as follows:
struct sockaddr_in adr_clnt;/* AF_INET */
int len_inet; /* length */
int s; /* Socket */
char dgram; /* Recv buffer */
len_inet = sizeof adr_clnt;
z = recvfrom(s, /* Socket */
dgram, /* Receiving buffer */
sizeof dgram, /* Max recv buf size */
MSG_PEEK, /* Flags: Peek at data */
(struct sockaddr *)&adr_clnt,/* Addr */
&len_inet); /* Addr len, in & out */
Notice the flag option MSG_PEEK. This option directs the kernel to carry out the recvfrom(2) call as normal except that the datagram is not to be removed from the queue as "read." This allows the tcpd program to "peek" at the datagram that the server will subsequently read, if access is granted.
Notice that the data itself is not important here. What this MSG_PEEK operation accomplishes is that it returns the client's IP address (in the example, this is placed into adr_clnt). The wrapper program can determine from the variable adr_clnt whether this datagram should be processed by the server or not.