2013-01-16

The Debian "ifupdown" package and a possible replacement

If you have ever configured a Debian server, chances are you've interacted with the "ifupdown" package many times (hint, it's the commands "ifup" and "ifdown").  For what it claims to do, it's very simple to understand and configure, but that still leaves a lot to be desired for more complicated system administration.

Long story short, I got tired of its limitations and tried to make some changes to it myself only to discover that it is one of the least maintainable pieces of C code I have ever laid eyes on (possibly excepting a few IOCCC entries).  As a result, I've started a rewrite-from-scratch unimaginatively called "ifupdown-ng" and implemented in Python.  My #1 goal is bug-for-bug compatibility with ifupdown.  I eventually want to support a lot more features and make sysadmins' lives a lot easier, but it first needs to be a safe drop-in replacement without changing a single line in your "/etc/network" directory.

If you've never dived into exactly how "ifupdown" really does its thing and are wondering why I'd go to all the trouble, well, let me give several examples of where it falls over for more complicated configurations:
  • Interfaces are brought down according to the _current_ configuration, not the configuration at the time they were brought up.  This means that if you change an interface from DHCP to static while it is still up, "ifdown" will not actually terminate the DHCP client when the interface is later brought down.
  • It's virtually impossible to safely make changes over a remote connection (such as SSH).  The best you can do is "ifdown eth0 && ifup eth0" but as mentioned above that breaks down with more than the simplest possible configuration changes.
  • Most DHCP configuration parameters, despite being a key part of the interface configuration, cannot be specified from /etc/network/interfaces but must be configured in other config files:
    • Extra options to be passed to the DHCP server (or requested from it) must be manually described in /etc/dhcp/dhclient.conf.
    • It's not possible to use a different "dhclient.conf" file for different interfaces
    • If you want to run commands after bringing up the interface, the "up" option in the config won't do it reliably.  You instead need to add custom scripts in different locations depending on which DHCP client program you are using (and many of them don't support hooks at all).
    • The ISC DHCP client will always be told to "DHCRELEASE" its addresses on termination, even if that isn't desired on the current network; if you configured the address for Wake-On-LAN you may not want the OS to blindly release its 2-week lease every day at 5PM. .
  • Logical interfaces, such as 802.1Q VLANs, MAC-based VLANs, software bridges, tunnels, etc are not supported except via shell-script hooks installed as a part of other packages.  Furthermore, you can't extend the list of "methods" (dhcp, ppp, etc) or "address families" (inet, inet6) supported by ifupdown without making code changes.  Overall the functionality that is available is significantly more complicated than it really needs to be, and it suffers from all the limitations described above.
  • There is no dependency management and the default ordering is extremely problematic.  If I configure a VLAN 42 on eth0 ("eth0.42") device with both "inet" and "inet6" addresses, then I can only put the VLAN configuration options in one of the two interface declarations or "ifup -a" will try to create it twice, and even then I have to put it on the first one of the two.  Even worse, when I later run "ifdown -a" on the same configuration, it will take them down in the wrong order and report errors for every command in the second stanza because the interface was deleted by the config in the first one.
So after I got tired of dealing with all of those issues in my network configuration and testing, I decided to do something about it and just fix "ifupdown" once and for all.  I figured I could extend the code to have some kind of decent plugin features and add some kind of dependency chaining.

<Silly narrative>

So I downloaded the source package and started poking around:

kyle@artemis:~/deb-src$ apt-get source ifupdown
Reading package lists... Done
Building dependency tree       
Reading state information... Done
iNOTICE: 'ifupdown' packaging is maintained in the 'Hg' version control system at:
http://anonscm.debian.org/hg/collab-maint/ifupdown/
Need to get 106 kB of source archives.
Get:1 http://mirrors.us.kernel.org/debian/ testing/main ifupdown 0.7.5 (dsc) [1,588 B]
Get:2 http://mirrors.us.kernel.org/debian/ testing/main ifupdown 0.7.5 (tar) [104 kB]
Fetched 106 kB in 0s (371 kB/s)   
pdpkg-source: info: extracting ifupdown in ifupdown-0.7.5
dpkg-source: info: unpacking ifupdown_0.7.5.tar.gz

