Logo

Linux Router Guide - Update

Since publishing my first article about a Linux based internet router a lot of things have changed. For starters, I live on a different continent now. I have switched the internet service provider on several occasions. I went from dial-up first to ISDN and then to DSL, first volume based, then flat rate. Also the hardware and software configuration changed over time...

Hardware

The router hardware now consists of an i440BX board featuring a P3-1GHz supported by 512 MB RAM and a 27 GB harddisk. After moving house, the hardware is now fixed to the wall in a cupboard, see the picture:

Cupboard

In the past I used to burn bootable CD-ROMs containing the root file system. However, for ease of maintenance I now skip the final step of the production process and use the CD master on the production harddisk directly, mounted read-only. The remaining almost 27 GB of the harddisk are used for caching. The harddisk is wall-mounted using special rubber elements to decouple noise and vibration from the wall. This and the position of the cupboard outside the living areas means the operation of a harddisk is completely noise-free, thus removing one of the reasons for operating a diskless system.

As before, the construction of a diskless (or root read-only) router can be broken into distinct stages:

Well, without further ado, here we go into the details.

Setting up a Linux system

In principle, you can use whatever distro you are familiar with and which you can really tailor down to your needs in the second step. In fact, the tailoring already starts with the setup, since you do not want to install anything that is not needed. Complexity compromises security.

In my case, I stuck with Slackware since I have some experience with it, and it comes as a much less complex distro to start with. During installation I tried to minimize the number of installed packages. For those of you interested: the following is the content of my /var/log/packages directory which under Slackware contains the list of installed packages:

root@conner:~# ls /var/log/packages/
aaa_base-11.0.0-noarch-2     inetd-1.79s-i486-7
aaa_elflibs-11.0.0-i486-9    iproute2-2.6.16_060323-i486-1
at-3.1.10-i486-1             iptables-1.3.5-i486-2
autoconf-2.60-noarch-1       less-394-i486-1
automake-1.9.6-noarch-1      libtermcap-1.2.3-i486-6
bash-3.1.017-i486-1          libtool-1.5.22-i486-1
bin-11.0-i486-3              lilo-22.7.1-i486-2
bin86-0.16.15-i486-1         lsof-4.76-i486-1
binutils-2.15.92.0.2-i486-3  make-3.81-i486-1
bison-2.1-i486-1             man-1.6c-i486-2
byacc-1.9-i386-1             mkinitrd-1.0.1-i486-3
bzip2-1.0.3-i486-3           module-init-tools-3.2.2-i486-2
coreutils-5.97-i486-1        ncurses-5.5-i486-1
cxxlibs-6.0.3-i486-1         openssl-solibs-0.9.8d-i486-1
dcron-2.3.3-i486-5           perl-5.8.8-i486-3
devs-2.3.1-noarch-25         pkgconfig-0.21-i486-3
diffutils-2.8.1-i486-3       pkgtools-11.0.0-i486-4
e2fsprogs-1.38-i486-2        procps-3.2.7-i486-1
elvis-2.2_0-i486-2           proftpd-1.3.0-i486-1
etc-11.0-noarch-2            sed-4.1.5-i486-1
findutils-4.2.28-i486-1      shadow-4.0.3-i486-13
flex-2.5.4a-i486-3           shared-mime-info-0.19-i486-1
gawk-3.1.5-i486-3            strace-4.5.14-i486-1
gcc-3.4.6-i486-1             sudo-1.6.8p12-i486-1
gcc-g++-3.4.6-i486-1         sysklogd-1.4.1-i486-9
glibc-2.3.6-i486-6           sysvinit-2.84-i486-69
glibc-solibs-2.3.6-i486-6    tar-1.15.1-i486-2
grep-2.5-i486-3              tcpip-0.17-i486-39
groff-1.19.2-i486-1          traceroute-1.4a12-i386-2
gzip-1.3.5-i486-1            util-linux-2.12r-i486-5
hdparm-6.6-i486-1            zlib-1.2.3-i486-1

Your choice may be different in parts, but it should give you some idea. After installation I went on to wipe cruft out of /etc and to simplify almost every aspect of system configuration. Simplification is particularly important since /etc is going to be mounted in a RAM disk in stage 3 of this guideline.

For those of you interested in how far you can go, here is an overview of the content of my /etc for your perusal.

