Print eui64 too while printing clients
[snf-nfdhcpd] / nfdhcpd
diff --git a/nfdhcpd b/nfdhcpd
index ee506d0..c66c435 100755 (executable)
--- a/nfdhcpd
+++ b/nfdhcpd
 #
 
 import os
+import signal
+import errno
 import re
+import sys
 import glob
 import time
 import logging
 import logging.handlers
-import subprocess
+import threading
+import traceback
 
 import daemon
+import daemon.runner
+import daemon.pidlockfile
 import nfqueue
 import pyinotify
+import setproctitle
+from lockfile import LockTimeout
 
 import IPy
-from select import select
+import socket
+import select
 from socket import AF_INET, AF_INET6
 
+from scapy.data import ETH_P_ALL
+from scapy.packet import BasePacket
 from scapy.layers.l2 import Ether
 from scapy.layers.inet import IP, UDP
-from scapy.layers.inet6 import *
+from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
+                               ICMPv6NDOptDstLLAddr, \
+                               ICMPv6NDOptPrefixInfo, \
+                               ICMPv6NDOptRDNSS
 from scapy.layers.dhcp import BOOTP, DHCP
-from scapy.sendrecv import sendp
+from scapy.layers.dhcp6 import DHCP6_Reply, DHCP6OptDNSServers, \
+                               DHCP6OptServerId, DHCP6OptClientId, \
+                               DUID_LLT, DHCP6_InfoRequest, DHCP6OptDNSDomains
 
+
+DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
-DEFAULT_NFQUEUE_NUM = 42
 DEFAULT_USER = "nobody"
-DEFAULT_LEASE_TIME = 604800 # 1 week
-DEFAULT_RENEWAL_TIME = 600  # 10 min
+DEFAULT_LEASE_LIFETIME = 604800 # 1 week
+DEFAULT_LEASE_RENEWAL = 600  # 10 min
+DEFAULT_RA_PERIOD = 300 # seconds
+DHCP_DUMMY_SERVER_IP = "1.2.3.4"
 
-LOG_FILENAME = "/var/log/nfdhcpd/nfdhcpd.log"
+LOG_FILENAME = "nfdhcpd.log"
 
 SYSFS_NET = "/sys/class/net"
-DHCP_DUMMY_SERVER_IP = "1.2.3.4"
 
-LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
-PERIODIC_RA_TIMEOUT = 30 # seconds
+LOG_FORMAT = "%(asctime)-15s %(levelname)-8s %(message)s"
+
+# Configuration file specification (see configobj documentation)
+CONFIG_SPEC = """
+[general]
+pidfile = string()
+datapath = string()
+logdir = string()
+user = string()
+
+[dhcp]
+enable_dhcp = boolean(default=True)
+lease_lifetime = integer(min=0, max=4294967295)
+lease_renewal = integer(min=0, max=4294967295)
+server_ip = ip_addr()
+dhcp_queue = integer(min=0, max=65535)
+nameservers = ip_addr_list(family=4)
+domain = string(default=None)
+
+[ipv6]
+enable_ipv6 = boolean(default=True)
+ra_period = integer(min=1, max=4294967295)
+rs_queue = integer(min=0, max=65535)
+ns_queue = integer(min=0, max=65535)
+dhcp_queue = integer(min=0, max=65535)
+nameservers = ip_addr_list(family=6)
+domains = force_list(default=None)
+"""
+
 
 DHCPDISCOVER = 1
 DHCPOFFER = 2
@@ -82,39 +127,169 @@ DHCP_REQRESP = {
     }
 
 