kyle@artemis:~/deb-src$ cd ifupdown-0.7.5/

kyle@artemis:~/deb-src/ifupdown-0.7.5$ ls
biblio.bib  COPYING        ifup.8            Makefile      TODO.scripts
BUGS        debian         ifupdown.nw       makenwdep.sh
ChangeLog   examples       interfaces.5.pre  modules.dia
contrib     execution.dia  makecdep.sh       README

Hmm... the only things that look like source files are a couple shell scripts and that one "ifupdown.nw" file.  I wonder what that is?  Let's open it in VIM:

\documentclass{article}
\usepackage{graphicx}
\usepackage{noweb}
\usepackage{tikz}
\usetikzlibrary{calc}
\usetikzlibrary{positioning}
\pagestyle{noweb}
\noweboptions{smallcode,hideunuseddefs}
\begin{document}
@

\def\nwendcode{\endtrivlist\endgroup}
\let\nwdocspar=\relax

\title{ Interface Tools\thanks{
Copyright \copyright\ 1999--2007 Anthony Towns. This program is free
software; you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any
later version.
}}

Whoops... nope, that's definitely LaTeX documentation, I wonder where the code is?  Let's see if we can find a "main" function around here somewhere:

kyle@artemis:~/deb-src/ifupdown-0.7.5$ grep 'int main' -A3 -B3 -r .
./ifupdown.nw-querying /etc/network/interfaces (when called as [[ifquery]]).
./ifupdown.nw-
./ifupdown.nw-<<main>>=
./ifupdown.nw:int main(int argc, char **argv) {
./ifupdown.nw- <<variables local to main>>
./ifupdown.nw-
./ifupdown.nw- <<ensure environment is sane>>

Oh god... it found the LaTeX file...  I may be in trouble now.  Even worse, I'm pretty sure that code snippets such as "<<variables local to main>>" are not valid ANSI C.

</Silly narrative>

Long story short, the code is written in what's called "literate programming", specifically a language called "noweb".  The gist of the idea is to write your code and comments in a special self-documenting form so you can process it one way to get a design doc and a different way to get the actual usable source-code for a compiler.  Unfortunately, while the idea isn't bad, the implementation is terrible and the result is completely unreadable spaghetti code.

Here's one representative sample:

\subsection{File Handling}

So, the first and most obvious thing to deal with is the file
handling. Nothing particularly imaginative here.

<<variables local to read interfaces>>=
FILE *f;
int line;
@

<<open file or [[return NULL]]>>=
f = fopen(filename, "r");
if ( f == NULL ) return NULL;
line = 0;
@

<<close file>>=
fclose(f);
line = -1;
@

Each of these is used from precisely one place, inside the body of the read_interfaces() function.  Even worse, there are another 3 full blocks of <<variables local to read interfaces>> which are appended together during macro expansion.  In order to edit the code for this one relatively straightforward function, you are scrolling across 500 lines of crap just to see the full list of local variables.

Even worse is that most of the comments are exactly as useful as the one above, "... Nothing particularly imaginative here" indeed!

At this point I gave up, installed noweb, and just built the package so I could have some C files to inspect and reverse-engineer.  This worked out OK, and some magic even managed to preserve indentation across all the macro-expansion, but the resulting file was only marginally better than the over-documented spaghetti-code.  Since the author put his full faith in his "self-documenting" noweb style, he didn't bother to put more than about 36 lines of real C comments in the code, and most of those aren't even full comment lines but just a few words at the end of a code line.

Having gotten entirely exhausted from wading through that (please pardon my language) crock of shit, I decided that what I really wanted to do now was start from scratch and stretch my Python muscles a bit.  I've spent much of the last year learning to write Python pretty well for my work at Google, but this was the first personal project it really seemed appropriate to use on.

Anyways, the results are still in progress and it doesn't actually even do anything yet, but I'm interested in outside opinions on the overall approach.  If you missed the link above, I've hosted it here on GitHub: