Create a Network Service

The power of the computer is it's ability to network. As we use computers to provide data, it's even more useful to make that data accessible from a network. The Unix operating system provided a simple structure in which to multi-task programs and with the addition of the BSD (Berkeley Software Distribution) Tcp/IP stack and networking calls it became fairly easy to create network services.

You don't need to be a C programming guru to create a network service. There are several ways to create network services without writing a single line of C code. Your choices are:

    • Use the Internet Super Server, also known as inetd.

    • Use a perl script to write a socket program.

    • Use a command line utility to provide the service.

Lets start with some terminology:

Socket: This comes from the programming API that BSD provided in the very early Unix days. A socket is a structure of data that documents a network connection endpoint. Sockets are usually created in a C program, but Perl also has the ability to create sockets.

TCP/IP: Transmission Control Protocol / Internet Protocol. This is called a network protocol. A network protocol describes how data is passed over the network. TCP/IP provides a couple of communications methods, mainly a TCP socket, which is a reliable, session tracked connection, and a UDP socket, which is a connectionless datagram connection. While TCP may be slower due to all the handshaking required to establish a reliable connection, UDP provides a fast but raw data transfer that is not reliable. Most interactive network services will use TCP, while services like file sharing would use UDP.

Daemon: This is a program that runs to provide a service. Not all daemon's are network capable. The idea is that we create a program that runs in a continuous loop. This daemon will accept input data, a request, process the request and return the results.

Listen: A network daemon that provides a service will Listen on a socket for connection requests. When we define the socket, we specify the protocol to be use, such as TCP or UDP. We may also specify the listen queue size. This defines the number of simultaneous connects we will accept before rejecting a client connection. This is defined because a network daemon will generally listen for a request, then fork a child process to handle that request. It takes time for that fork and child process to start up. Therefore we set the listen queue size to buffer up multiple connections, giving us time to spawn the children to do the work. This also prevents our network daemon from becoming saturated with too many connection requests.

IP Address: All sockets need to be assigned an address. The IPV4 (Internet Protocol Version 4) specification states that an IP address is 32 bits. The 32 bit address is broken down into 4, 8-bit bytes, each byte separated by a dot (.). For example 192.168.224.50. An IP Address consists of 2 parts, a network part and a host part. The network part is always at the begining of the IP addres and the host part is always at the end. The separation point of the network part and the host part depends on the class of IP Address and the Netmask. There are several classes of IP Address,

Netmask: This is a number that is used to identify the part of the IP Address that represent the network portion. This is a mask word that has it's bits set to one to represent the network part. For example 255.255.255.0. The netmask is also used for determining routing.

CIDR: Classless Inter-Domain Routing. A notation to represent an IP Address with specification that indicates the number of bits to be viewed as the network part, above and beyond the netmask. A typical CIDR notation for a class C address of 192.168.224.50 would be 192.168.224.0/24, which indicates that 24 bits (from the left) are to be used as the network portion. As another example, consider the class B address 172.24.0.31. Normally the netmask for a class B address would be 255.255.0.0. We can use CIDR to specify that not the first 16 bits are network part, but rather the first 24 bits are network part, and the last 8 bits are host part, 172.24.0.0/24.

Port: All sockets also need to be assigned a port number. When we identify a network connection we generally reference an IP Address and a port number. Each IP Address can have up to 64K ports, as per the IPV4 (Internet Protocol Version 4) specification. Ports 1 thru 1024 are reserved as priviledged ports, only accessible by programs running as root.

NIC: Network Interface Controller. This is the hardware that provides the connection to the computer. Usually a plugin card that connects to your network. Two catagories, wired and wireless. A single NIC will be assigned a single IP Address. That little plug on the back of your computer that looks like a phone jack, only wider, is connected to a NIC.

If we were to write a network service in C, the sequence would be:

    1. create the socket, specifying the protocol and port number.

    2. listen on the socket using the specified port.

    3. pend on a connection. This is called an accept.

    4. fork a child process on a connection request and pass the socket to the child.

    5. the child reads the request from the socket and processes it.

    6. the parent, in the mean time, goes back to listening for more connections.

There's quite a few lines of C code to implement the above sequence. The same hold true if you wish to use Perl to implement a socket. I don't find Perl provides any significant shortcuts to socket programing, with the exception of using some cpan package. In addition, if you write your network daemon in Perl, then you need to come up with a means to start it at boot time. Not a big issue with using an RC script, but there's a simpler way.

The inetd super server provides an excellent method to implement a network service. You can use just plain old script code to write your service. What inetd does is that he opens a socket, on the port number you specify, and waits for a connection request. When a connection occurs, he forks a child that executes the program you specify. The socket is passed from inetd, to the executing program, and appears to the program as STDIN and STDOUT. Thus your program merely needs to read from STDIN and write to STDOUT, just like any script program you may have written in the past.

The first thing you need to do to create an inetd service is to define a port number and name for the service. As an example, we'll create a service called "diskuse", so I'll create an entry to add to the /etc/services file, as follows:

# Local services

diskuse 5192/tcp # diskuse service by TFS

This defines a service, named diskuse, that runs on port 5192 using the TCP protocol.

Then I'll create the diskuse program and put it in /usr/sbin/diskuse. Here's the script:

#! /bin/bash

# NAME: diskuse

# AUTHOR: Thomas Sandholm

# DATE: Thu Aug 21 18:41:14 EDT 2008

# SUMMARY: Network service to show the diskuse of local ext3 filesystems.

# VERSION: 1.0

# DESCRIPTION: This program will be called by inetd as a network service. It

