Refactor nfdhcpd to support get_physindev()
[snf-nfdhcpd] / nfdhcpd
diff --git a/nfdhcpd b/nfdhcpd
index e521191..c7396c0 100755 (executable)
--- a/nfdhcpd
+++ b/nfdhcpd
 
 import os
 import re
+import sys
 import glob
 import time
 import logging
 import logging.handlers
 import threading
+import traceback
 import subprocess
 
 import daemon
+import daemon.pidlockfile
 import nfqueue
 import pyinotify
 
@@ -37,11 +40,15 @@ 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 *
+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
 
 DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
@@ -72,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)
@@ -109,32 +117,124 @@ DHCP_REQRESP = {
     }
 
 
+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
+
+    indev_ifindex = payload.get_indev()
+    logging.debug("Incomming packet from tap %s", indev_ifindex)
+
+    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
+
+    """
+    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)
+
+    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):
-        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
 
-    def process_IN_CLOSE_WRITE(self, event):
-        self.server.add_iface(os.path.join(event.path, event.name))
+        """
+        self.server.remove_tap(event.name)
+
+    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_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
 
 
@@ -149,18 +249,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
@@ -177,41 +289,66 @@ 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,
+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,
                  dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
                  dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
-                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers = [],
-                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers = []):
+                 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
-        self.dhcp_nameservers = dhcp_nameservers
-        self.ipv6_nameservers = ipv6_nameservers
+        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
+
         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()
         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
@@ -226,9 +363,27 @@ class VMNetProxy(object):
             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))
+        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
+                      queue_num, family)
         q = nfqueue.queue()
         q.set_callback(callback)
         q.fast_open(queue_num, family)
@@ -237,28 +392,42 @@ class VMNetProxy(object):
         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 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)
 
     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):
+        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')
+            f = open(path, 'r')
         except EnvironmentError:
-            logging.debug("%s is probably down, removing" % iface)
-            self.remove_iface(iface)
+            logging.debug("%s is probably down, removing", iface)
+            self.remove_tap(iface)
 
             return ifindex
 
@@ -268,11 +437,11 @@ class VMNetProxy(object):
                 ifindex = int(ifindex)
             except ValueError, e:
                 logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
-                             " output '%s'" % (iface, ifindex))
+                             " output '%s'", iface, ifindex)
         except EnvironmentError, e:
-            logging.warn("Error reading %s's ifindex from sysfs: %s" %
-                         (iface, str(e)))
-            self.remove_iface(iface)
+            logging.warn("Error reading %s's ifindex from sysfs: %s",
+                         iface, str(e))
+            self.remove_tap(iface)
         finally:
             f.close()
 
@@ -283,142 +452,79 @@ class VMNetProxy(object):
         """ Get the interface hardware address from sysfs
 
         """
-        file = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
-        if not file.startswith(SYSFS_NET):
+        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_iface(iface)
+            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)))
+            logging.warn("Failed to read hw address for %s from sysfs: %s",
+                         iface, str(e))
         finally:
             f.close()
 
         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
-            m = re.match("^([^\\s]+) dev %s" % def_dev, route)
-            if not m:
-                continue
-            def_net = m.groups(1)
-
-            try:
-                def_net = IPy.IP(def_net)
-            except ValueError, e:
-                logging.warn("Unable to parse default route entry %s: %s" %
-                             (def_net, str(e)))
-
-        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 EnvironmentError, e:
-            logging.warn("Unable to open binding file %s: %s" % (path, str(e)))
-            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.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] = 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
+                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):
-        """ Generate a reply to a BOOTP/DHCP request
+    def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
+        """ 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()
@@ -430,26 +536,34 @@ class VMNetProxy(object):
         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 = []
@@ -460,41 +574,46 @@ 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))
+                         " 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
@@ -505,24 +624,50 @@ 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)
+        self.sendp(resp, binding.indev)
 
-    def rs_response(self, i, payload):
+    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)
@@ -531,41 +676,60 @@ class VMNetProxy(object):
             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):
+    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
@@ -576,15 +740,18 @@ class VMNetProxy(object):
         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)
@@ -592,22 +759,33 @@ class VMNetProxy(object):
                 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)))
+                logging.warn("Unkown error during periodic RA on %s: %s",
+                             tap, str(e))
             i += 1
-        logging.debug("Sent %d RAs in %.2f seconds" % (i, time.time() - start))
+        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()
 
-        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()
         if self.ipv6_enabled:
@@ -619,7 +797,7 @@ class VMNetProxy(object):
         while True:
             rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
             if xlist:
-                logging.warn("Warning: Exception on %s" %
+                logging.warn("Warning: Exception on %s",
                              ", ".join([ str(fd) for fd in xlist]))
 
             if rlist:
@@ -633,9 +811,11 @@ class VMNetProxy(object):
                 for fd in rlist:
                     try:
                         self.nfq[fd].process_pending()
+                    except RuntimeError, e:
+                        logging.warn("Error processing fd %d: %s", fd, str(e))
                     except Exception, e:
-                        logging.warn("Error processing fd %d: %s" %
-                                     (fd, str(e)))
+                        logging.warn("Unknown error processing fd %d: %s",
+                                     fd, str(e))
 
             if self.ipv6_enabled:
                 # Calculate the new timeout
@@ -648,9 +828,9 @@ class VMNetProxy(object):
 
 
 if __name__ == "__main__":
+    import capng
     import optparse
     from cStringIO import StringIO
-    from capng import *
     from pwd import getpwnam, getpwuid
     from configobj import ConfigObj, ConfigObjError, flatten_errors
 
@@ -662,7 +842,7 @@ if __name__ == "__main__":
         try:
             family = int(family)
         except ValueError:
-            raise vaildate.VdtParamError(family)
+            raise validate.VdtParamError(family)
         if isinstance(value, (str, unicode)):
             value = [value]
         if not isinstance(value, list):
@@ -688,44 +868,59 @@ if __name__ == "__main__":
                       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.umask = 0022
-        d.open()
-
     try:
         config = ConfigObj(opts.config_file, configspec=config_spec)
-    except ConfigObjError, e:
+    except ConfigObjError, err:
         sys.stderr.write("Failed to parse config file %s: %s" %
-                         (opts.config_file, str(e)))
+                         (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, _) in flatten_errors(config, results):
+        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)))
+                logging.fatal(" '%s' in section '%s' failed validation",
+                              key, ", ".join(section_list))
             else:
-                logging.fatal(" Section '%s' is missing" % ", ".join(section_list))
+                logging.fatal(" Section '%s' is missing",
+                              ", ".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 = {}
@@ -736,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"):
@@ -746,6 +942,7 @@ if __name__ == "__main__":
             "ipv6_nameservers": config["ipv6"]["nameservers"],
         })
 
+    # pylint: disable=W0142
     proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
 
     # Drop all capabilities except CAP_NET_RAW and change uid
@@ -755,25 +952,26 @@ if __name__ == "__main__":
         uid = getpwnam(config["general"]["user"])
 
     logging.debug("Setting capabilities and changing uid")
-    logging.debug("User: %s, uid: %d, gid: %d" %
-                  (config["general"]["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)
-
-    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.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_ADMIN)
+    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
+                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
 
     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 :