root@conner:~# ls -R /etc
/etc:
HOSTNAME       hosts        login.access   profile       shadow
adjtime        inetd.conf   login.defs     proftpd.conf  shadow-
apache/        inittab      modules.devfs  protocols     shells
devfsd.conf    inputrc      motd           random-seed   squid/
dialogrc       ioctl.save   mtab           rc.d/         sudoers
fdprm          issue        networks       resolv.conf   syslog.conf
fstab          ld.so.cache  nscd.conf      screenrc      termcap
group          ld.so.conf   nsswitch.conf  securetty
hardwareclock  lilo.conf@   passwd         service/
host.conf      localtime    ppp/           services

/etc/apache:
access.conf  httpd.conf  magic  mime.types  srm.conf

/etc/ppp:
auth-down*  chap-secrets  ip-up*       ipx-up*      resolv.conf
auth-fail*  ip-down*      ip-up.bind*  options
auth-up*    ip-mark*      ipx-down*    pap-secrets

/etc/rc.d:
rc.0*  rc.3*  rc.crond*     rc.httpd*  rc.mrhttpd*  rc.sap*
rc.1*  rc.6*  rc.dnscache*  rc.inet1*  rc.overlay*  rc.squid*
rc.2*  rc.S*  rc.firewall*  rc.inetd*  rc.pppd*     rc.syslog*

/etc/service:
dnscache/  ftp/  http/  ppp/  telnet/

/etc/service/dnscache:
env/  log/  root/  run*  seed

/etc/service/dnscache/env:
CACHESIZE  DATALIMIT  FORWARDONLY  IP  IPSEND  ROOT

/etc/service/dnscache/log:
main/  run*  status

/etc/service/dnscache/log/main:

/etc/service/dnscache/root:
ip/  servers/

/etc/service/dnscache/root/ip:
127.0.0.1  192.168.157

/etc/service/dnscache/root/servers:
\@

/etc/service/ftp:
run*

/etc/service/http:
run*

/etc/service/ppp:
run*

/etc/service/telnet:
run*  supervise/

/etc/service/telnet/supervise:
control|  lock  ok|  status

/etc/squid:
cachemgr.conf  mime.conf  squid.conf

I also tend to install the latest 2.4 kernel from kernel.org. Note that the later 2.4 kernels require a glibc patch with older versions of Slackware. The easiest solution, of course, is to install the latest Slackware version.

Now, why 2.4 I hear you asking? I have to confess that I run 2.6 on all other machines. However, 2.4 is smaller, more mature, and provides devfs, as described in stage 3 of this guideline. Devfs has been removed from current 2.6 kernels where it is replaced by a different system called udev. There have been semi-religious discussions about the topic. I for my part will not entertain these discussions. I might move to 2.6 and udev on the router at some point, but for the time being my motto is "never change a running system".

So much about setting up the system. Make sure you also read the original article which contains some background information that is still valid.

Tailoring the Linux system to the intended use

As a first step the functional scope and every expectation should be specified. Truth be told, my requirements have changed over time, often with a rapid prototyping approach. Nonetheless I can list them as they are at the time of writing:

This list almost implies the installation of a ppp daemon, a dns resolver, a caching HTTP proxy, a telnet daemon, an ftp daemon and a web server, combined with a firewall script and some glue to make it all work.

Before talking about the installation and configuration of all those components, I would like to describe the new system of run levels, as specified in /etc/inittab and the corresponding scripts in /etc/rc.d. The router knows the following runlevels:

LevelDescription
SStartup of system
1Local maintenance mode (keyboard, VGA screen)
2LAN maintenance mode (telnet, ftp, http)
3Productive mode (ppp, dns, http proxy, NAT)
0System halting
6System rebooting

The use of runlevel 2 is a deviation from standard Slackware definition. It is very useful since it allows low-level maintenance of the router system without having to connect a keyboard and a monitor to the wall-mounted unit.

Let's look at the script /etc/rc.d/rc.2 which is called by init when the system enters runlevel 2:

#!/bin/bash
#
# Martin Rogge, 18/08/2002, 16/03/2003, 26/04/2003, 10/08/2004, 06/02/2006
#

# Tell the viewers what's going to happen.
echo "Going from runlevel $PREVLEVEL to runlevel $RUNLEVEL..."

if [ $PREVLEVEL '>' 9 ]; then
  PREVLEVEL=0
fi

if [ $PREVLEVEL '<' 2 ]; then

  # Start the syslogd and klogd daemons
  . /etc/rc.d/rc.syslog start

  # Initialize the transport layer
  . /etc/rc.d/rc.inet1

  # Start the crond server
  . /etc/rc.d/rc.crond start

  # Start the inetd server
  . /etc/rc.d/rc.inetd start

  # Start the web server
  . /etc/rc.d/rc.mrhttpd start

fi

if [ $PREVLEVEL '>' 2 ]; then

  # Stop the Squid proxy server
  . /etc/rc.d/rc.squid stop

  # Stop the DNS resolver
  . /etc/rc.d/rc.dnscache stop

  # Stop the PPP daemon
  . /etc/rc.d/rc.pppd stop

fi

# Set up firewall rules
. /etc/rc.d/rc.firewall internal

As you can see, depending on the previous run level, the script will stop or start services to reach the desired set of running services. The other scripts in /etc/rc.d/ work in the same way. All those scripts have in common that they only manage the services explicitly described here. Services unknown to the scripts are not affected.

Let's now look at the services in more detail.

Pppd

Pppd is the ppp daemon (surprise, surprise). It manages dial-up connections using the PPP protocol. Note that most DSL connections are of this type, including mine. All you need is a pppd module that implements PPPoE (PPP over Ethernet) and a reference to it in the pppd configuration file /etc/ppp/options. After importing this module, pppd will accept the name of a network interface in place of a serial device in the configuration file, eg. eth1. The PPPoE module is part of the pppd distribution.

Development activities around pppd have not been very fast in recent years. You might have to google for a download location of version 2.4.4 since the official site seems to be a bit outdated, plus it seems to have an unreliable download section. It is then just a matter of compiling the software and installing it. The only thing to look out for is that "make install" might not install the PPPoE module rp-pppoe.so to where you want it. I had to copy it manually to /usr/local/lib/pppd/2.4.4/rp-pppoe.so.

For reference, my configuration file /etc/ppp/options looks like this:

# General PPPD configuration options
plugin /usr/local/lib/pppd/2.4.4/rp-pppoe.so
eth1
noipdefault
noauth
default-asyncmap
defaultroute
hide-password
usepeerdns
mtu 1492
mru 1492
noaccomp
noccp
nobsdcomp
nodeflate
nopcomp
novj
novjccomp
user "your_username_here"
lcp-echo-interval 20
lcp-echo-failure 3
demand
persist
192.168.158.2:192.168.158.1
ipcp-accept-remote
ipcp-accept-local
ktune

Also interesting in this context is the topic of MTU and MRU. MTU stands for maximum transmission unit, and similarly MRU stands for maximum receive unit. They define the maximum packet size used by the IP protocol and are usually set to 1500 for most network interface cards. However, the PPPoE protocol requires 8 bytes for itself, so for optimum performance (in order to avoid fragmentation), the rest of the local area network should set their MTU and MRU to 1492. This involves some clever tweaking, particularly with computers running MS Windows. Google is your friend.

The following is the script setting up my router's network interfaces during system startup:

#!/bin/bash
#
# rc.inet1	This shell script sets up the transport layer
#
# Martin Rogge, 18/08/2002, 16/03/2003, 07/01/2004, 22/01/2006
#

# Attach the loopback device
  /sbin/ifconfig lo 127.0.0.1
  /sbin/route add -net 127.0.0.0 netmask 255.0.0.0 dev lo

# echo "Attempting to configure eth0 by contacting a DHCP server..."
# /sbin/dhcpcd -t 10 -d eth0

  echo "Configuring eth0 as 192.168.157.7..."
  /sbin/ifconfig eth0 192.168.157.7 broadcast 192.168.157.255 netmask 255.255.255.0 up mtu 1492
  if [ ! $? = 0 ]; then
    echo "Failed to configure eth0 as 192.168.157.7..."
  fi

  echo "Configuring eth1 for PPPoE..."
  /sbin/ifconfig eth1 up mtu 1500
  if [ ! $? = 0 ]; then
    echo "Failed to configure eth1 for PPPoE..."
  fi

# Default gateway
# /sbin/route add default gw 192.168.157.7 metric 1 dev eth0

# EOF