# runs on port 5192 using the TCP protocol. It will show the disk

# usage for local ext3 filesystems. It uses the df command to get

# the disk usage data and filters out the header line and replaces

# multiple whitespace with a colon (:).

# MAIN

df -l -t ext3 | sed -e 1d -e 's/[ ][ ]*/:/g'

exit 0

Put this program in /usr/sbin/diskuse and set the mode bit for read & execute,

# chmod +rx /usr/sbin/diskuse

Run the program to confirm it works and generates output such as:

# /usr/sbin/diskuse

/dev/sda1:1008888:504080:453560:53%:/

Of course your output may be different.

/etc/inetd.conf

If you're using inetd, then we need to update the inetd.conf configuration file. We'll discuss the configuration of xinetd later.

We need to update the /etc/inetd.conf file to add our service.

The structure of the /etc/inetd.conf file is fully documented in the man pages, INETD(8), but to summarize the structure, you have a single line to define a single service, the definition of the fields are as follows:

<service_name> <sock_type> <proto> <flags> <user> <server_path> <args>

My entry for the diskuse service is as follows:

# network service to display diskuse for local ext3 filesystems

diskuse stream tcp nowait root /usr/sbin/diskuse

service_name == diskuse

sock_type == stream

proto == tcp

flags == nowait

user == root

server_path == /usr/sbin/diskuse

Now we restart the inetd. I'm using Debian, so my command may be different than yours.

# /etc/init.d/openbsd-inetd restart

And now I can test my service. I'll use the telnet client as we can specify the port number to connect to. This is handy, as my program merely returns ascii data, something the telnet client can handle.

deb01:/etc/init.d# telnet localhost 5192

Trying 127.0.0.1...

Connected to localhost.localdomain.

Escape character is '^]'.

/dev/sda1:1008888:504316:453324:53%:/

Connection closed by foreign host.

deb01:/etc/init.d#

And there you have it. the line beginning with /dev/sda1... is the data returned from my network service. To verify that I can access this network service from another system, I go over to my openSuSE system and issue the following:

beast2:/usr/sbin # cat < /dev/tcp/deb01/5192

/dev/sda1:1008888:504316:453324:53%:/

beast2:/usr/sbin #

This is a neat little trick. The bash shell has support for "special" file descriptors, namely sockets.

If you redirect stdin from /dev/tcp/hostname/portnumber the shell actually opens a read-only socket to the named host and port number, and the output is return to stdout. Unfortunately the Debian developers felt that the special file descriptor support in bash wasn't too useful, so when they built the bash shell the opted to leave that support out.

Securing your Network Service

You might wish to restrict what systems are able to access your web service. That's easy to accomplish by using tcp wrappers, also called tcpd. There's a manpage that discusses tcp wrappers, TCPD(8), and you'll also want to look at the man page HOSTS_ACCESS(5). There are 2 additional files we need to edit for using tcp wrappers, the /etc/hosts.deny and /etc/hosts.access. Depending on the Linux distribution you're using, you may or may not need to modify the /etc/inetd.conf file. I'm using Debian here at home, so I do not need to update the /etc/inetd.conf file. For other distribution, see the man page on tcpd to determine if and what edits to /etc/inetd.conf are necessary.

The rules for accessing a service are as follows:

ACCESS CONTROL FILES

The access control software consults two files. The search stops at the

first match:

o Access will be granted when a (daemon,client) pair matches an

entry in the /etc/hosts.allow file.

o Otherwise, access will be denied when a (daemon,client) pair

matches an entry in the /etc/hosts.deny file.

o Otherwise, access will be granted.

A non-existing access control file is treated as if it were an empty

file. Thus, access control can be turned off by providing no access

control files.

So I set up my /etc/hosts.deny with the following:

ALL: ALL

Which say's to deny everything. In my /etc/hosts.allow, I specify the service I wish to restrict, then follow that with the list of hosts who are allowed access. Here's my /etc/hosts.allow:

diskuse: 192.168.224.90/32

This entry will allow IP address 192.168.224.90 (this is the address of deb01, the local system). Note the slash (/) at the end of the address. This is called CIDR notation (Classless Inter-Domain Routing), and it specifies what part of the IP address is the network part, the first 32 bits. So this entry will allow the machine with IP address 192.168.224.90 access to the diskuse service. Here's what shows up in my /var/log/daemon.log when an unauthorized access occurs:

Aug 25 20:21:17 deb01 inetd[3440]: refused connection from 192.168.224.50, service diskuse (tcp)

I attempted to access the service on deb01 (192.168.224.90) from system beast2 (192.168.224.50). Now, if I update my /etc/hosts.allow with:

diskuse: 192.168.224.90/32, 192.168.224.50/32

It all works. Now let me try and access the diskuse service from deb02 (192.168.224.91). I get the following in my /var/log/daemon.log file:

Aug 25 20:23:38 deb01 inetd[3550]: refused connection from 192.168.224.91, service diskuse (tcp)

And you see that deb02 (192.168.224.91) is denied. This may seem like a hassle to put every host in the hosts.allow file. Considering that all my systems on my home network are in the address range of 192.168.224.0/24, I could use a CIDR address to allow any machine from the 192.168.224.0 network to connect. So I change my hosts.allow to read:

diskuse: 192.168.224.0/24

You'll notice I specified a network mask of 24 bits instead of 32. I'm indicating to only look at the first 24 bit of the 32-bit IP address, the network part. This is a neat trick, I've allowed ANY machine on the 192.168.224.0 network to access my service.

See also: http://sites.google.com/site/tfsidc/Reserving-TCPIP-Port-for-Service