+def get_indev(payload):
+    try:
+        indev_ifindex = payload.get_physindev()
+        if indev_ifindex:
+            logging.debug(" - Incoming packet from bridge with ifindex %s",
+                          indev_ifindex)
+            return indev_ifindex
+    except AttributeError:
+        #TODO: return error value
+        logging.debug("No get_physindev() supported")
+        return 0
+
+    indev_ifindex = payload.get_indev()
+    logging.debug(" - Incoming packet from tap with ifindex %s", indev_ifindex)
+
+    return indev_ifindex
+
+
+def parse_binding_file(path):
+    """ Read a client configuration from a tap file
+
+    """
+    logging.info("Parsing binding file %s", path)
+    try:
+        iffile = open(path, 'r')
+    except EnvironmentError, e:
+        logging.warn(" - Unable to open binding file %s: %s", path, str(e))
+        return None
+
+    tap = os.path.basename(path)
+    indev = None
+    mac = None
+    ip = None
+    hostname = None
+    subnet = None
+    gateway = None
+    subnet6 = None
+    gateway6 = None
+    eui64 = None
+
+    def get_value(line):
+        v = line.strip().split('=')[1]
+        if v == '':
+            return None
+        return v
+
+    for line in iffile:
+        if line.startswith("IP="):
+            ip = get_value(line)
+        elif line.startswith("MAC="):
+            mac = get_value(line)
+        elif line.startswith("HOSTNAME="):
+            hostname = get_value(line)
+        elif line.startswith("INDEV="):
+            indev = get_value(line)
+        elif line.startswith("SUBNET="):
+            subnet = get_value(line)
+        elif line.startswith("GATEWAY="):
+            gateway = get_value(line)
+        elif line.startswith("SUBNET6="):
+            subnet6 = get_value(line)
+        elif line.startswith("GATEWAY6="):
+            gateway6 = get_value(line)
+        elif line.startswith("EUI64="):
+            eui64 = get_value(line)
+
+    try:
+        return Client(tap=tap, mac=mac, ip=ip, hostname=hostname,
+                      indev=indev, subnet=subnet, gateway=gateway,
+                      subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
+    except ValueError:
+        logging.warning(" - Cannot add client for host %s and IP %s on tap %s",
+                        hostname, ip, tap)
+        return None
+
+
 class ClientFileHandler(pyinotify.ProcessEvent):
     def __init__(self, server):
         pyinotify.ProcessEvent.__init__(self)
         self.server = server
 
-    def process_IN_DELETE(self, event):
-        self.server.remove_iface(event.name)
+    def process_IN_DELETE(self, event):  # pylint: disable=C0103
+        """ Delete file handler
+
+        Currently this removes an interface from the watch list
+
+        """
+        self.server.remove_tap(event.name)
+
+    def process_IN_CLOSE_WRITE(self, event):  # pylint: disable=C0103
+        """ Add file handler
 
-    def process_IN_CLOSE_WRITE(self, event):
-        self.server.add_iface(os.path.join(event.path, event.name))
+        Currently this adds an interface to the watch list
+
+        """
+        self.server.add_tap(os.path.join(event.path, event.name))
 
 
 class Client(object):
-    def __init__(self, mac=None, ips=None, link=None, hostname=None):
+    def __init__(self, tap=None, indev=None,
+                 mac=None, ip=None, hostname=None,
+                 subnet=None, gateway=None,
+                 subnet6=None, gateway6=None, eui64=None):
         self.mac = mac
-        self.ips = ips
+        self.ip = ip
         self.hostname = hostname
-        self.link = link
-        self.iface = None
-
-    @property
-    def ip(self):
-        return self.ips[0]
+        self.indev = indev
+        self.tap = tap
+        self.subnet = subnet
+        self.gateway = gateway
+        self.net = Subnet(net=subnet, gw=gateway, dev=tap)
+        self.subnet6 = subnet6
+        self.gateway6 = gateway6
+        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
+        self.eui64 = eui64
+        self.open_socket()
 
     def is_valid(self):
-        return self.mac is not None and self.ips is not None\
-               and self.hostname is not None
+        return self.mac is not None and self.hostname is not None
+
+
+    def open_socket(self):
+
+        logging.info(" - Opening L2 socket and binding to %s", self.tap)
+        try:
+            s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
+            s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
+            s.bind((self.tap, ETH_P_ALL))
+            self.socket = s
+        except socket.error, e:
+            logging.warning(" - Cannot open socket %s", e)
+
+
+    def sendp(self, data):
+
+        if isinstance(data, BasePacket):
+            data = str(data)
+
+        logging.debug(" - Sending raw packet %r", data)
+
+        try:
+            count = self.socket.send(data, socket.MSG_DONTWAIT)
+        except socket.error, e:
+            logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
+            self.socket.close()
+            self.open_socket()
+            raise e
+
+        ldata = len(data)
+        logging.debug(" - Sent %d bytes on %s", count, self.tap)
+        if count != ldata:
+            logging.warn(" - Truncated msg: %d/%d bytes sent",
+                         count, ldata)
 
 
 class Subnet(object):
     def __init__(self, net=None, gw=None, dev=None):
         if isinstance(net, str):
-            self.net = IPy.IP(net)
+            try:
+                self.net = IPy.IP(net)
+            except ValueError, e:
+                logging.warning(" - IPy error: %s", e)
+                raise e
         else:
             self.net = net
         self.gw = gw
@@ -122,18 +297,30 @@ class Subnet(object):
 
     @property
     def netmask(self):
+        """ Return the netmask in textual representation
+
+        """
         return str(self.net.netmask())
 
     @property
     def broadcast(self):
+        """ Return the broadcast address in textual representation
+
+        """
         return str(self.net.broadcast())
 
     @property
     def prefix(self):
+        """ Return the network as an IPy.IP
+
+        """
         return self.net.net()
 
     @property
     def prefixlen(self):
+        """ Return the prefix length as an integer
+
+        """
         return self.net.prefixlen()
 
     @staticmethod
@@ -141,6 +328,8 @@ class Subnet(object):
         """ Compute an EUI-64 address from an EUI-48 (MAC) address
 
         """
+        if mac is None:
+            return None
         comp = mac.split(":")
         prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
         eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
@@ -150,237 +339,306 @@ class Subnet(object):
         return IPy.IP(":".join(prefix))
 
     def make_eui64(self, mac):
+        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
+        subnet.
+
+        """
         return self._make_eui64(self.net, mac)
 
     def make_ll64(self, mac):
+        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
+
+        """
         return self._make_eui64("fe80::", mac)
 
 
-class VMNetProxy(object):
-    def __init__(self, data_path, dhcp_queue_num=None,
-                 rs_queue_num=None, ns_queue_num=None):
+class VMNetProxy(object):  # pylint: disable=R0902
+    def __init__(self, data_path, dhcp_queue_num=None,  # pylint: disable=R0913
+                 rs_queue_num=None, ns_queue_num=None, dhcpv6_queue_num=None,
+                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
+                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
+                 dhcp_domain=None,
+                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
+                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None,
+                 dhcpv6_domains=None):
+
+        try:
+            getattr(nfqueue.payload, 'get_physindev')
+            self.mac_indexed_clients = False
+        except AttributeError:
+            self.mac_indexed_clients = True
         self.data_path = data_path
+        self.lease_lifetime = dhcp_lease_lifetime
+        self.lease_renewal = dhcp_lease_renewal
+        self.dhcp_domain = dhcp_domain
+        self.dhcp_server_ip = dhcp_server_ip
+        self.ra_period = ra_period
+        if dhcp_nameservers is None:
+            self.dhcp_nameserver = []
+        else:
+            self.dhcp_nameservers = dhcp_nameservers
+
+        if ipv6_nameservers is None:
+            self.ipv6_nameservers = []
+        else:
+            self.ipv6_nameservers = ipv6_nameservers
+
+        if dhcpv6_domains is None:
+            self.dhcpv6_domains = []
+        else:
+            self.dhcpv6_domains = dhcpv6_domains
+
+        self.ipv6_enabled = False
+
         self.clients = {}
-        self.subnets = {}
-        self.ifaces = {}
-        self.v6nets = {}
+        #self.subnets = {}
+        #self.ifaces = {}
+        #self.v6nets = {}
         self.nfq = {}
 
         # Inotify setup
         self.wm = pyinotify.WatchManager()
         mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
         mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
-        handler = ClientFileHandler(self)
-        self.notifier = pyinotify.Notifier(self.wm, handler)
+        inotify_handler = ClientFileHandler(self)
+        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
         self.wm.add_watch(self.data_path, mask, rec=True)
 
         # NFQUEUE setup
         if dhcp_queue_num is not None:
-            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
+            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
 
         if rs_queue_num is not None:
-            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
+            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
+            self.ipv6_enabled = True
 
         if ns_queue_num is not None:
-            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
+            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
+            self.ipv6_enabled = True
+
+        if dhcpv6_queue_num is not None:
+            self._setup_nfqueue(dhcpv6_queue_num, AF_INET6, self.dhcpv6_response, 10)
+            self.ipv6_enabled = True
+
+    def get_binding(self, ifindex, mac):
+        try:
+            if self.mac_indexed_clients:
+                logging.debug(" - Getting binding for mac %s", mac)
+                b = self.clients[mac]
+            else:
+                logging.debug(" - Getting binding for ifindex %s", ifindex)
+                b = self.clients[ifindex]
+            return b
+        except KeyError:
+            logging.debug(" - No client found for mac / ifindex %s / %s",
+                          mac, ifindex)
+            return None
+
+    def _cleanup(self):
+        """ Free all resources for a graceful exit
+
+        """
+        logging.info("Cleaning up")
 
-    def _setup_nfqueue(self, queue_num, family, callback):
-        logging.debug("Setting up NFQUEUE for queue %d, AF %s" %
-                      (queue_num, family))
+        logging.debug(" - Closing netfilter queues")
+        for q, _ in self.nfq.values():
+            q.close()
+
+        logging.debug(" - Stopping inotify watches")
+        self.notifier.stop()
+
+        logging.info(" - Cleanup finished")
+
+    def _setup_nfqueue(self, queue_num, family, callback, pending):
+        logging.info("Setting up NFQUEUE for queue %d, AF %s",
+                      queue_num, family)
         q = nfqueue.queue()
         q.set_callback(callback)
         q.fast_open(queue_num, family)
         q.set_queue_maxlen(5000)
         # This is mandatory for the queue to operate
         q.set_mode(nfqueue.NFQNL_COPY_PACKET)
-        self.nfq[q.get_fd()] = q
+        self.nfq[q.get_fd()] = (q, pending)
+        logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
 
     def build_config(self):
         self.clients.clear()
-        self.subnets.clear()
 
-        for file in glob.glob(os.path.join(self.data_path, "*")):
-            self.add_iface(file)
+        for path in glob.glob(os.path.join(self.data_path, "*")):
+            self.add_tap(path)
+
+        self.print_clients()
 
     def get_ifindex(self, iface):
         """ Get the interface index from sysfs
 
         """
-        file = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
-        if not file.startswith(SYSFS_NET):
+        logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
+
+        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
+        if not path.startswith(SYSFS_NET):
             return None
 
         ifindex = None
 
         try:
-            f = open(file, 'r')
-            ifindex = int(f.readline().strip())
+            f = open(path, 'r')
+        except EnvironmentError:
+            logging.debug(" - %s is probably down, removing", iface)
+            self.remove_tap(iface)
+
+            return ifindex
+
+        try:
+            ifindex = f.readline().strip()
+            try:
+                ifindex = int(ifindex)
+            except ValueError, e:
+                logging.warn(" - Failed to get ifindex for %s, cannot parse"
+                             " sysfs output '%s'", iface, ifindex)
+        except EnvironmentError, e:
+            logging.warn(" - Error reading %s's ifindex from sysfs: %s",
+                         iface, str(e))
+            self.remove_tap(iface)
+        finally:
             f.close()
-        except IOError:
-            logging.debug("%s is down, removing" % iface)
-            self.remove_iface(iface)
 
         return ifindex
 
-
     def get_iface_hw_addr(self, iface):
         """ Get the interface hardware address from sysfs
 
         """
-        file = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
-        if not file.startswith(SYSFS_NET):
+        logging.debug(" - Getting mac for iface %s", iface)
+        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
+        if not path.startswith(SYSFS_NET):
             return None
 
         addr = None
         try:
-            f = open(file, 'r')
+            f = open(path, 'r')
+        except EnvironmentError:
+            logging.debug(" - %s is probably down, removing", iface)
+            self.remove_tap(iface)
+            return addr
+
+        try:
             addr = f.readline().strip()
+        except EnvironmentError, e:
+            logging.warn(" - Failed to read hw address for %s from sysfs: %s",
+                         iface, str(e))
+        finally:
             f.close()
-        except IOError:
-            logging.debug("%s is down, removing" % iface)
-            self.remove_iface(iface)
 
         return addr
 
-    def parse_routing_table(self, table="main", family=4):
-        """ Parse the given routing table to get connected route, gateway and
-        default device.
-
-        """
-        ipro = subprocess.Popen(["ip", "-%d" % family, "ro", "ls",
-                                 "table", table], stdout=subprocess.PIPE)
-        routes = ipro.stdout.readlines()
-
-        def_gw = None
-        def_dev = None
-        def_net = None
-
-        for route in routes:
-            match = re.match(r'^default.*via ([^\s]+).*dev ([^\s]+)', route)
-            if match:
-                def_gw, def_dev = match.groups()
-                break
-
-        for route in routes:
-            # Find the least-specific connected route
-            try:
-                def_net = re.match("^([^\\s]+) dev %s" %
-                                   def_dev, route).groups()[0]
-                def_net = IPy.IP(def_net)
-            except:
-                pass
-
-        return Subnet(net=def_net, gw=def_gw, dev=def_dev)
-
-    def parse_binding_file(self, path):
-        """ Read a client configuration from a tap file
-
-        """
-        try:
-            iffile = open(path, 'r')
-        except:
-            return (None, None, None, None)
-        mac = None
-        ips = None
-        link = None
-        hostname = None
-
-        for line in iffile:
-            if line.startswith("IP="):
-                ip = line.strip().split("=")[1]
-                ips = ip.split()
-            elif line.startswith("MAC="):
-                mac = line.strip().split("=")[1]
-            elif line.startswith("LINK="):
-                link = line.strip().split("=")[1]
-            elif line.startswith("HOSTNAME="):
-                hostname = line.strip().split("=")[1]
-
-        return Client(mac=mac, ips=ips, link=link, hostname=hostname)
-
-    def add_iface(self, path):
+    def add_tap(self, path):
         """ Add an interface to monitor
 
         """
-        iface = os.path.basename(path)
+        tap = os.path.basename(path)
 
-        logging.debug("Updating configuration for %s" % iface)
-        binding = self.parse_binding_file(path)
-        ifindex = self.get_ifindex(iface)
+        logging.info("Updating configuration for %s", tap)
+        b = parse_binding_file(path)
+        if b is None:
+            return
+        ifindex = self.get_ifindex(b.tap)
 
         if ifindex is None:
-            logging.warn("Stale configuration for %s found" % iface)
+            logging.warn(" - Stale configuration for %s found", tap)
         else:
-            if binding.is_valid():
-                binding.iface = iface
-                self.clients[binding.mac] = binding
-                self.subnets[binding.link] = self.parse_routing_table(
-                                                binding.link)
-                logging.debug("Added client %s on %s" %
-                              (binding.hostname, iface))
-                self.ifaces[ifindex] = iface
-                self.v6nets[iface] = self.parse_routing_table(binding.link, 6)
-
-    def remove_iface(self, iface):
+            if b.is_valid():
+                if self.mac_indexed_clients:
+                    self.clients[b.mac] = b
+                    k = b.mac
+                else:
+                    self.clients[ifindex] = b
+                    k = ifindex
+                logging.info(" - Added client:")
+                logging.info(" + %10s | %20s %20s %10s %20s %40s",
+                             k, b.hostname, b.mac, b.tap, b.ip, b.eui64)
+
+    def remove_tap(self, tap):
         """ Cleanup clients on a removed interface
 
         """
-        if iface in self.v6nets:
-            del self.v6nets[iface]
-
-        for mac in self.clients.keys():
-            if self.clients[mac].iface == iface:
-                del self.clients[mac]
+        try:
+            for k, cl in self.clients.items():
+                if cl.tap == tap:
+                    logging.info("Removing client %s and closing socket on %s",
+                                 cl.hostname, cl.tap)
+                    logging.info(" - %10s | %20s %20s %10s %20s %40s",
+                                 k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
+                    cl.socket.close()
+                    del self.clients[k]
+        except:
+            logging.debug("Client on %s disappeared!!!", tap)
 
-        for ifindex in self.ifaces.keys():
-            if self.ifaces[ifindex] == iface:
-                del self.ifaces[ifindex]
 
-        logging.debug("Removed interface %s" % iface)
-
-    def dhcp_response(self, i, payload):
-        """ Generate a reply to a BOOTP/DHCP request
+    def dhcp_response(self, arg1, arg2=None):  # pylint: disable=W0613,R0914
+        """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
 
         """
+        logging.info(" * Processing pending DHCP request")
+        # Workaround for supporting both squeezy's nfqueue-bindings-python
+        # and wheezy's python-nfqueue because for some reason the function's
+        # signature has changed and has broken compatibility
+        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
+        if arg2:
+            payload = arg2
+        else:
+            payload = arg1
         # Decode the response - NFQUEUE relays IP packets
         pkt = IP(payload.get_data())
-
-        # Get the actual interface from the ifindex
-        iface = self.ifaces[payload.get_indev()]
-
-        # Signal the kernel that it shouldn't further process the packet
-        payload.set_verdict(nfqueue.NF_DROP)
+        #logging.debug(pkt.show())
 
         # Get the client MAC address
         resp = pkt.getlayer(BOOTP).copy()
         hlen = resp.hlen
         mac = resp.chaddr[:hlen].encode("hex")
-        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
+        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
 
         # Server responses are always BOOTREPLYs
         resp.op = "BOOTREPLY"
         del resp.payload
 
-        try:
-            binding = self.clients[mac]
-        except KeyError:
-            logging.warn("Invalid client %s on %s" % (mac, iface))
+        indev = get_indev(payload)
+
+        binding = self.get_binding(indev, mac)
+        if binding is None:
+            # We don't know anything about this interface, so accept the packet
+            # and return
+            logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
+            # We don't know what to do with this packet, so let the kernel
+            # handle it
+            payload.set_verdict(nfqueue.NF_ACCEPT)
             return
 
-        if iface != binding.iface:
-            logging.warn("Received spoofed DHCP request for %s from interface"
-                         " %s instead of %s" %
-                         (mac, iface, binding.iface))
+        # Signal the kernel that it shouldn't further process the packet
+        payload.set_verdict(nfqueue.NF_DROP)
+
+        if mac != binding.mac:
+            logging.warn(" - Recieved spoofed DHCP request: mac %s, indev %s",
+                         mac, indev)
             return
 
-        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
+        if not binding.ip:
+            logging.info(" - No IP found in binding file.")
+            return
+
+        logging.info(" - Generating DHCP response:"
+                     " host %s, mac %s, tap %s, indev %s",
+                       binding.hostname, mac, binding.tap, indev)
+
+
+        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
                IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
                UDP(sport=pkt.dport, dport=pkt.sport)/resp
-        subnet = self.subnets[binding.link]
+        subnet = binding.net
 
         if not DHCP in pkt:
-            logging.warn("Invalid request from %s on %s, no DHCP"
-                         " payload found" % (binding.mac, iface))
+            logging.warn(" - Invalid request from %s on %s, no DHCP"
+                         " payload found", binding.mac, binding.tap)
             return
 
         dhcp_options = []
@@ -391,43 +649,47 @@ class VMNetProxy(object):
             if type(opt) is tuple and opt[0] == "requested_addr":
                 requested_addr = opt[1]
 
-        logging.info("%s from %s on %s" %
-                    (DHCP_TYPES.get(req_type, "UNKNOWN"), binding.mac, iface))
+        logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
+                     binding.mac, binding.tap)
+
+        if self.dhcp_domain:
+            domainname = self.dhcp_domain
+        else:
+            domainname = binding.hostname.split('.', 1)[-1]
 
         if req_type == DHCPREQUEST and requested_addr != binding.ip:
             resp_type = DHCPNAK
-            logging.info("Sending DHCPNAK to %s on %s: requested %s"
-                         " instead of %s" %
-                         (binding.mac, iface, requested_addr, binding.ip))
+            logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
+                         " instead of %s", binding.mac, binding.tap,
+                         requested_addr, binding.ip)
 
         elif req_type in (DHCPDISCOVER, DHCPREQUEST):
             resp_type = DHCP_REQRESP[req_type]
-            resp.yiaddr = self.clients[mac].ip
+            resp.yiaddr = binding.ip
             dhcp_options += [
                  ("hostname", binding.hostname),
-                 ("domain", binding.hostname.split('.', 1)[-1]),
-                 ("router", subnet.gw),
-                 ("name_server", "194.177.210.10"),
-                 ("name_server", "194.177.210.211"),
+                 ("domain", domainname),
                  ("broadcast_address", str(subnet.broadcast)),
                  ("subnet_mask", str(subnet.netmask)),
-                 ("renewal_time", DEFAULT_RENEWAL_TIME),
-                 ("lease_time", DEFAULT_LEASE_TIME),
+                 ("renewal_time", self.lease_renewal),
+                 ("lease_time", self.lease_lifetime),
             ]
+            if subnet.gw:
+                dhcp_options += [("router", subnet.gw)]
+            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
 
         elif req_type == DHCPINFORM:
             resp_type = DHCP_REQRESP[req_type]
             dhcp_options += [
                  ("hostname", binding.hostname),
-                 ("domain", binding.hostname.split('.', 1)[-1]),
-                 ("name_server", "194.177.210.10"),
-                 ("name_server", "194.177.210.211"),
+                 ("domain", domainname),
             ]
+            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
 
         elif req_type == DHCPRELEASE:
             # Log and ignore
-            logging.info("DHCPRELEASE from %s on %s" %
-                         (binding.mac, iface))
+            logging.info(" - DHCPRELEASE from %s on %s",
+                         binding.hostname, binding.tap)
             return
 
         # Finally, always add the server identifier and end options
@@ -438,176 +700,448 @@ class VMNetProxy(object):
         ]
         resp /= DHCP(options=dhcp_options)
 
-        logging.info("%s to %s (%s) on %s" %
-                      (DHCP_TYPES[resp_type], mac, binding.ip, iface))
-        sendp(resp, iface=iface, verbose=False)
+        logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
+                     binding.ip, binding.tap)
+        try:
+            binding.sendp(resp)
+        except socket.error, e:
+            logging.warn(" - DHCP response on %s (%s) failed: %s",
+                         binding.tap, binding.hostname, str(e))
+        except Exception, e:
+            logging.warn(" - Unkown error during DHCP response on %s (%s): %s",
+                         binding.tap, binding.hostname, str(e))
+
+    def dhcpv6_response(self, arg1, arg2=None):  # pylint: disable=W0613
+
+        logging.info(" * Processing pending DHCPv6 request")
+        # Workaround for supporting both squeezy's nfqueue-bindings-python
+        # and wheezy's python-nfqueue because for some reason the function's
+        # signature has changed and has broken compatibility
+        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
+        if arg2:
+            payload = arg2
+        else:
+            payload = arg1
+        pkt = IPv6(payload.get_data())
+        indev = get_indev(payload)
+
+        #TODO: figure out how to find the src mac
+        mac = None
+        binding = self.get_binding(indev, mac)
+        if binding is None:
+            # We don't know anything about this interface, so accept the packet
+            # and return
+            logging.debug(" - Ignoring dhcpv6 request for mac %s", mac)
+            # We don't know what to do with this packet, so let the kernel
+            # handle it
+            payload.set_verdict(nfqueue.NF_ACCEPT)
+            return
+
+        # Signal the kernel that it shouldn't further process the packet
+        payload.set_verdict(nfqueue.NF_DROP)
+
+        subnet = binding.net6
+
+        if subnet.net is None:
+            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
+            return
+
+        indevmac = self.get_iface_hw_addr(binding.indev)
+        ifll = subnet.make_ll64(indevmac)
+        if ifll is None:
+            return
+
+        ofll = subnet.make_ll64(binding.mac)
+        if ofll is None:
+            return
+
+        logging.info(" - Generating DHCPv6 response for host %s (mac %s) on tap %s",
+                      binding.hostname, binding.mac, binding.tap)
+
+        if self.dhcpv6_domains:
+            domains = self.dhcpv6_domains
+        else:
+            domains = [binding.hostname.split('.', 1)[-1]]
+
+        # We do this in order not to caclulate optlen ourselves
+        dnsdomains = str(DHCP6OptDNSDomains(dnsdomains=domains))
+        dnsservers = str(DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers))
+
+        resp = Ether(src=indevmac, dst=binding.mac)/\
+               IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
+               UDP(sport=pkt.dport, dport=pkt.sport)/\
+               DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
+               DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
+               DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
+               DHCP6OptDNSDomains(dnsdomains)/\
+               DHCP6OptDNSServers(dnsservers)
 
-    def rs_response(self, i, payload):
+        try:
+            binding.sendp(resp)
+        except socket.error, e:
+            logging.warn(" - DHCPv6 on %s (%s) failed: %s",
+                         binding.tap, binding.hostname, str(e))
+        except Exception, e:
+            logging.warn(" - Unkown error during DHCPv6 on %s (%s): %s",
+                         binding.tap, binding.hostname, str(e))
+
+
+    def rs_response(self, arg1, arg2=None):  # pylint: disable=W0613
         """ Generate a reply to a BOOTP/DHCP request
 
         """
-        # Get the actual interface from the ifindex
-        iface = self.ifaces[payload.get_indev()]
-        ifmac = self.get_iface_hw_addr(iface)
-        subnet = self.v6nets[iface]
-        ifll = subnet.make_ll64(ifmac)
+        logging.info(" * Processing pending RS request")
+        # Workaround for supporting both squeezy's nfqueue-bindings-python
+        # and wheezy's python-nfqueue because for some reason the function's
+        # signature has changed and has broken compatibility
+        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
+        if arg2:
+            payload = arg2
+        else:
+            payload = arg1
+        pkt = IPv6(payload.get_data())
+        #logging.debug(pkt.show())
+        try:
+            mac = pkt.lladdr
+        except:
+            logging.debug(" - Cannot obtain lladdr in rs")
+            return
+
+        indev = get_indev(payload)
+
+        binding = self.get_binding(indev, mac)
+        if binding is None:
+            # We don't know anything about this interface, so accept the packet
+            # and return
+            logging.debug(" - Ignoring router solicitation on for mac %s", mac)
+            # We don't know what to do with this packet, so let the kernel
+            # handle it
+            payload.set_verdict(nfqueue.NF_ACCEPT)
+            return
 
         # Signal the kernel that it shouldn't further process the packet
         payload.set_verdict(nfqueue.NF_DROP)
 
-        resp = Ether(src=self.get_iface_hw_addr(iface))/\
-               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
+        if mac != binding.mac:
+            logging.warn(" - Received spoofed RS request: mac %s, tap %s",
+                         mac, binding.tap)
+            return
+
+        subnet = binding.net6
+
+        if subnet.net is None:
+            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
+            return
+
+        indevmac = self.get_iface_hw_addr(binding.indev)
+        ifll = subnet.make_ll64(indevmac)
+        if ifll is None:
+            return
+
+        logging.info(" - Generating RA for host %s (mac %s) on tap %s",
+                      binding.hostname, mac, binding.tap)
+
+        resp = Ether(src=indevmac)/\
+               IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
                ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                      prefixlen=subnet.prefixlen)
 
-        logging.info("RA on %s for %s" % (iface, subnet.net))
-        sendp(resp, iface=iface, verbose=False)
+        if self.ipv6_nameservers:
+            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
+                                     lifetime=self.ra_period * 3)
 
-    def ns_response(self, i, payload):
+        try:
+            binding.sendp(resp)
+        except socket.error, e:
+            logging.warn(" - RA on %s (%s) failed: %s",
+                         binding.tap, binding.hostname, str(e))
+        except Exception, e:
+            logging.warn(" - Unkown error during RA on %s (%s): %s",
+                         binding.tap, binding.hostname, str(e))
+
+    def ns_response(self, arg1, arg2=None):  # pylint: disable=W0613
         """ Generate a reply to an ICMPv6 neighbor solicitation
 
         """
-        # Get the actual interface from the ifindex
-        iface = self.ifaces[payload.get_indev()]
-        ifmac = self.get_iface_hw_addr(iface)
-        subnet = self.v6nets[iface]
-        ifll = subnet.make_ll64(ifmac)
+
+        logging.info(" * Processing pending NS request")
+        # Workaround for supporting both squeezy's nfqueue-bindings-python
+        # and wheezy's python-nfqueue because for some reason the function's
+        # signature has changed and has broken compatibility
+        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
+        if arg2:
+            payload = arg2
+        else:
+            payload = arg1
 
         ns = IPv6(payload.get_data())
+        #logging.debug(ns.show())
+        try:
+            mac = ns.lladdr
+        except:
+            logging.debug(" - Cannot obtain lladdr from ns")
+            return
 
-        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
-            logging.debug("Received NS for a non-routable IP (%s)" % ns.tgt)
+
+        indev = get_indev(payload)
+
+        binding = self.get_binding(indev, mac)
+        if binding is None:
+            # We don't know anything about this interface, so accept the packet
+            # and return
+            logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
+                          ns.tgt)
+            # We don't know what to do with this packet, so let the kernel
+            # handle it
             payload.set_verdict(nfqueue.NF_ACCEPT)
-            return 1
+            return
 
         payload.set_verdict(nfqueue.NF_DROP)
 
-        try:
-            client_lladdr = ns.lladdr
-        except AttributeError:
+        if mac != binding.mac:
+            logging.warn(" - Received spoofed NS request"
+                         " for mac %s from tap %s", mac, binding.tap)
+            return
+
+        subnet = binding.net6
+        if subnet.net is None:
+            logging.debug(" - No IPv6 network assigned for the interface")
+            return
+
+        indevmac = self.get_iface_hw_addr(binding.indev)
+
+        ifll = subnet.make_ll64(indevmac)
+        if ifll is None:
+            return
+
+        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
+            logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
             return 1
 
-        resp = Ether(src=ifmac, dst=client_lladdr)/\
+        logging.info(" - Generating NA for host %s (mac %s) on tap %s",
+                     binding.hostname, mac, binding.tap)
+
+        resp = Ether(src=indevmac, dst=binding.mac)/\
                IPv6(src=str(ifll), dst=ns.src)/\
                ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
-               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
+               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
 
-        logging.info("NA on %s for %s" % (iface, ns.tgt))
-        sendp(resp, iface=iface, verbose=False)
-        return 1
+        try:
+            binding.sendp(resp)
+        except socket.error, e:
+            logging.warn(" - NA on %s (%s) failed: %s",
+                         binding.tap, binding.hostname, str(e))
+        except Exception, e:
+            logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
+                         binding.tap, binding.hostname, str(e))
 
     def send_periodic_ra(self):
-        logging.debug("Sending out periodic RAs")
+        # Use a separate thread as this may take a _long_ time with
+        # many interfaces and we want to be responsive in the mean time
+        threading.Thread(target=self._send_periodic_ra).start()
+
+    def _send_periodic_ra(self):
+        logging.info("Sending out periodic RAs")
         start = time.time()
         i = 0
-        for client in self.clients.values():
-            iface = client.iface
-            ifmac = self.get_iface_hw_addr(iface)
-            if not ifmac:
+        for binding in self.clients.values():
+            tap = binding.tap
+            indev = binding.indev
+            # mac = binding.mac
+            subnet = binding.net6
+            if subnet.net is None:
+                logging.debug(" - Skipping periodic RA on interface %s,"
+                              " as it is not IPv6-connected", tap)
                 continue
-
-            subnet = self.v6nets[iface]
-            ifll = subnet.make_ll64(ifmac)
-            resp = Ether(src=ifmac)/\
-                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
+            indevmac = self.get_iface_hw_addr(indev)
+            ifll = subnet.make_ll64(indevmac)
+            if ifll is None:
+                continue
+            resp = Ether(src=indevmac)/\
+                   IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
                    ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                          prefixlen=subnet.prefixlen)
+            if self.ipv6_nameservers:
+                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
+                                         lifetime=self.ra_period * 3)
             try:
-                sendp(resp, iface=iface, verbose=False)
-            except:
-                logging.debug("Periodic RA on %s failed" % iface)
+                binding.sendp(resp)
+            except socket.error, e:
+                logging.warn(" - Periodic RA on %s (%s) failed: %s",
+                             tap, binding.hostname, str(e))
+            except Exception, e:
+                logging.warn(" - Unkown error during periodic RA on %s (%s):"
+                             " %s", tap, binding.hostname, str(e))
             i += 1
-        logging.debug("Sent %d RAs in %.2f seconds" % (i, time.time() - start))
+        logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
 
     def serve(self):
+        """ Safely perform the main loop, freeing all resources upon exit
+
+        """
+        try:
+            self._serve()
+        finally:
+            self._cleanup()
+
+    def _serve(self):
         """ Loop forever, serving DHCP requests
 
         """
         self.build_config()
 
-        iwfd = self.notifier._fd
+        # Yes, we are accessing _fd directly, but it's the only way to have a
+        # single select() loop ;-)
+        iwfd = self.notifier._fd  # pylint: disable=W0212
 
         start = time.time()
-        timeout = PERIODIC_RA_TIMEOUT
-        self.send_periodic_ra()
+        if self.ipv6_enabled:
+            timeout = self.ra_period
+            self.send_periodic_ra()
+        else:
+            timeout = None
 
         while True:
-            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
-            if xlist:
-                logging.warn("Warning: Exception on %s" %
-                             ", ".join([ str(fd) for fd in xlist]))
+            try:
+                rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
+                                                [], [], timeout)
+            except select.error, e:
+                if e[0] == errno.EINTR:
+                    logging.debug("select() got interrupted")
+                    continue
 
-            # First check if there are any inotify (= configuration change)
-            # events
-            if not (rlist or xlist):
-                # We were woken up by a timeout
-                start = time.time()
-                self.send_periodic_ra()
+            if xlist:
+                logging.warn("Warning: Exception on %s",
+                             ", ".join([str(fd) for fd in xlist]))
 
-            else:
+            if rlist:
                 if iwfd in rlist:
+                # First check if there are any inotify (= configuration change)
+                # events
                     self.notifier.read_events()
                     self.notifier.process_events()
                     rlist.remove(iwfd)
 
+                logging.debug("Pending requests on fds %s", rlist)
+
                 for fd in rlist:
                     try:
-                        self.nfq[fd].process_pending()
-                    except e, msg:
-                        logging.warn("Error processing fd %d: %s" % (fd, e))
-
-            # Calculate the new timeout
-            timeout = PERIODIC_RA_TIMEOUT - (time.time() - start)
-
-            # Just to be safe we won't miss anything
-            if timeout <= 0:
-                logging.debug("Send extra RAs")
-                self.send_periodic_ra()
-                timeout = PERIODIC_RA_TIMEOUT
+                        q, num = self.nfq[fd]
+                        cnt = q.process_pending(num)
+                        logging.debug(" * Processed %d requests on NFQUEUE"
+                                      " with fd %d", cnt, fd)
+                    except RuntimeError, e:
+                        logging.warn("Error processing fd %d: %s", fd, str(e))
+                    except Exception, e:
+                        logging.warn("Unknown error processing fd %d: %s",
+                                     fd, str(e))
+
+            if self.ipv6_enabled:
+                # Calculate the new timeout
+                timeout = self.ra_period - (time.time() - start)
+
+                if timeout <= 0:
+                    start = time.time()
+                    self.send_periodic_ra()
+                    timeout = self.ra_period - (time.time() - start)
+
+    def print_clients(self):
+        logging.info("%10s   %20s %20s %10s %20s %40s",
+                     'Key', 'Client', 'MAC', 'TAP', 'IP', 'IPv6')
+        for k, cl in self.clients.items():
+            logging.info("%10s | %20s %20s %10s %20s %40s",
+                         k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
 
 
 
 if __name__ == "__main__":
+    import capng
     import optparse
-    from capng import *
+    from cStringIO import StringIO
     from pwd import getpwnam, getpwuid
+    from configobj import ConfigObj, ConfigObjError, flatten_errors
+
+    import validate
+
+    validator = validate.Validator()
+
+    def is_ip_list(value, family=4):
+        try:
+            family = int(family)
+        except ValueError:
+            raise validate.VdtParamError(family)
+        if isinstance(value, (str, unicode)):
+            value = [value]
+        if not isinstance(value, list):
+            raise validate.VdtTypeError(value)
+
+        for entry in value:
+            try:
+                ip = IPy.IP(entry)
+            except ValueError:
+                raise validate.VdtValueError(entry)
+
+            if ip.version() != family:
+                raise validate.VdtValueError(entry)
+        return value
+
+    validator.functions["ip_addr_list"] = is_ip_list
+    config_spec = StringIO(CONFIG_SPEC)
 
     parser = optparse.OptionParser()
-    parser.add_option("-p", "--path", dest="data_path",
-                      help="The location of the data files", metavar="DIR",
-                      default=DEFAULT_PATH)
-    parser.add_option("-c", "--dhcp-queue", dest="dhcp_queue",
-                      help="The nfqueue to receive DHCP requests from"
-                           " (default: %d" % DEFAULT_NFQUEUE_NUM, type="int",
-                      metavar="NUM", default=DEFAULT_NFQUEUE_NUM)
-    parser.add_option("-r", "--rs-queue", dest="rs_queue",
-                      help="The nfqueue to receive IPv6 router"
-                           " solicitations from (default: %d)" %
-                           DEFAULT_NFQUEUE_NUM, type="int",
-                      metavar="NUM", default=DEFAULT_NFQUEUE_NUM)
-    parser.add_option("-n", "--ns-queue", dest="ns_queue",
-                      help="The nfqueue to receive IPv6 neighbor"
-                           " solicitations from (default: %d)" %
-                           DEFAULT_NFQUEUE_NUM, type="int",
-                      metavar="NUM", default=44)
-    parser.add_option("-u", "--user", dest="user",
-                      help="An unprivileged user to run as",
-                      metavar="UID", default=DEFAULT_USER)
+    parser.add_option("-c", "--config", dest="config_file",
+                      help="The location of the data files", metavar="FILE",
+                      default=DEFAULT_CONFIG)
     parser.add_option("-d", "--debug", action="store_true", dest="debug",
                       help="Turn on debugging messages")
-    parser.add_option("-f", "--foreground", action="store_false", dest="daemonize",
-                      default=True, help="Do not daemonize, stay in the foreground")
-
+    parser.add_option("-f", "--foreground", action="store_false",
+                      dest="daemonize", default=True,
+                      help="Do not daemonize, stay in the foreground")
 
     opts, args = parser.parse_args()
 
-    if opts.daemonize:
-        d = daemon.DaemonContext()
-        d.open()
+    try:
+        config = ConfigObj(opts.config_file, configspec=config_spec)
+    except ConfigObjError, err:
+        sys.stderr.write("Failed to parse config file %s: %s" %
+                         (opts.config_file, str(err)))
+        sys.exit(1)
+
+    results = config.validate(validator)
+    if results != True:
+        logging.fatal("Configuration file validation failed! See errors below:")
+        for (section_list, key, unused) in flatten_errors(config, results):
+            if key is not None:
+                logging.fatal(" '%s' in section '%s' failed validation",
+                              key, ", ".join(section_list))
+            else:
+                logging.fatal(" Section '%s' is missing",
+                              ", ".join(section_list))
+        sys.exit(1)
 
-    pidfile = open("/var/run/nfdhcpd.pid", "w")
-    pidfile.write("%s" % os.getpid())
-    pidfile.close()
+    try:
+        uid = getpwuid(config["general"].as_int("user"))
+    except ValueError:
+        uid = getpwnam(config["general"]["user"])
+
+    # Keep only the capabilities we need
+    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
+    # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
+    # CAP_SETPCAP: needed by capng_change_id()
+    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
+    capng.capng_update(capng.CAPNG_ADD,
+                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
+                       capng.CAP_NET_ADMIN)
+    capng.capng_update(capng.CAPNG_ADD,
+                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
+                       capng.CAP_NET_RAW)
+    capng.capng_update(capng.CAPNG_ADD,
+                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
+                       capng.CAP_SETPCAP)
+    # change uid
+    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
+                          capng.CAPNG_DROP_SUPP_GRP | \
+                          capng.CAPNG_CLEAR_BOUNDING)
 
     logger = logging.getLogger()
     if opts.debug:
@@ -616,33 +1150,88 @@ if __name__ == "__main__":
         logger.setLevel(logging.INFO)
 
     if opts.daemonize:
-        handler = logging.handlers.RotatingFileHandler(LOG_FILENAME,
-                                                       maxBytes=2097152)
+        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
+        handler = logging.handlers.WatchedFileHandler(logfile)
     else:
         handler = logging.StreamHandler()
 
     handler.setFormatter(logging.Formatter(LOG_FORMAT))
     logger.addHandler(handler)
 
+    # Rename this process so 'ps' output looks like
+    # this is a native executable.
+    # NOTE: due to a bug in python-setproctitle, one cannot yet
+    # set individual values for command-line arguments, so only show
+    # the name of the executable instead.
+    # setproctitle.setproctitle("\x00".join(sys.argv))
+    setproctitle.setproctitle(sys.argv[0])
+
+    if opts.daemonize:
+        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
+            config["general"]["pidfile"], 10)
+        # Remove any stale PID files, left behind by previous invocations
+        if daemon.runner.is_pidfile_stale(pidfile):
+            logger.warning("Removing stale PID lock file %s", pidfile.path)
+            pidfile.break_lock()
+
+        d = daemon.DaemonContext(pidfile=pidfile,
+                                 umask=0022,
+                                 stdout=handler.stream,
+                                 stderr=handler.stream,
+                                 files_preserve=[handler.stream])
+        try:
+            d.open()
+        except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
+            logger.critical("Failed to lock pidfile %s,"
+                            " another instance running?", pidfile.path)
+            sys.exit(1)
+
     logging.info("Starting up")
-    proxy = VMNetProxy(opts.data_path, opts.dhcp_queue,
-                       opts.rs_queue, opts.ns_queue)
+    logging.info("Running as %s (uid:%d, gid: %d)",
+                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
+
+    proxy_opts = {}
+    if config["dhcp"].as_bool("enable_dhcp"):
+        proxy_opts.update({
+            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
+            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
+            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
+            "dhcp_server_ip": config["dhcp"]["server_ip"],
+            "dhcp_nameservers": config["dhcp"]["nameservers"],
+            "dhcp_domain": config["dhcp"]["domain"],
+        })
+
+    if config["ipv6"].as_bool("enable_ipv6"):
+        proxy_opts.update({
+            "dhcpv6_queue_num": config["ipv6"].as_int("dhcp_queue"),
+            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
+            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
+            "ra_period": config["ipv6"].as_int("ra_period"),
+            "ipv6_nameservers": config["ipv6"]["nameservers"],
+            "dhcpv6_domains": config["ipv6"]["domains"],
+        })
+
+    # pylint: disable=W0142
+    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
 
-    # Drop all capabilities except CAP_NET_RAW and change uid
-    try:
-        uid = getpwuid(int(opts.user))
-    except ValueError:
-        uid = getpwnam(opts.user)
-
-    logging.info("Setting capabilities and changing uid")
-    logging.debug("User: %s, uid: %d, gid: %d" %
-                  (opts.user, uid.pw_uid, uid.pw_gid))
-    capng_clear(CAPNG_SELECT_BOTH)
-    capng_update(CAPNG_ADD, CAPNG_EFFECTIVE|CAPNG_PERMITTED, CAP_NET_RAW)
-    capng_change_id(uid.pw_uid, uid.pw_gid,
-                    CAPNG_DROP_SUPP_GRP | CAPNG_CLEAR_BOUNDING)
     logging.info("Ready to serve requests")
-    proxy.serve()
+
+
+    def debug_handler(signum, _):
+        logging.debug('Received signal %d. Printing proxy state...', signum)
+        proxy.print_clients()
+
+    # Set the signal handler for debuging clients
+    signal.signal(signal.SIGUSR1, debug_handler)
+    signal.siginterrupt(signal.SIGUSR1, False)
+
+    try:
+        proxy.serve()
+    except Exception:
+        if opts.daemonize:
+            exc = "".join(traceback.format_exception(*sys.exc_info()))
+            logging.critical(exc)
+        raise
 
 
 # vim: set ts=4 sts=4 sw=4 et :