Securing inetd Servers
So far, the discussion has been around customized code within each server to carry out your security policy. This has a number of disadvantages:
Code to manage security must be built in to each server that is exposed to hostilities.
Each server must go through rigorous testing to verify its accuracy and resilience against attack.
Multiple points of access allow additional points of weakness to be exploited.
The first two points speak well for themselves. The last point is illustrated by the problem of securing many doors when a shopping mall closes. When several doors must be locked, it is far easier to overlook one of them. Additionally, it is far more likely that one door will be found with weaknesses that can be exploited. For all these reasons, it is desirable to put security policy into a centralized module.
Centralized Network Policy
In the last chapter, you saw how the inetd daemon made server design simpler. The inetd daemon provides all the server code necessary to listen for client requests and start the server only when it is necessary to do so. The inetd daemon provides one additional level of convenience: It allows a centralized network security model to be installed.
If you are using one of the newer Linux distributions such as Red Hat Linux 6.0, then you already have Wietse Venema's TCP wrapper program being invoked by inetd. To verify this, grep for the entry for telnetd, as follows:
# grep telnet /etc/inetd.conf
telnet stream tcp nowait root /usr/sbin/tcpd in.telnetd
From the example output, you see that inetd invokes the executable /usr/sbin/tcpd when a telnet request arrives. If you also grep for ftp, you will see that /usr/sbin/tcpd (hereafter simply referred to as tcpd) is also invoked for that service. So, what does this tcpd program do?
It should be emphasized that this tcpd program inserts itself between inetd and the server (telnetd, for example). This is done in a transparent way, because it does not perform any input or output on the socket. The tcpd program simply applies its network security rules and then invokes the intended server if access is granted.
From the grep example shown earlier, the tcpd program is provided the output string in.telnetd as its command name (its argv value). This tells tcpd what server to invoke if access is to be granted. If access is denied for any reason, the attempt is logged and the socket is closed (when tcpd exits), without invoking the server.
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.
Figure 16.1. 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?
Now, let's briefly state each solution in the following sections.
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.
So far, you have digested the theory behind the TCP wrapper concept. Next, you'll see this idea illustrated in the concrete form of example programs.