4 # nfdcpd: A promiscuous, NFQUEUE-based DHCP server for virtual machine hosting
5 # Copyright (c) 2010 GRNET SA
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
30 import logging.handlers
36 import daemon.pidlockfile
40 from lockfile import LockTimeout
45 from socket import AF_INET, AF_INET6
47 from scapy.data import ETH_P_ALL
48 from scapy.packet import BasePacket
49 from scapy.layers.l2 import Ether
50 from scapy.layers.inet import IP, UDP
51 from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
52 ICMPv6NDOptDstLLAddr, \
53 ICMPv6NDOptPrefixInfo, \
55 from scapy.layers.dhcp import BOOTP, DHCP
56 from scapy.layers.dhcp6 import DHCP6_Reply, DHCP6OptDNSServers, \
57 DHCP6OptServerId, DHCP6OptClientId, \
58 DUID_LLT, DHCP6_InfoRequest, DHCP6OptDNSDomains
61 DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
62 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
63 DEFAULT_USER = "nobody"
64 DEFAULT_LEASE_LIFETIME = 604800 # 1 week
65 DEFAULT_LEASE_RENEWAL = 600 # 10 min
66 DEFAULT_RA_PERIOD = 300 # seconds
67 DHCP_DUMMY_SERVER_IP = "1.2.3.4"
69 LOG_FILENAME = "nfdhcpd.log"
71 SYSFS_NET = "/sys/class/net"
73 LOG_FORMAT = "%(asctime)-15s %(levelname)-8s %(message)s"
75 # Configuration file specification (see configobj documentation)
84 enable_dhcp = boolean(default=True)
85 lease_lifetime = integer(min=0, max=4294967295)
86 lease_renewal = integer(min=0, max=4294967295)
88 dhcp_queue = integer(min=0, max=65535)
89 nameservers = ip_addr_list(family=4)
90 domain = string(default=None)
93 enable_ipv6 = boolean(default=True)
94 ra_period = integer(min=1, max=4294967295)
95 rs_queue = integer(min=0, max=65535)
96 ns_queue = integer(min=0, max=65535)
97 dhcp_queue = integer(min=0, max=65535)
98 nameservers = ip_addr_list(family=6)
99 domains = force_list(default=None)
113 DHCPDISCOVER: "DHCPDISCOVER",
114 DHCPOFFER: "DHCPOFFER",
115 DHCPREQUEST: "DHCPREQUEST",
116 DHCPDECLINE: "DHCPDECLINE",
119 DHCPRELEASE: "DHCPRELEASE",
120 DHCPINFORM: "DHCPINFORM",
124 DHCPDISCOVER: DHCPOFFER,
125 DHCPREQUEST: DHCPACK,
130 def get_indev(payload):
132 indev_ifindex = payload.get_physindev()
134 logging.debug(" - Incoming packet from bridge with ifindex %s",
137 except AttributeError:
138 #TODO: return error value
139 logging.debug("No get_physindev() supported")
142 indev_ifindex = payload.get_indev()
143 logging.debug(" - Incoming packet from tap with ifindex %s", indev_ifindex)
148 def parse_binding_file(path):
149 """ Read a client configuration from a tap file
152 logging.info("Parsing binding file %s", path)
154 iffile = open(path, 'r')
155 except EnvironmentError, e:
156 logging.warn(" - Unable to open binding file %s: %s", path, str(e))
159 tap = os.path.basename(path)
171 v = line.strip().split('=')[1]
177 if line.startswith("IP="):
179 elif line.startswith("MAC="):
180 mac = get_value(line)
181 elif line.startswith("HOSTNAME="):
182 hostname = get_value(line)
183 elif line.startswith("INDEV="):
184 indev = get_value(line)
185 elif line.startswith("SUBNET="):
186 subnet = get_value(line)
187 elif line.startswith("GATEWAY="):
188 gateway = get_value(line)
189 elif line.startswith("SUBNET6="):
190 subnet6 = get_value(line)
191 elif line.startswith("GATEWAY6="):
192 gateway6 = get_value(line)
193 elif line.startswith("EUI64="):
194 eui64 = get_value(line)
197 return Client(tap=tap, mac=mac, ip=ip, hostname=hostname,
198 indev=indev, subnet=subnet, gateway=gateway,
199 subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
201 logging.warning(" - Cannot add client for host %s and IP %s on tap %s",
206 class ClientFileHandler(pyinotify.ProcessEvent):
207 def __init__(self, server):
208 pyinotify.ProcessEvent.__init__(self)
211 def process_IN_DELETE(self, event): # pylint: disable=C0103
212 """ Delete file handler
214 Currently this removes an interface from the watch list
217 self.server.remove_tap(event.name)
219 def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
222 Currently this adds an interface to the watch list
225 self.server.add_tap(os.path.join(event.path, event.name))
228 class Client(object):
229 def __init__(self, tap=None, indev=None,
230 mac=None, ip=None, hostname=None,
231 subnet=None, gateway=None,
232 subnet6=None, gateway6=None, eui64=None):
235 self.hostname = hostname
239 self.gateway = gateway
240 self.net = Subnet(net=subnet, gw=gateway, dev=tap)
241 self.subnet6 = subnet6
242 self.gateway6 = gateway6
243 self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
248 return self.mac is not None and self.hostname is not None
251 def open_socket(self):
253 logging.info(" - Opening L2 socket and binding to %s", self.tap)
255 s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
256 s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
257 s.bind((self.tap, ETH_P_ALL))
259 except socket.error, e:
260 logging.warning(" - Cannot open socket %s", e)
263 def sendp(self, data):
265 if isinstance(data, BasePacket):
268 logging.debug(" - Sending raw packet %r", data)
271 count = self.socket.send(data, socket.MSG_DONTWAIT)
272 except socket.error, e:
273 logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
279 logging.debug(" - Sent %d bytes on %s", count, self.tap)
281 logging.warn(" - Truncated msg: %d/%d bytes sent",
285 class Subnet(object):
286 def __init__(self, net=None, gw=None, dev=None):
287 if isinstance(net, str):
289 self.net = IPy.IP(net)
290 except ValueError, e:
291 logging.warning(" - IPy error: %s", e)
300 """ Return the netmask in textual representation
303 return str(self.net.netmask())
307 """ Return the broadcast address in textual representation
310 return str(self.net.broadcast())
314 """ Return the network as an IPy.IP
317 return self.net.net()
321 """ Return the prefix length as an integer
324 return self.net.prefixlen()
327 def _make_eui64(net, mac):
328 """ Compute an EUI-64 address from an EUI-48 (MAC) address
333 comp = mac.split(":")
334 prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
335 eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
336 eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
337 for l in range(0, len(eui64), 2):
338 prefix += ["".join(eui64[l:l+2])]
339 return IPy.IP(":".join(prefix))
341 def make_eui64(self, mac):
342 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
346 return self._make_eui64(self.net, mac)
348 def make_ll64(self, mac):
349 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
352 return self._make_eui64("fe80::", mac)
355 class VMNetProxy(object): # pylint: disable=R0902
356 def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
357 rs_queue_num=None, ns_queue_num=None, dhcpv6_queue_num=None,
358 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
359 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
361 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
362 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None,
363 dhcpv6_domains=None):
366 getattr(nfqueue.payload, 'get_physindev')
367 self.mac_indexed_clients = False
368 except AttributeError:
369 self.mac_indexed_clients = True
370 self.data_path = data_path
371 self.lease_lifetime = dhcp_lease_lifetime
372 self.lease_renewal = dhcp_lease_renewal
373 self.dhcp_domain = dhcp_domain
374 self.dhcp_server_ip = dhcp_server_ip
375 self.ra_period = ra_period
376 if dhcp_nameservers is None:
377 self.dhcp_nameserver = []
379 self.dhcp_nameservers = dhcp_nameservers
381 if ipv6_nameservers is None:
382 self.ipv6_nameservers = []
384 self.ipv6_nameservers = ipv6_nameservers
386 if dhcpv6_domains is None:
387 self.dhcpv6_domains = []
389 self.dhcpv6_domains = dhcpv6_domains
391 self.ipv6_enabled = False
400 self.wm = pyinotify.WatchManager()
401 mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
402 mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
403 inotify_handler = ClientFileHandler(self)
404 self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
405 self.wm.add_watch(self.data_path, mask, rec=True)
408 if dhcp_queue_num is not None:
409 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
411 if rs_queue_num is not None:
412 self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
413 self.ipv6_enabled = True
415 if ns_queue_num is not None:
416 self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
417 self.ipv6_enabled = True
419 if dhcpv6_queue_num is not None:
420 self._setup_nfqueue(dhcpv6_queue_num, AF_INET6, self.dhcpv6_response, 10)
421 self.ipv6_enabled = True
423 def get_binding(self, ifindex, mac):
425 if self.mac_indexed_clients:
426 logging.debug(" - Getting binding for mac %s", mac)
427 b = self.clients[mac]
429 logging.debug(" - Getting binding for ifindex %s", ifindex)
430 b = self.clients[ifindex]
433 logging.debug(" - No client found for mac / ifindex %s / %s",
438 """ Free all resources for a graceful exit
441 logging.info("Cleaning up")
443 logging.debug(" - Closing netfilter queues")
444 for q, _ in self.nfq.values():
447 logging.debug(" - Stopping inotify watches")
450 logging.info(" - Cleanup finished")
452 def _setup_nfqueue(self, queue_num, family, callback, pending):
453 logging.info("Setting up NFQUEUE for queue %d, AF %s",
456 q.set_callback(callback)
457 q.fast_open(queue_num, family)
458 q.set_queue_maxlen(5000)
459 # This is mandatory for the queue to operate
460 q.set_mode(nfqueue.NFQNL_COPY_PACKET)
461 self.nfq[q.get_fd()] = (q, pending)
462 logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
464 def build_config(self):
467 for path in glob.glob(os.path.join(self.data_path, "*")):
472 def get_ifindex(self, iface):
473 """ Get the interface index from sysfs
476 logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
478 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
479 if not path.startswith(SYSFS_NET):
486 except EnvironmentError:
487 logging.debug(" - %s is probably down, removing", iface)
488 self.remove_tap(iface)
493 ifindex = f.readline().strip()
495 ifindex = int(ifindex)
496 except ValueError, e:
497 logging.warn(" - Failed to get ifindex for %s, cannot parse"
498 " sysfs output '%s'", iface, ifindex)
499 except EnvironmentError, e:
500 logging.warn(" - Error reading %s's ifindex from sysfs: %s",
502 self.remove_tap(iface)
508 def get_iface_hw_addr(self, iface):
509 """ Get the interface hardware address from sysfs
512 logging.debug(" - Getting mac for iface %s", iface)
513 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
514 if not path.startswith(SYSFS_NET):
520 except EnvironmentError:
521 logging.debug(" - %s is probably down, removing", iface)
522 self.remove_tap(iface)
526 addr = f.readline().strip()
527 except EnvironmentError, e:
528 logging.warn(" - Failed to read hw address for %s from sysfs: %s",
535 def add_tap(self, path):
536 """ Add an interface to monitor
539 tap = os.path.basename(path)
541 logging.info("Updating configuration for %s", tap)
542 b = parse_binding_file(path)
545 ifindex = self.get_ifindex(b.tap)
548 logging.warn(" - Stale configuration for %s found", tap)
551 if self.mac_indexed_clients:
552 self.clients[b.mac] = b
555 self.clients[ifindex] = b
557 logging.info(" - Added client:")
558 logging.info(" + %10s | %20s %20s %10s %20s %40s",
559 k, b.hostname, b.mac, b.tap, b.ip, b.eui64)
561 def remove_tap(self, tap):
562 """ Cleanup clients on a removed interface
566 for k, cl in self.clients.items():
568 logging.info("Removing client %s and closing socket on %s",
570 logging.info(" - %10s | %20s %20s %10s %20s %40s",
571 k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
575 logging.debug("Client on %s disappeared!!!", tap)
578 def dhcp_response(self, arg1, arg2=None): # pylint: disable=W0613,R0914
579 """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
582 logging.info(" * Processing pending DHCP request")
583 # Workaround for supporting both squeezy's nfqueue-bindings-python
584 # and wheezy's python-nfqueue because for some reason the function's
585 # signature has changed and has broken compatibility
586 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
591 # Decode the response - NFQUEUE relays IP packets
592 pkt = IP(payload.get_data())
593 #logging.debug(pkt.show())
595 # Get the client MAC address
596 resp = pkt.getlayer(BOOTP).copy()
598 mac = resp.chaddr[:hlen].encode("hex")
599 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
601 # Server responses are always BOOTREPLYs
602 resp.op = "BOOTREPLY"
605 indev = get_indev(payload)
607 binding = self.get_binding(indev, mac)
609 # We don't know anything about this interface, so accept the packet
611 logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
612 # We don't know what to do with this packet, so let the kernel
614 payload.set_verdict(nfqueue.NF_ACCEPT)
617 # Signal the kernel that it shouldn't further process the packet
618 payload.set_verdict(nfqueue.NF_DROP)
620 if mac != binding.mac:
621 logging.warn(" - Recieved spoofed DHCP request: mac %s, indev %s",
626 logging.info(" - No IP found in binding file.")
629 logging.info(" - Generating DHCP response:"
630 " host %s, mac %s, tap %s, indev %s",
631 binding.hostname, mac, binding.tap, indev)
634 resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
635 IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
636 UDP(sport=pkt.dport, dport=pkt.sport)/resp
640 logging.warn(" - Invalid request from %s on %s, no DHCP"
641 " payload found", binding.mac, binding.tap)
645 requested_addr = binding.ip
646 for opt in pkt[DHCP].options:
647 if type(opt) is tuple and opt[0] == "message-type":
649 if type(opt) is tuple and opt[0] == "requested_addr":
650 requested_addr = opt[1]
652 logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
653 binding.mac, binding.tap)
656 domainname = self.dhcp_domain
658 domainname = binding.hostname.split('.', 1)[-1]
660 if req_type == DHCPREQUEST and requested_addr != binding.ip:
662 logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
663 " instead of %s", binding.mac, binding.tap,
664 requested_addr, binding.ip)
666 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
667 resp_type = DHCP_REQRESP[req_type]
668 resp.yiaddr = binding.ip
670 ("hostname", binding.hostname),
671 ("domain", domainname),
672 ("broadcast_address", str(subnet.broadcast)),
673 ("subnet_mask", str(subnet.netmask)),
674 ("renewal_time", self.lease_renewal),
675 ("lease_time", self.lease_lifetime),
678 dhcp_options += [("router", subnet.gw)]
679 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
681 elif req_type == DHCPINFORM:
682 resp_type = DHCP_REQRESP[req_type]
684 ("hostname", binding.hostname),
685 ("domain", domainname),
687 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
689 elif req_type == DHCPRELEASE:
691 logging.info(" - DHCPRELEASE from %s on %s",
692 binding.hostname, binding.tap)
695 # Finally, always add the server identifier and end options
697 ("message-type", resp_type),
698 ("server_id", DHCP_DUMMY_SERVER_IP),
701 resp /= DHCP(options=dhcp_options)
703 logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
704 binding.ip, binding.tap)
707 except socket.error, e:
708 logging.warn(" - DHCP response on %s (%s) failed: %s",
709 binding.tap, binding.hostname, str(e))
711 logging.warn(" - Unkown error during DHCP response on %s (%s): %s",
712 binding.tap, binding.hostname, str(e))
714 def dhcpv6_response(self, arg1, arg2=None): # pylint: disable=W0613
716 logging.info(" * Processing pending DHCPv6 request")
717 # Workaround for supporting both squeezy's nfqueue-bindings-python
718 # and wheezy's python-nfqueue because for some reason the function's
719 # signature has changed and has broken compatibility
720 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
725 pkt = IPv6(payload.get_data())
726 indev = get_indev(payload)
728 #TODO: figure out how to find the src mac
730 binding = self.get_binding(indev, mac)
732 # We don't know anything about this interface, so accept the packet
734 logging.debug(" - Ignoring dhcpv6 request for mac %s", mac)
735 # We don't know what to do with this packet, so let the kernel
737 payload.set_verdict(nfqueue.NF_ACCEPT)
740 # Signal the kernel that it shouldn't further process the packet
741 payload.set_verdict(nfqueue.NF_DROP)
743 subnet = binding.net6
745 if subnet.net is None:
746 logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
749 indevmac = self.get_iface_hw_addr(binding.indev)
750 ifll = subnet.make_ll64(indevmac)
754 ofll = subnet.make_ll64(binding.mac)
758 logging.info(" - Generating DHCPv6 response for host %s (mac %s) on tap %s",
759 binding.hostname, binding.mac, binding.tap)
761 if self.dhcpv6_domains:
762 domains = self.dhcpv6_domains
764 domains = [binding.hostname.split('.', 1)[-1]]
766 # We do this in order not to caclulate optlen ourselves
767 dnsdomains = str(DHCP6OptDNSDomains(dnsdomains=domains))
768 dnsservers = str(DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers))
770 resp = Ether(src=indevmac, dst=binding.mac)/\
771 IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
772 UDP(sport=pkt.dport, dport=pkt.sport)/\
773 DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
774 DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
775 DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
776 DHCP6OptDNSDomains(dnsdomains)/\
777 DHCP6OptDNSServers(dnsservers)
781 except socket.error, e:
782 logging.warn(" - DHCPv6 on %s (%s) failed: %s",
783 binding.tap, binding.hostname, str(e))
785 logging.warn(" - Unkown error during DHCPv6 on %s (%s): %s",
786 binding.tap, binding.hostname, str(e))
789 def rs_response(self, arg1, arg2=None): # pylint: disable=W0613
790 """ Generate a reply to a BOOTP/DHCP request
793 logging.info(" * Processing pending RS request")
794 # Workaround for supporting both squeezy's nfqueue-bindings-python
795 # and wheezy's python-nfqueue because for some reason the function's
796 # signature has changed and has broken compatibility
797 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
802 pkt = IPv6(payload.get_data())
803 #logging.debug(pkt.show())
807 logging.debug(" - Cannot obtain lladdr in rs")
810 indev = get_indev(payload)
812 binding = self.get_binding(indev, mac)
814 # We don't know anything about this interface, so accept the packet
816 logging.debug(" - Ignoring router solicitation on for mac %s", mac)
817 # We don't know what to do with this packet, so let the kernel
819 payload.set_verdict(nfqueue.NF_ACCEPT)
822 # Signal the kernel that it shouldn't further process the packet
823 payload.set_verdict(nfqueue.NF_DROP)
825 if mac != binding.mac:
826 logging.warn(" - Received spoofed RS request: mac %s, tap %s",
830 subnet = binding.net6
832 if subnet.net is None:
833 logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
836 indevmac = self.get_iface_hw_addr(binding.indev)
837 ifll = subnet.make_ll64(indevmac)
841 logging.info(" - Generating RA for host %s (mac %s) on tap %s",
842 binding.hostname, mac, binding.tap)
844 resp = Ether(src=indevmac)/\
845 IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
846 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
847 prefixlen=subnet.prefixlen)
849 if self.ipv6_nameservers:
850 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
851 lifetime=self.ra_period * 3)
855 except socket.error, e:
856 logging.warn(" - RA on %s (%s) failed: %s",
857 binding.tap, binding.hostname, str(e))
859 logging.warn(" - Unkown error during RA on %s (%s): %s",
860 binding.tap, binding.hostname, str(e))
862 def ns_response(self, arg1, arg2=None): # pylint: disable=W0613
863 """ Generate a reply to an ICMPv6 neighbor solicitation
867 logging.info(" * Processing pending NS request")
868 # Workaround for supporting both squeezy's nfqueue-bindings-python
869 # and wheezy's python-nfqueue because for some reason the function's
870 # signature has changed and has broken compatibility
871 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
877 ns = IPv6(payload.get_data())
878 #logging.debug(ns.show())
882 logging.debug(" - Cannot obtain lladdr from ns")
886 indev = get_indev(payload)
888 binding = self.get_binding(indev, mac)
890 # We don't know anything about this interface, so accept the packet
892 logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
894 # We don't know what to do with this packet, so let the kernel
896 payload.set_verdict(nfqueue.NF_ACCEPT)
899 payload.set_verdict(nfqueue.NF_DROP)
901 if mac != binding.mac:
902 logging.warn(" - Received spoofed NS request"
903 " for mac %s from tap %s", mac, binding.tap)
906 subnet = binding.net6
907 if subnet.net is None:
908 logging.debug(" - No IPv6 network assigned for the interface")
911 indevmac = self.get_iface_hw_addr(binding.indev)
913 ifll = subnet.make_ll64(indevmac)
917 if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
918 logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
921 logging.info(" - Generating NA for host %s (mac %s) on tap %s",
922 binding.hostname, mac, binding.tap)
924 resp = Ether(src=indevmac, dst=binding.mac)/\
925 IPv6(src=str(ifll), dst=ns.src)/\
926 ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
927 ICMPv6NDOptDstLLAddr(lladdr=indevmac)
931 except socket.error, e:
932 logging.warn(" - NA on %s (%s) failed: %s",
933 binding.tap, binding.hostname, str(e))
935 logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
936 binding.tap, binding.hostname, str(e))
938 def send_periodic_ra(self):
939 # Use a separate thread as this may take a _long_ time with
940 # many interfaces and we want to be responsive in the mean time
941 threading.Thread(target=self._send_periodic_ra).start()
943 def _send_periodic_ra(self):
944 logging.info("Sending out periodic RAs")
947 for binding in self.clients.values():
949 indev = binding.indev
951 subnet = binding.net6
952 if subnet.net is None:
953 logging.debug(" - Skipping periodic RA on interface %s,"
954 " as it is not IPv6-connected", tap)
956 indevmac = self.get_iface_hw_addr(indev)
957 ifll = subnet.make_ll64(indevmac)
960 resp = Ether(src=indevmac)/\
961 IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
962 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
963 prefixlen=subnet.prefixlen)
964 if self.ipv6_nameservers:
965 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
966 lifetime=self.ra_period * 3)
969 except socket.error, e:
970 logging.warn(" - Periodic RA on %s (%s) failed: %s",
971 tap, binding.hostname, str(e))
973 logging.warn(" - Unkown error during periodic RA on %s (%s):"
974 " %s", tap, binding.hostname, str(e))
976 logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
979 """ Safely perform the main loop, freeing all resources upon exit
988 """ Loop forever, serving DHCP requests
993 # Yes, we are accessing _fd directly, but it's the only way to have a
994 # single select() loop ;-)
995 iwfd = self.notifier._fd # pylint: disable=W0212
998 if self.ipv6_enabled:
999 timeout = self.ra_period
1000 self.send_periodic_ra()
1006 rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
1008 except select.error, e:
1009 if e[0] == errno.EINTR:
1010 logging.debug("select() got interrupted")
1014 logging.warn("Warning: Exception on %s",
1015 ", ".join([str(fd) for fd in xlist]))
1019 # First check if there are any inotify (= configuration change)
1021 self.notifier.read_events()
1022 self.notifier.process_events()
1025 logging.debug("Pending requests on fds %s", rlist)
1029 q, num = self.nfq[fd]
1030 cnt = q.process_pending(num)
1031 logging.debug(" * Processed %d requests on NFQUEUE"
1032 " with fd %d", cnt, fd)
1033 except RuntimeError, e:
1034 logging.warn("Error processing fd %d: %s", fd, str(e))
1035 except Exception, e:
1036 logging.warn("Unknown error processing fd %d: %s",
1039 if self.ipv6_enabled:
1040 # Calculate the new timeout
1041 timeout = self.ra_period - (time.time() - start)
1045 self.send_periodic_ra()
1046 timeout = self.ra_period - (time.time() - start)
1048 def print_clients(self):
1049 logging.info("%10s %20s %20s %10s %20s %40s",
1050 'Key', 'Client', 'MAC', 'TAP', 'IP', 'IPv6')
1051 for k, cl in self.clients.items():
1052 logging.info("%10s | %20s %20s %10s %20s %40s",
1053 k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
1057 if __name__ == "__main__":
1060 from cStringIO import StringIO
1061 from pwd import getpwnam, getpwuid
1062 from configobj import ConfigObj, ConfigObjError, flatten_errors
1066 validator = validate.Validator()
1068 def is_ip_list(value, family=4):
1070 family = int(family)
1072 raise validate.VdtParamError(family)
1073 if isinstance(value, (str, unicode)):
1075 if not isinstance(value, list):
1076 raise validate.VdtTypeError(value)
1082 raise validate.VdtValueError(entry)
1084 if ip.version() != family:
1085 raise validate.VdtValueError(entry)
1088 validator.functions["ip_addr_list"] = is_ip_list
1089 config_spec = StringIO(CONFIG_SPEC)
1091 parser = optparse.OptionParser()
1092 parser.add_option("-c", "--config", dest="config_file",
1093 help="The location of the data files", metavar="FILE",
1094 default=DEFAULT_CONFIG)
1095 parser.add_option("-d", "--debug", action="store_true", dest="debug",
1096 help="Turn on debugging messages")
1097 parser.add_option("-f", "--foreground", action="store_false",
1098 dest="daemonize", default=True,
1099 help="Do not daemonize, stay in the foreground")
1101 opts, args = parser.parse_args()
1104 config = ConfigObj(opts.config_file, configspec=config_spec)
1105 except ConfigObjError, err:
1106 sys.stderr.write("Failed to parse config file %s: %s" %
1107 (opts.config_file, str(err)))
1110 results = config.validate(validator)
1112 logging.fatal("Configuration file validation failed! See errors below:")
1113 for (section_list, key, unused) in flatten_errors(config, results):
1115 logging.fatal(" '%s' in section '%s' failed validation",
1116 key, ", ".join(section_list))
1118 logging.fatal(" Section '%s' is missing",
1119 ", ".join(section_list))
1123 uid = getpwuid(config["general"].as_int("user"))
1125 uid = getpwnam(config["general"]["user"])
1127 # Keep only the capabilities we need
1128 # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1129 # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1130 # CAP_SETPCAP: needed by capng_change_id()
1131 capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1132 capng.capng_update(capng.CAPNG_ADD,
1133 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1134 capng.CAP_NET_ADMIN)
1135 capng.capng_update(capng.CAPNG_ADD,
1136 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1138 capng.capng_update(capng.CAPNG_ADD,
1139 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1142 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1143 capng.CAPNG_DROP_SUPP_GRP | \
1144 capng.CAPNG_CLEAR_BOUNDING)
1146 logger = logging.getLogger()
1148 logger.setLevel(logging.DEBUG)
1150 logger.setLevel(logging.INFO)
1153 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1154 handler = logging.handlers.WatchedFileHandler(logfile)
1156 handler = logging.StreamHandler()
1158 handler.setFormatter(logging.Formatter(LOG_FORMAT))
1159 logger.addHandler(handler)
1161 # Rename this process so 'ps' output looks like
1162 # this is a native executable.
1163 # NOTE: due to a bug in python-setproctitle, one cannot yet
1164 # set individual values for command-line arguments, so only show
1165 # the name of the executable instead.
1166 # setproctitle.setproctitle("\x00".join(sys.argv))
1167 setproctitle.setproctitle(sys.argv[0])
1170 pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1171 config["general"]["pidfile"], 10)
1172 # Remove any stale PID files, left behind by previous invocations
1173 if daemon.runner.is_pidfile_stale(pidfile):
1174 logger.warning("Removing stale PID lock file %s", pidfile.path)
1175 pidfile.break_lock()
1177 d = daemon.DaemonContext(pidfile=pidfile,
1179 stdout=handler.stream,
1180 stderr=handler.stream,
1181 files_preserve=[handler.stream])
1184 except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1185 logger.critical("Failed to lock pidfile %s,"
1186 " another instance running?", pidfile.path)
1189 logging.info("Starting up")
1190 logging.info("Running as %s (uid:%d, gid: %d)",
1191 config["general"]["user"], uid.pw_uid, uid.pw_gid)
1194 if config["dhcp"].as_bool("enable_dhcp"):
1196 "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1197 "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1198 "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1199 "dhcp_server_ip": config["dhcp"]["server_ip"],
1200 "dhcp_nameservers": config["dhcp"]["nameservers"],
1201 "dhcp_domain": config["dhcp"]["domain"],
1204 if config["ipv6"].as_bool("enable_ipv6"):
1206 "dhcpv6_queue_num": config["ipv6"].as_int("dhcp_queue"),
1207 "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1208 "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1209 "ra_period": config["ipv6"].as_int("ra_period"),
1210 "ipv6_nameservers": config["ipv6"]["nameservers"],
1211 "dhcpv6_domains": config["ipv6"]["domains"],
1214 # pylint: disable=W0142
1215 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1217 logging.info("Ready to serve requests")
1220 def debug_handler(signum, _):
1221 logging.debug('Received signal %d. Printing proxy state...', signum)
1222 proxy.print_clients()
1224 # Set the signal handler for debuging clients
1225 signal.signal(signal.SIGUSR1, debug_handler)
1226 signal.siginterrupt(signal.SIGUSR1, False)
1232 exc = "".join(traceback.format_exception(*sys.exc_info()))
1233 logging.critical(exc)
1237 # vim: set ts=4 sts=4 sw=4 et :