Refactor nfdhcpd to support get_physindev()
[snf-nfdhcpd] / nfdhcpd
diff --git a/nfdhcpd b/nfdhcpd
index e6b26aa..c7396c0 100755 (executable)
--- a/nfdhcpd
+++ b/nfdhcpd
@@ -27,9 +27,11 @@ import time
 import logging
 import logging.handlers
 import threading
+import traceback
 import subprocess
 
 import daemon
+import daemon.pidlockfile
 import nfqueue
 import pyinotify
 
@@ -38,6 +40,8 @@ import socket
 from select 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 IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
@@ -45,7 +49,6 @@ from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
                                ICMPv6NDOptPrefixInfo, \
                                ICMPv6NDOptRDNSS
 from scapy.layers.dhcp import BOOTP, DHCP
-from scapy.sendrecv import sendp
 
 DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
@@ -76,6 +79,7 @@ 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)
@@ -113,40 +117,32 @@ DHCP_REQRESP = {
     }
 
 
-def parse_routing_table(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
-        m = re.match("^([^\\s]+) dev %s" % def_dev, route)
-        if not m:
-            continue
-        def_net = m.groups(1)
+def get_indev(payload):
+    try:
+        indev_ifindex = payload.get_physindev()
+        if indev_ifindex:
+            logging.debug("Incomming packet from bridge %s", indev_ifindex)
+            return indev_ifindex
+    except AttributeError:
+        #TODO: return error value
+        logging.debug("No get_physindev supported")
+        return 0
 
-        try:
-            def_net = IPy.IP(def_net)
-        except ValueError, e:
-            logging.warn("Unable to parse default route entry %s: %s",
-                         def_net, str(e))
+    indev_ifindex = payload.get_indev()
+    logging.debug("Incomming packet from tap %s", indev_ifindex)
 
-    return Subnet(net=def_net, gw=def_gw, dev=def_dev)
+    return indev_ifindex
 
+def get_binding(proxy, ifindex, mac):
+    try:
+        if proxy.mac_indexed_clients:
+            b = proxy.clients[mac]
+        else:
+            b = proxy.clients[ifindex]
+        return b
+    except KeyError:
+        logging.debug("No client found for mac/ifindex %s/%s", mac, ifindex)
+        return None
 
 def parse_binding_file(path):
     """ Read a client configuration from a tap file
@@ -156,63 +152,89 @@ def parse_binding_file(path):
         iffile = open(path, 'r')
     except EnvironmentError, e:
         logging.warn("Unable to open binding file %s: %s", path, str(e))
-        return (None, None, None, None)
+        return None
 
+    tap = os.path.basename(path)
+    indev = None
     mac = None
-    ips = None
-    link = 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 = line.strip().split("=")[1]
-            ips = ip.split()
+            ip = get_value(line)
         elif line.startswith("MAC="):
-            mac = line.strip().split("=")[1]
-        elif line.startswith("LINK="):
-            link = line.strip().split("=")[1]
+            mac = get_value(line)
         elif line.startswith("HOSTNAME="):
-            hostname = line.strip().split("=")[1]
-
-    return Client(mac=mac, ips=ips, link=link, hostname=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)
+
+    return Client(tap=tap, mac=mac, ip=ip,
+                  hostname=hostname, indev=indev, subnet=subnet,
+                  gateway=gateway, subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
 
 class ClientFileHandler(pyinotify.ProcessEvent):
     def __init__(self, server):
         pyinotify.ProcessEvent.__init__(self)
         self.server = server
 
-    def process_IN_DELETE(self, event):
+    def process_IN_DELETE(self, event): # pylint: disable=C0103
         """ Delete file handler
 
         Currently this removes an interface from the watch list
 
         """
-        self.server.remove_iface(event.name)
+        self.server.remove_tap(event.name)
 
-    def process_IN_CLOSE_WRITE(self, event):
+    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
         """ Add file handler
 
         Currently this adds an interface to the watch list
 
         """
-        self.server.add_iface(os.path.join(event.path, event.name))
+        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
 
     def is_valid(self):
-        return self.mac is not None and self.ips is not None\
+        return self.mac is not None and self.ip is not None\
                and self.hostname is not None
 
 
@@ -285,12 +307,19 @@ class VMNetProxy(object): # pylint: disable=R0902
                  rs_queue_num=None, ns_queue_num=None,
                  dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
                  dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
+                 dhcp_domain='',
                  dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
                  ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=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:
@@ -306,10 +335,13 @@ class VMNetProxy(object): # pylint: disable=R0902
         self.ipv6_enabled = False
 
         self.clients = {}
-        self.subnets = {}
-        self.ifaces = {}
-        self.v6nets = {}
+        #self.subnets = {}
+        #self.ifaces = {}
+        #self.v6nets = {}
         self.nfq = {}
+        self.l2socket = socket.socket(socket.AF_PACKET,
+                                      socket.SOCK_RAW, ETH_P_ALL)
+        self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
 
         # Inotify setup
         self.wm = pyinotify.WatchManager()
@@ -331,6 +363,24 @@ class VMNetProxy(object): # pylint: disable=R0902
             self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
             self.ipv6_enabled = True
 
+    def _cleanup(self):
+        """ Free all resources for a graceful exit
+
+        """
+        logging.info("Cleaning up")
+
+        logging.debug("Closing netfilter queues")
+        for q in self.nfq.values():
+            q.close()
+
+        logging.debug("Closing socket")
+        self.l2socket.close()
+
+        logging.debug("Stopping inotify watches")
+        self.notifier.stop()
+
+        logging.info("Cleanup finished")
+
     def _setup_nfqueue(self, queue_num, family, callback):
         logging.debug("Setting up NFQUEUE for queue %d, AF %s",
                       queue_num, family)
@@ -342,12 +392,26 @@ class VMNetProxy(object): # pylint: disable=R0902
         q.set_mode(nfqueue.NFQNL_COPY_PACKET)
         self.nfq[q.get_fd()] = q
 
+    def sendp(self, data, dev):
+        """ Send a raw packet using a layer-2 socket
+
+        """
+        logging.debug("%s", data)
+        if isinstance(data, BasePacket):
+            data = str(data)
+
+        self.l2socket.bind((dev, ETH_P_ALL))
+        count = self.l2socket.send(data)
+        ldata = len(data)
+        if count != ldata:
+            logging.warn("Truncated send on %s (%d/%d bytes sent)",
+                         dev, count, ldata)
+
     def build_config(self):
         self.clients.clear()
-        self.subnets.clear()
 
         for path in glob.glob(os.path.join(self.data_path, "*")):
-            self.add_iface(path)
+            self.add_tap(path)
 
     def get_ifindex(self, iface):
         """ Get the interface index from sysfs
@@ -363,7 +427,7 @@ class VMNetProxy(object): # pylint: disable=R0902
             f = open(path, 'r')
         except EnvironmentError:
             logging.debug("%s is probably down, removing", iface)
-            self.remove_iface(iface)
+            self.remove_tap(iface)
 
             return ifindex
 
@@ -377,7 +441,7 @@ class VMNetProxy(object): # pylint: disable=R0902
         except EnvironmentError, e:
             logging.warn("Error reading %s's ifindex from sysfs: %s",
                          iface, str(e))
-            self.remove_iface(iface)
+            self.remove_tap(iface)
         finally:
             f.close()
 
@@ -397,7 +461,7 @@ class VMNetProxy(object): # pylint: disable=R0902
             f = open(path, 'r')
         except EnvironmentError:
             logging.debug("%s is probably down, removing", iface)
-            self.remove_iface(iface)
+            self.remove_tap(iface)
             return addr
 
         try:
@@ -410,56 +474,57 @@ class VMNetProxy(object): # pylint: disable=R0902
 
         return addr
 
-    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 = parse_binding_file(path)
-        ifindex = self.get_ifindex(iface)
+        logging.debug("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] = parse_routing_table(binding.link)
-                logging.debug("Added client %s on %s", binding.hostname, iface)
-                self.ifaces[ifindex] = iface
-                self.v6nets[iface] = 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
+                else:
+                    self.clients[ifindex] = b
+                logging.debug("Added client:")
+                logging.debug("%5s: %10s %20s %7s %15s",
+                               ifindex, b.hostname, b.mac, b.tap, b.ip)
+        logging.debug("\n\n\n\n\n")
+        logging.debug("%10s %20s %7s %15s", 'Client', 'MAC', 'TAP', 'IP')
+        for b in self.clients.values():
+            logging.debug("%10s %20s %7s %15s", b.hostname, b.mac, b.tap, b.ip)
+
+    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]
-
-        for ifindex in self.ifaces.keys():
-            if self.ifaces[ifindex] == iface:
-                del self.ifaces[ifindex]
+        try:
+            for k in self.clients.keys():
+                if self.clients[k].tap = tap:
+                    logging.debug("Removing client on interface %s", tap)
+                    logging.debug("%10s %20s %7s %15s",
+                                  b.hostname, b.mac, b.tap, b.ip)
+                    del self.clients[k]
+        except:
+            logging.debug("Client on %s disappeared!!!", tap)
 
-        logging.debug("Removed interface %s", iface)
 
     def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
-        """ Generate a reply to a BOOTP/DHCP request
+        """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
 
         """
         # 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("IN DHCP RESPONCE")
+        #logging.debug(pkt.show())
 
         # Get the client MAC address
         resp = pkt.getlayer(BOOTP).copy()
@@ -471,25 +536,34 @@ class VMNetProxy(object): # pylint: disable=R0902
         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 = get_binding(self, 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 %d", 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 for mac %s from tap %s", mac, tap)
             return
 
-        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
+        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)
+                         " payload found", binding.mac, binding.tap)
             return
 
         dhcp_options = []
@@ -501,39 +575,45 @@ class VMNetProxy(object): # pylint: disable=R0902
                 requested_addr = opt[1]
 
         logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
-                     binding.mac, iface)
+                     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)
+                         " 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),
+                 ("domain", domainname),
                  ("broadcast_address", str(subnet.broadcast)),
                  ("subnet_mask", str(subnet.netmask)),
                  ("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]),
+                 ("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.mac, binding.tap )
             return
 
         # Finally, always add the server identifier and end options
@@ -545,23 +625,49 @@ class VMNetProxy(object): # pylint: disable=R0902
         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)
+                     binding.ip, binding.tap)
+        self.sendp(resp, binding.indev)
 
     def rs_response(self, i, payload): # 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)
+        pkt = IPv6(payload.get_data())
+        logging.debug("IN RS RESPONCE")
+        #logging.debug(pkt.show())
+        mac = pkt.lladdr
+        logging.debug("rs for mac %s", mac)
+
+        indev = get_indev(payload)
+
+        binding = get_binding(self, 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))/\
+        if mac != binding.mac:
+            logging.warn("Recieved spoofed RS request for mac %s from tap %s", mac, 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)
+
+
+        resp = Ether(src=indevmac)/\
                IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
                ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                      prefixlen=subnet.prefixlen)
@@ -570,41 +676,60 @@ class VMNetProxy(object): # pylint: disable=R0902
             resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
                                      lifetime=self.ra_period * 3)
 
-        logging.info("RA on %s for %s", iface, subnet.net)
-        sendp(resp, iface=iface, verbose=False)
+        logging.info("RA on %s for %s", binding.indev, subnet.net)
+        self.sendp(resp, binding.indev)
 
     def ns_response(self, i, payload): # 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)
-
         ns = IPv6(payload.get_data())
+        logging.debug("IN NS RESPONCE")
+        logging.debug(ns.show())
+        mac = ns.lladdr
 
-        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)
+        logging.debug("dst %s  tgt %s" , ns.dst, ns.tgt)
+
+        indev = get_indev(payload)
+
+        binding = get_binding(self, 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("Recieved spoofed NS request for mac %s from tap %s", mac, 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 not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
+            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
+            payload.set_verdict(nfqueue.NF_ACCEPT)
             return 1
 
-        resp = Ether(src=ifmac, dst=client_lladdr)/\
+        logging.debug("na ether %s %s", binding.mac, ns.src)
+        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
+        logging.info("NA on %s for %s", binding.indev, ns.tgt)
+        self.sendp(resp, binding.indev)
 
     def send_periodic_ra(self):
         # Use a separate thread as this may take a _long_ time with
@@ -615,15 +740,18 @@ class VMNetProxy(object): # pylint: disable=R0902
         logging.debug("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)/\
+            indevmac = self.get_iface_hw_addr(indev)
+            ifll = subnet.make_ll64(indevmac)
+            resp = Ether(src=indevmac)/\
                    IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
                    ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                          prefixlen=subnet.prefixlen)
@@ -631,22 +759,31 @@ class VMNetProxy(object): # pylint: disable=R0902
                 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
                                          lifetime=self.ra_period * 3)
             try:
-                sendp(resp, iface=iface, verbose=False)
+                self.sendp(resp, indev)
             except socket.error, e:
-                logging.warn("Periodic RA on %s failed: %s", iface, str(e))
+                logging.warn("Periodic RA on %s failed: %s", tap, str(e))
             except Exception, e:
                 logging.warn("Unkown error during periodic RA on %s: %s",
-                             iface, str(e))
+                             tap, str(e))
             i += 1
         logging.debug("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()
 
-        # Yes, we are accessing _fd directly, but it's the only way to have a 
+        # 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
 
@@ -738,11 +875,6 @@ if __name__ == "__main__":
 
     opts, args = parser.parse_args()
 
-    if opts.daemonize:
-        d = daemon.DaemonContext()
-        d.umask = 0022
-        d.open()
-
     try:
         config = ConfigObj(opts.config_file, configspec=config_spec)
     except ConfigObjError, err:
@@ -762,16 +894,33 @@ if __name__ == "__main__":
                               ", ".join(section_list))
         sys.exit(1)
 
-    pidfile = open(config["general"]["pidfile"], "w")
-    pidfile.write("%s" % os.getpid())
-    pidfile.close()
-
     logger = logging.getLogger()
     if opts.debug:
         logger.setLevel(logging.DEBUG)
     else:
         logger.setLevel(logging.INFO)
 
+    if opts.daemonize:
+        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
+        handler = logging.handlers.RotatingFileHandler(logfile,
+                                                       maxBytes=2097152)
+    else:
+        handler = logging.StreamHandler()
+
+    handler.setFormatter(logging.Formatter(LOG_FORMAT))
+    logger.addHandler(handler)
+
+    if opts.daemonize:
+        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
+            config["general"]["pidfile"], 10)
+
+        d = daemon.DaemonContext(pidfile=pidfile,
+                                 stdout=handler.stream,
+                                 stderr=handler.stream,
+                                 files_preserve=[handler.stream])
+        d.umask = 0022
+        d.open()
+
     logging.info("Starting up")
 
     proxy_opts = {}
@@ -782,6 +931,7 @@ if __name__ == "__main__":
             "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"):
@@ -804,25 +954,24 @@ if __name__ == "__main__":
     logging.debug("Setting capabilities and changing uid")
     logging.debug("User: %s, uid: %d, gid: %d",
                   config["general"]["user"], uid.pw_uid, uid.pw_gid)
+
+    # Keep only the capabilities we need
+    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
     capng.capng_clear(capng.CAPNG_SELECT_BOTH)
     capng.capng_update(capng.CAPNG_ADD,
                        capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
-                       capng.CAP_NET_RAW)
+                       capng.CAP_NET_ADMIN)
     capng.capng_change_id(uid.pw_uid, uid.pw_gid,
                           capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
 
-    if opts.daemonize:
-        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
-        handler = logging.handlers.RotatingFileHandler(logfile,
-                                                       maxBytes=2097152)
-    else:
-        handler = logging.StreamHandler()
-
-    handler.setFormatter(logging.Formatter(LOG_FORMAT))
-    logger.addHandler(handler)
-
     logging.info("Ready to serve requests")
-    proxy.serve()
+    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 :