X-Git-Url: https://code.grnet.gr/git/snf-nfdhcpd/blobdiff_plain/68da8f2066cd8ca83f6b109c8c9e24573388f2d8..98804a5e1ce89e34e2a8206693009099babe6d1d:/nfdhcpd diff --git a/nfdhcpd b/nfdhcpd index df30d0e..c66c435 100755 --- a/nfdhcpd +++ b/nfdhcpd @@ -20,6 +20,8 @@ # import os +import signal +import errno import re import sys import glob @@ -27,15 +29,19 @@ import time import logging import logging.handlers import threading -import subprocess +import traceback import daemon +import daemon.runner +import daemon.pidlockfile import nfqueue import pyinotify +import setproctitle +from lockfile import LockTimeout import IPy import socket -from select import select +import select from socket import AF_INET, AF_INET6 from scapy.data import ETH_P_ALL @@ -47,6 +53,10 @@ from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \ ICMPv6NDOptPrefixInfo, \ ICMPv6NDOptRDNSS from scapy.layers.dhcp import BOOTP, DHCP +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" @@ -60,7 +70,7 @@ LOG_FILENAME = "nfdhcpd.log" SYSFS_NET = "/sys/class/net" -LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s" +LOG_FORMAT = "%(asctime)-15s %(levelname)-8s %(message)s" # Configuration file specification (see configobj documentation) CONFIG_SPEC = """ @@ -77,13 +87,16 @@ 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) """ @@ -114,68 +127,80 @@ 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.group(1) +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 - 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(" - Incoming packet from tap with ifindex %s", indev_ifindex) - return Subnet(net=def_net, gw=def_gw, dev=def_dev) + 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, None, None, None) + logging.warn(" - Unable to open binding file %s: %s", path, str(e)) + 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] + 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(mac=mac, ips=ips, link=link, hostname=hostname) + 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): @@ -183,44 +208,88 @@ class ClientFileHandler(pyinotify.ProcessEvent): pyinotify.ProcessEvent.__init__(self) self.server = server - def process_IN_DELETE(self, event): # pylint: disable=C0103 + 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): # pylint: disable=C0103 + 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 + 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 @@ -259,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:] @@ -281,17 +352,25 @@ class Subnet(object): return self._make_eui64("fe80::", mac) -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, +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): + 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: @@ -304,16 +383,18 @@ class VMNetProxy(object): # pylint: disable=R0902 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 = {} - 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() @@ -325,18 +406,51 @@ class VMNetProxy(object): # pylint: disable=R0902 # 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 - def _setup_nfqueue(self, queue_num, family, callback): - logging.debug("Setting up NFQUEUE for queue %d, AF %s", + 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") + + 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) @@ -344,33 +458,23 @@ class VMNetProxy(object): # pylint: disable=R0902 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 - - def sendp(self, data, iface): - """ Send a raw packet using a layer-2 socket - - """ - if isinstance(data, BasePacket): - data = str(data) - - self.l2socket.bind((iface, ETH_P_ALL)) - count = self.l2socket.send(data) - ldata = len(data) - if count != ldata: - logging.warn("Truncated send on %s (%d/%d bytes sent)", - iface, count, ldata) + 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 path in glob.glob(os.path.join(self.data_path, "*")): - self.add_iface(path) + self.add_tap(path) + + self.print_clients() def get_ifindex(self, iface): """ Get the interface index from sysfs """ + 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 @@ -380,8 +484,8 @@ class VMNetProxy(object): # pylint: disable=R0902 try: 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 @@ -390,22 +494,22 @@ class VMNetProxy(object): # pylint: disable=R0902 try: ifindex = int(ifindex) except ValueError, e: - logging.warn("Failed to get ifindex for %s, cannot parse sysfs" - " output '%s'", iface, ifindex) + 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", + logging.warn(" - Error reading %s's ifindex from sysfs: %s", iface, str(e)) - self.remove_iface(iface) + self.remove_tap(iface) finally: f.close() return ifindex - def get_iface_hw_addr(self, iface): """ Get the interface hardware address from sysfs """ + 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 @@ -414,100 +518,127 @@ class VMNetProxy(object): # pylint: disable=R0902 try: 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", + logging.warn(" - Failed to read hw address for %s from sysfs: %s", iface, str(e)) finally: f.close() 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.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] = 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 + 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] - - for ifindex in self.ifaces.keys(): - if self.ifaces[ifindex] == iface: - del self.ifaces[ifindex] + 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) - logging.debug("Removed interface %s", iface) - def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914 - """ 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 = [] @@ -518,40 +649,47 @@ class VMNetProxy(object): # pylint: disable=R0902 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), + ("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.hostname, binding.tap) return # Finally, always add the server identifier and end options @@ -562,25 +700,149 @@ 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) - self.sendp(resp, iface) + 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) + + 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, i, payload): # pylint: disable=W0613 + 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) @@ -588,41 +850,90 @@ 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) - self.sendp(resp, iface) - - def ns_response(self, i, payload): # pylint: disable=W0613 + 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) - self.sendp(resp, iface) - 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): # Use a separate thread as this may take a _long_ time with @@ -630,43 +941,58 @@ class VMNetProxy(object): # pylint: disable=R0902 threading.Thread(target=self._send_periodic_ra).start() def _send_periodic_ra(self): - logging.debug("Sending out periodic RAs") + 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: - self.sendp(resp, iface) + binding.sendp(resp) except socket.error, e: - logging.warn("Periodic RA on %s failed: %s", iface, str(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", - iface, str(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() - # 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 + iwfd = self.notifier._fd # pylint: disable=W0212 start = time.time() if self.ipv6_enabled: @@ -676,10 +1002,17 @@ class VMNetProxy(object): # pylint: disable=R0902 timeout = None while True: - rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout) + 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 + if xlist: logging.warn("Warning: Exception on %s", - ", ".join([ str(fd) for fd in xlist])) + ", ".join([str(fd) for fd in xlist])) if rlist: if iwfd in rlist: @@ -689,9 +1022,14 @@ class VMNetProxy(object): # pylint: disable=R0902 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() + 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: @@ -707,6 +1045,14 @@ class VMNetProxy(object): # pylint: disable=R0902 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 @@ -742,7 +1088,6 @@ if __name__ == "__main__": validator.functions["ip_addr_list"] = is_ip_list config_spec = StringIO(CONFIG_SPEC) - parser = optparse.OptionParser() parser.add_option("-c", "--config", dest="config_file", help="The location of the data files", metavar="FILE", @@ -753,14 +1098,8 @@ if __name__ == "__main__": 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, err: @@ -780,9 +1119,29 @@ if __name__ == "__main__": ", ".join(section_list)) sys.exit(1) - pidfile = open(config["general"]["pidfile"], "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: @@ -790,7 +1149,46 @@ if __name__ == "__main__": else: logger.setLevel(logging.INFO) + if opts.daemonize: + 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") + 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"): @@ -800,50 +1198,40 @@ 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"): 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(config["general"].as_int("user")) - except ValueError: - uid = getpwnam(config["general"]["user"]) + logging.info("Ready to serve requests") - 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_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() + def debug_handler(signum, _): + logging.debug('Received signal %d. Printing proxy state...', signum) + proxy.print_clients() - handler.setFormatter(logging.Formatter(LOG_FORMAT)) - logger.addHandler(handler) + # Set the signal handler for debuging clients + signal.signal(signal.SIGUSR1, debug_handler) + signal.siginterrupt(signal.SIGUSR1, False) - 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 :