If you have a contract with your ISP based on online time you will want pppd to close the connection after a certain timeout. However, you do not want to count unsolicited packets from the outside world (since they arrive all the time, be it from infected Windows machines, be it through P2P protocols). The official way of achieving this involves some crazy functionality named PPP packet filtering. The easier way is fixing the coding where the check is made. This is what open source is all about, is it not? Below I copied the patch for pppd version 2.4.1.

--- auth.c-original Mon Mar 12 23:54:33 2001
+++ auth.c  Tue Mar 13 10:54:11 2001
@@ -761,7 +761,7 @@
     if (idle_time_hook != 0) {
        tlim = idle_time_hook(&idle);
     } else {
-       itime = MIN(idle.xmit_idle, idle.recv_idle);
+       itime = idle.xmit_idle;
        tlim = idle_time_limit - itime;
     }
     if (tlim <= 0) {

Pppd also enables us to keep a log of online times as well as updating the DNS resolver with the current list of DNS servers published by the ISP at the time of going online. This can be achieved through the scripts /etc/ppp/ip-up and /etc/ppp/ip-down that are executed by pppd when a connection is established or closed, respectively. We will return to this subject after looking at the other services.

Djbdns

Djbdns is a surprisingly controversial package containing (amongst other binaries) a dns server called tinydns and a dns resolver called dnscache. The software itself is small, efficient, robust and secure, particularly when compared to some of the dinosaur packages occupying the ground since the early unix days. From my point of view, the controversy is to some extent about a new kid on the block performing better than the older kids, but also about the strong and uncompromising views of the creator - Daniel J. Bernstein.

From the attributes I mentioned it is clear that dnscache is the DNS resolver of choice for my router. Installation could be easy (download from http://cr.yp.to/djbdns/install.html, compile and install) if it wasn't for two things we must tackle independently.

One is the directory structure. The installation procedure expects the services to be run from an environment called tcp tools (also provided by Daniel J. Bernstein). This entails that they expect to be run in a specific directory structure under /service. Tcp tools exercise process control similar to a combination of init and inetd, an option which I would like to decline (especially since dnscache is so stable anyway). Therefore my startup script /service/dnscache/run has been changed to:

#!/bin/sh
#exec >/dev/vc/5
#exec >>/var/log/dnscache/dnscache.log
exec >/var/log/dnscache/dnscache.log
exec 2>&1
exec <seed
exec /usr/local/bin/envdir /service/dnscache/env \
     sh -c 'exec /usr/local/bin/envuidgid dnscache /usr/local/bin/dnscache'

I do not feel strongly about the directory structure, but since I want the root file system to be read-only, I chose to make /service a symbolic link to /etc/service.

The second issue is more subtle. I mentioned earlier, the ISP provides us at the time of establishing a PPPoE connection with the IP addresses of the current DNS servers that can be used for forwarding. This information is passed to the script /etc/ppp/ip-up. Unfortunately, with the distributed version of dnscache the only way of making it re-read the DNS servers is to kill and restart it.

I have created a patch that makes dnscache re-read the list of forwarders when receiving a SIGHUP signal. As far as I can see this modification should be safe - from my understanding of when signal handlers are called there should be no race possible. However, I must clearly state that the modification is not endorsed by Daniel J. Bernstein and that all inquiries and error reports should go to ME only.

*** dnscache.b  2001-02-11 22:11:45.000000000 +0100
--- dnscache.c  2007-02-28 21:13:46.000000000 +0100
***************
*** 1,3 ****
--- 1,4 ----
+ #include <signal.h>
  #include <unistd.h>
  #include "env.h"
  #include "exit.h"
***************
*** 386,391 ****
--- 387,398 ----

  char seed[128];

+ void signal_handler(const int sig)
+ {
+   if (sig == SIGHUP)
+     roots_init();
+ }
+
  int main()
  {
    char *x;
***************
*** 442,447 ****
--- 449,456 ----
    if (socket_listen(tcp53,20) == -1)
      strerr_die2sys(111,FATAL,"unable to listen on TCP socket: ");

+   signal(SIGHUP, signal_handler);
+
    log_startup();
    doit();
  }

Squid

Squid is a caching http proxy. It cuts down response time for http clients by caching the requested resources locally on the proxy server (either in memory or on disk). Note this is probably not noticeable within a given user session because usually (depending on the user settings) the web browser itself caches the requested objects. However, across users or across sessions the effect should be noticeable.

Installation is simple. Download the latest stable package from http://www.squid-cache.org/, compile it and install it according to the documentation. The interesting part is the configuration. I can show you my configuration file, but your guess is as good as mine.

# General Settings

http_port 8080
icp_port 0
cache_effective_user squid
cache_effective_group squid
visible_hostname conner.home

# ACL definition

acl all src 0.0.0.0/0.0.0.0
acl localhost src 127.0.0.0/8
acl to_localhost dst 127.0.0.0/8
acl ssl_ports port 443
acl proxy_ports port 80
acl proxy_ports port 443
acl connect method CONNECT
acl home src 192.168.157.0/24

# Access rules

http_access deny !proxy_ports
http_access deny connect !ssl_ports
http_access deny to_localhost
http_access allow home
http_access deny all
http_reply_access allow all
icp_access deny all

# Files & directories

cache_dir ufs /var/cache/squid 1000 16 256
cache_access_log /var/log/squid/access.log
cache_log /var/log/squid/cache.log
cache_store_log /var/log/squid/store.log
pid_filename /var/log/squid/squid.pid
coredump_dir /var/log/squid

# Cache Settings

cache_mem 384 MB
maximum_object_size 100000 KB
maximum_object_size_in_memory 100 KB

# Other settings

forwarded_for off
auth_param basic children 5
auth_param basic realm Squid proxy-caching web server
auth_param basic credentialsttl 12 hours
refresh_pattern . 60 20% 10080

Inetd, telnet and proftpd

These are part of the Slackware distribution. The only adaptation from my side happens within the configuration files which I simply quote here.

# This is inetd.conf

ftp     stream  tcp nowait  root  /usr/sbin/proftpd     proftpd
telnet  stream  tcp nowait  root  /usr/sbin/in.telnetd  in.telnetd
# ProFTPD configuration file.
#
# Martin Rogge, 30/07/2002, 22/01/2006

ServerName          "ProFTPD"
ServerType          inetd
DefaultServer       on

Port                21
Umask               022

MaxInstances        10

User                ftp
Group               ftp

RootLogin           on

UseReverseDNS       off
IdentLookups        off

PassivePorts        65000 65535

SystemLog           /var/log/proftpd/proftpd.log
TransferLog         /var/log/proftpd/transfer.log

<Directory /*>
  AllowOverwrite    on
</Directory>

<Anonymous ~ftp>
  RequireValidShell off
  User              ftp
  Group             ftp
  UserAlias         anonymous ftp
</Anonymous>

Some background information can be found in the original article.

Web server

The router provides web services strictly to the LAN - to report connection state, online time and data volume, and to allow clients to execute selected actions on the router.

In previous versions of the router I had used Apache web server for this purpose. Apache is probably the most widely used web server on this planet. It has enterprise software strength and runs a large part of the internet. I have never had any problem with Apache. It is, however, a big application that uses up resources. Another drawback: due to Apache relying on files in the /etc directory, it is not quite compatible with my runlevel system, because the idea is that in runlevel 2 (when the web server is still running) it is possible to unmount /etc for maintenance work. Remember, during normal startup the /etc directory is copied from the read-only root partition into a RAM disk and subsequently mounted under /etc.

When thinking about it rationally, I basically had the following requirements for a web server:

Since I could not find a suitable solution on the open source market, I decided to write a HTTP server especially for my router scenario. The result is called MrHTTPD, and it is available on Sourceforge.

MrHTTPD is a threaded design that matches all the requirements above. The binary is around 11 kilobytes in size, that is 1/30 of Apache httpd. Without HTTP Keepalive, it serves static files 3 times as fast as Apache. It has been volume tested and is now installed as the productive web server on my router.

Installation is straightforward: download the source from Sourceforge, unpack it and follow the instructions.

NAT and firewall

As you might know, NAT (network address translation) is called IP masquerading in the Linux world. IP masquerading and firewalling are fundamental kernel functions, controlled by the user space tool iptables. In the original article I have explained the functioning and the setup of IP masquerading and firewalling in great detail.

Further daemons

During startup, apart from the kernel daemons, only devfsd is started. On runlevel 1, syslogd, klogd and crond are started. On runlevel 2, inetd and mrhttpd are started. Eventually, on runlevel 3, the productive services pppd, dnscache and squid are started, and the NAT function is switched on.

The complete process list of the router operating in runlevel 3 looks like this:

F S   UID   PID  PPID  C PRI  NI ADDR SZ  WCHAN TTY          TIME CMD
4 S     0     1     0  2  69   0 -   157 1438dd ?        00:00:07 init
1 S     0     2     1  0  69   0 -     0 12024d ?        00:00:00 keventd
1 S     0     3     1  0  79  19 -     0 118095 ?        00:00:00 ksoftirqd_CPU0
1 S     0     4     1  0  69   0 -     0 12c6f7 ?        00:00:00 kswapd
1 S     0     5     1  0  69   0 -     0 138802 ?        00:00:00 bdflush
1 S     0     6     1  0  69   0 -     0 1388da ?        00:00:00 kupdated
1 S     0     7     1  0  69   0 -     0 16b731 ?        00:00:00 kjournald
1 S     0    11     1  0  69   0 -   377 17c16d ?        00:00:00 devfsd
1 S     0    21     1  0  69   0 -     0 16b731 ?        00:00:00 kjournald
1 S     0    32     1  0  69   0 -   380 1438dd ?        00:00:00 syslogd
5 S     0    34     1  0  69   0 -   368 1152bf ?        00:00:00 klogd
1 S     0    40     1  0  69   0 -   418 11bcb7 ?        00:00:00 crond
5 S     0    42     1  0  72   0 -   376 1438dd ?        00:00:00 inetd
5 S    16    44     1  0  69   0 -   501 1438dd ?        00:00:00 mrhttpd
5 S     0    46     1  0  69   0 -   543 1438dd ?        00:00:00 pppd
4 S    17    47     1  0  69   0 -   933 1440cc ?        00:00:00 dnscache
1 S     0    49     1  0  69   0 -   957 116bce ?        00:00:00 squid
4 S    19    52    49  0  74   0 -  2310 1440cc ?        00:00:00 squid
0 S    19    74    52  0  69   0 -   329 13d173 ?        00:00:00 unlinkd
0 S     0   154     1  0  69   0 -   366 188797 vc/1     00:00:00 agetty
0 S     0   155     1  0  69   0 -   366 188797 vc/2     00:00:00 agetty
0 R     0   160    42  0  73   0 -   399      - ?        00:00:00 in.telnetd
4 S     0   161   160  0  71   0 -   593 116bce pts/0    00:00:00 bash
0 R     0   166   161  0  71   0 -   477      - pts/0    00:00:00 ps

What about SSH?

Yeah, alright. In the original article I promised to move over to SSH (secure shell) as a replacement of telnet and ftp. So far, lazyness and "never change a running system" have prevented it. But one fine day...

And what about SMB?

SMB is the proprietary network protocol used by Microsoft Windows. Although there is very good server and client support in the open source world, I do not see a reason for it in my application. On the contrary, for security reasons I make sure that any IP packet related to SMB is filtered out by the firewall, no matter what interface it came from.

DNS forwarding and recording online time and data volume

This section is about topics that affect the setup of more than one service. Let us first turn towards DNS forwarding. Essentially we have set up dnscache to forward all DNS queries to the provider's DNS servers. Dnscache reads them from the file /service/dnscache/root/servers/@. In this document I have also described how to patch dnscache to re-read that file after receiving a SIGHUP signal.

The only thing remaining to be done is to update /service/dnscache/root/servers/@ and send dnscache the SIGHUP signal whenever the DNS servers change. This typically happens when a PPP connection is established. For that reason we add coding in the script /etc/ppp/ip-up that updates the file and sends the signal if required. For illustration purposes I shall print only the part of the script that deals with updating the DNS setting. In case you were wondering, I use Perl as script language for /etc/ip-up.

# Check if IP addresses are well formed

if (($ENV{'DNS1'} !~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/) ||
    ($ENV{'DNS2'} !~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/)) {
  exit 0;
}

open(DNS,'</service/dnscache/root/servers/@');
@output = <DNS>;
close DNS;

my $OLD1 = $output[0]; chop $OLD1;
my $OLD2 = $output[1]; chop $OLD2;

# Check if DNS servers have actually changed

if ( ($ENV{'DNS1'}, $ENV{'DNS1'}) eq ($OLD1, $OLD2) ) {
  exit 0;
}

open(DNS,'>/service/dnscache/root/servers/@');
print DNS <<EOF;
$ENV{'DNS1'}
$ENV{'DNS2'}
EOF
close DNS;

# The following requires a modified version of dnscache!

system('killall -1 dnscache 2> /dev/null');

Note that some people have suggested to empty the file /service/dnscache/root/servers/@ when the PPP connection goes down. This could be done in the script /etc/ppp/ip-down. It has the effect that dnscache answers DNS queries without delay when offline. The answer, of course, is "service not available". This makes sense in a scenario where you establish the PPP connection manually. In my case it makes more sense to keep the file when offline, because dnscache will then forward a new query to one of those servers and in doing so triggers PPP to establish a new connection to the ISP.

Let us now discuss the tracking and reporting of online time and data volume. This is certainly interesting information for the person in the household doing the system management, the bills, etc. In other words: me. It becomes particularly important when you are not on a flat rate, but even on a flat rate it might not be a bad idea to log these fundamentals in case you ever get into an argument with your ISP.

In order to do meaningful reporting, first we need to collect suitable data. I have chosen the following mechanism. The system records certain events in a set of log files. The log files are created chronologically according to the naming scheme iplog.<year>.<month> so that each event is stored in the file corresponding to the month and year of its time stamp. The following events are recorded:

EventTriggerScriptData
Going onlinepppd/etc/ppp/ip-upTime stamp, Interfaces, IP addresses, Data volume, DNS servers
Going offlinepppd/etc/ppp/ip-downTime stamp, Interfaces, IP addresses, Data volume
Midnightcrond/etc/ppp/ip-markTime stamp, Data volume

To give you a better impression, this is a typical excerpt of a log file:

15.02.2007 00:00:01 ip-mark RX:365018302 TX:34226294
15.02.2007 01:40:43 ip-down 0:ppp0 1:eth1 2:0 3:217.233.178.139 4:217.0.116.218 RX:368348239 TX:34731083
15.02.2007 01:44:04 ip-up   0:ppp0 1:eth1 2:0 3:217.233.175.159 4:217.0.116.218 RX:368348791 TX:34731768 DNS1:217.237.151.142 DNS2:217.237.151.51
15.02.2007 01:52:04 ip-down 0:ppp0 1:eth1 2:0 3:217.233.175.159 4:217.0.116.218 RX:368354245 TX:34740481
15.02.2007 01:53:58 ip-up   0:ppp0 1:eth1 2:0 3:217.233.164.1 4:217.0.116.218 RX:368354797 TX:34741070 DNS1:217.237.151.142 DNS2:217.237.151.51
15.02.2007 02:39:00 ip-down 0:ppp0 1:eth1 2:0 3:217.233.164.1 4:217.0.116.218 RX:369048676 TX:34848273
15.02.2007 08:16:00 ip-up   0:ppp0 1:eth1 2:0 3:217.233.167.55 4:217.0.116.218 RX:369049288 TX:34848744 DNS1:217.237.151.142 DNS2:217.237.151.51
16.02.2007 00:00:01 ip-mark RX:419479288 TX:40691543

Let's now review the three scripts. First comes /etc/ppp/ip-up. Please observe that I have snipped the coding to update the DNS servers since it is already listed above. Connoisseurs will notice that the script is written in Perl, a language very much suited to string operations.

#!/usr/bin/perl
#
# ip-up
#
# log event and update DNS servers
#
# Martin Rogge, 25/12/2003, 17/01/2004, 14/08/2004, 01/03/2007

my ($sec,$min,$hour,$day,$month,$year) = (localtime)[0,1,2,3,4,5];
$month += 1;
$year += 1900;
my $date = sprintf("%02s.%02s.%04s %02s:%02s:%02s", $day, $month, $year, $hour, $min, $sec);
my $filename = sprintf("/var/iplog/ip.log.%04s%02s", $year, $month);

open(OUT,">>$filename");
print OUT "$date ip-up  ";

my $i = 0;
foreach (@ARGV) {
  print OUT " $i:$_";
  $i++;
}

my @output = `/sbin/ifconfig eth1`;
foreach (@output) {
  if (/RX bytes:(\d+)/) {
    print OUT " RX:", $1;
  }
  if (/TX bytes:(\d+)/) {
    print OUT " TX:", $1;
  }
}

print OUT " DNS1:", $ENV{'DNS1'};
print OUT " DNS2:", $ENV{'DNS2'};

print OUT "\n";
close OUT;

open(OUT,">/var/iplog/ip.status");
print OUT "online\n";
close OUT;

# update of DNS settings follows here...

The corresponding code in /etc/ppp/ip-down:

#!/usr/bin/perl
#
# ip-down
#
# log event
#
# Martin Rogge, 25/12/2003, 17/01/2004, 14/08/2004

my ($sec,$min,$hour,$day,$month,$year) = (localtime)[0,1,2,3,4,5];
$month += 1;
$year += 1900;
my $date = sprintf("%02s.%02s.%04s %02s:%02s:%02s", $day, $month, $year, $hour, $min, $sec);
my $filename = sprintf("/var/iplog/ip.log.%04s%02s", $year, $month);

open(OUT,">>$filename");
print OUT "$date ip-down";

my $i = 0;
foreach (@ARGV) {
  print OUT " $i:$_";
  $i++;
}

my @output = `/sbin/ifconfig eth1`;
foreach (@output) {
  if (/RX bytes:(\d+)/) {
    print OUT " RX:", $1;
  }
  if (/TX bytes:(\d+)/) {
    print OUT " TX:", $1;
  }
}

print OUT "\n";
close OUT;

open(OUT,">/var/iplog/ip.status");
print OUT "offline\n";
close OUT;

Finally /var/spool/cron/crontabs/root:

0 0 * * * /etc/ppp/ip-mark 1>/dev/null

This entry causes crond to execute the script /etc/ppp/ip-mark every midnight:

#!/usr/bin/perl
#
# ip-mark
#
# put a mark into the ip log
#
# Martin Rogge, 25/12/2003, 17/01/2004, 14/08/2004, 18/02/2006

my ($sec,$min,$hour,$day,$month,$year) = (localtime)[0,1,2,3,4,5];
$month += 1;
$year += 1900;
my $date = sprintf("%02s.%02s.%04s %02s:%02s:%02s", $day, $month, $year, $hour, $min, $sec);
my $filename = sprintf("/var/iplog/ip.log.%04s%02s", $year, $month);

open(OUT,">>$filename");
print OUT "$date ip-mark";

my @output = `/sbin/ifconfig eth1`;
foreach (@output) {
  if (/RX bytes:(\d+)/) {
    print OUT " RX:", $1;
  }
  if (/TX bytes:(\d+)/) {
    print OUT " TX:", $1;
  }
}

print OUT "\n";
close OUT;

Recording the events is one thing. Compressing them into meaningful information is another. For that purpose I have created a series of more or less elaborate CGI scripts that are way to long to be included in this text. Instead, I give you a link to the output of a typical report. The output is based on the log file quoted above.

Making the root file system run read-only

My method of making the root file system read-only has not changed much from the method described in the original article. It involves identifying exactly which files should have write access during system operation, and placing them on a suitable filesystem that is made writeable during system startup. At present I have the following filesystems in operation:

root@conner:~# cat /proc/mounts
rootfs / rootfs rw 0 0
/dev/root / ext3 ro 0 0
none /dev devfs rw 0 0
ramfs /etc ramfs rw 0 0
ramfs /tmp ramfs rw 0 0
proc /proc proc rw 0 0
/dev/hda2 /var ext3 rw 0 0

As you can see, the root file system is mounted read-only. Another hard disk partition is mounted read-write under /var. This is the place for log files, and in particular, for the http cache. The /dev directory is taken care of by the devfs file system. The /tmp directory is a ram disk that is initially empty. And finally, the /etc directory is mounted as ramdisk that is initially filled with the content of /etc from the root file system.

An interesting option to be explored in future is the concept of a union file system. A union file system can implement a copy-on-write mechanism, ie. it can merge a read-only file system with a ram disk transparently, so that files opened for writing are copied into the ramdisk, from where they overlay the corresponding file in the read-only file system. An example of that can be seen when looking at Knoppix, a Linux distro that runs entirely from CD-ROM or DVD.

Burning the root file system on CD-ROM

In the original article I described in detail how the root file system is transferred onto a CD-ROM. However, at that time the aim was a completely diskless operation. Since I now provide http caching, there has to be a hard disk in the system, and we might as well run the root file system directly from the hard disk. I still make it read-only for added security. This may be less secure than running it from CD-ROM, but it would require root access to do any lasting changes.

Hope you found this article interesting. Feel free to send me an email if you have any questions or comments.

Martin