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 ret = "hostname %s, tap %s, mac %s" % \
286 (self.hostname, self.tap, self.mac)
288 ret += ", ip %s" % self.ip
290 ret += ", eui64 %s" % self.eui64
294 class Subnet(object):
295 def __init__(self, net=None, gw=None, dev=None):
296 if isinstance(net, str):
298 self.net = IPy.IP(net)
299 except ValueError, e:
300 logging.warning(" - IPy error: %s", e)
309 """ Return the netmask in textual representation
312 return str(self.net.netmask())
316 """ Return the broadcast address in textual representation
319 return str(self.net.broadcast())
323 """ Return the network as an IPy.IP
326 return self.net.net()
330 """ Return the prefix length as an integer
333 return self.net.prefixlen()
336 def _make_eui64(net, mac):
337 """ Compute an EUI-64 address from an EUI-48 (MAC) address
342 comp = mac.split(":")
343 prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
344 eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
345 eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
346 for l in range(0, len(eui64), 2):
347 prefix += ["".join(eui64[l:l+2])]
348 return IPy.IP(":".join(prefix))
350 def make_eui64(self, mac):
351 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
355 return self._make_eui64(self.net, mac)
357 def make_ll64(self, mac):
358 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
361 return self._make_eui64("fe80::", mac)
364 class VMNetProxy(object): # pylint: disable=R0902
365 def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
366 rs_queue_num=None, ns_queue_num=None, dhcpv6_queue_num=None,
367 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
368 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
370 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
371 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None,
372 dhcpv6_domains=None):
375 getattr(nfqueue.payload, 'get_physindev')
376 self.mac_indexed_clients = False
377 except AttributeError:
378 self.mac_indexed_clients = True
379 self.data_path = data_path
380 self.lease_lifetime = dhcp_lease_lifetime
381 self.lease_renewal = dhcp_lease_renewal
382 self.dhcp_domain = dhcp_domain
383 self.dhcp_server_ip = dhcp_server_ip
384 self.ra_period = ra_period
385 if dhcp_nameservers is None:
386 self.dhcp_nameserver = []
388 self.dhcp_nameservers = dhcp_nameservers
390 if ipv6_nameservers is None:
391 self.ipv6_nameservers = []
393 self.ipv6_nameservers = ipv6_nameservers
395 if dhcpv6_domains is None:
396 self.dhcpv6_domains = []
398 self.dhcpv6_domains = dhcpv6_domains
400 self.ipv6_enabled = False
409 self.wm = pyinotify.WatchManager()
410 mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
411 mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
412 inotify_handler = ClientFileHandler(self)
413 self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
414 self.wm.add_watch(self.data_path, mask, rec=True)
417 if dhcp_queue_num is not None:
418 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
420 if rs_queue_num is not None:
421 self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
422 self.ipv6_enabled = True
424 if ns_queue_num is not None:
425 self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
426 self.ipv6_enabled = True
428 if dhcpv6_queue_num is not None:
429 self._setup_nfqueue(dhcpv6_queue_num, AF_INET6, self.dhcpv6_response, 10)
430 self.ipv6_enabled = True
432 def get_binding(self, ifindex, mac):
434 if self.mac_indexed_clients:
435 logging.debug(" - Getting binding for mac %s", mac)
436 b = self.clients[mac]
438 logging.debug(" - Getting binding for ifindex %s", ifindex)
439 b = self.clients[ifindex]
440 logging.info(" - Client found. %s", b)
443 logging.info(" - No client found. mac: %s, ifindex: %s",
448 """ Free all resources for a graceful exit
451 logging.info("Cleaning up")
453 logging.debug(" - Closing netfilter queues")
454 for q, _ in self.nfq.values():
457 logging.debug(" - Stopping inotify watches")
460 logging.info(" - Cleanup finished")
462 def _setup_nfqueue(self, queue_num, family, callback, pending):
463 logging.info("Setting up NFQUEUE for queue %d, AF %s",
466 q.set_callback(callback)
467 q.fast_open(queue_num, family)
468 q.set_queue_maxlen(5000)
469 # This is mandatory for the queue to operate
470 q.set_mode(nfqueue.NFQNL_COPY_PACKET)
471 self.nfq[q.get_fd()] = (q, pending)
472 logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
474 def build_config(self):
477 for path in glob.glob(os.path.join(self.data_path, "*")):
482 def get_ifindex(self, iface):
483 """ Get the interface index from sysfs
486 logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
488 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
489 if not path.startswith(SYSFS_NET):
496 except EnvironmentError:
497 logging.debug(" - %s is probably down, removing", iface)
498 self.remove_tap(iface)
503 ifindex = f.readline().strip()
505 ifindex = int(ifindex)
506 except ValueError, e:
507 logging.warn(" - Failed to get ifindex for %s, cannot parse"
508 " sysfs output '%s'", iface, ifindex)
509 except EnvironmentError, e:
510 logging.warn(" - Error reading %s's ifindex from sysfs: %s",
512 self.remove_tap(iface)
518 def get_iface_hw_addr(self, iface):
519 """ Get the interface hardware address from sysfs
522 logging.debug(" - Getting mac for iface %s", iface)
523 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
524 if not path.startswith(SYSFS_NET):
530 except EnvironmentError:
531 logging.debug(" - %s is probably down, removing", iface)
532 self.remove_tap(iface)
536 addr = f.readline().strip()
537 except EnvironmentError, e:
538 logging.warn(" - Failed to read hw address for %s from sysfs: %s",
545 def add_tap(self, path):
546 """ Add an interface to monitor
549 tap = os.path.basename(path)
551 logging.info("Updating configuration for %s", tap)
552 b = parse_binding_file(path)
555 ifindex = self.get_ifindex(b.tap)
558 logging.warn(" - Stale configuration for %s found", tap)
561 if self.mac_indexed_clients:
562 self.clients[b.mac] = b
565 self.clients[ifindex] = b
567 logging.info(" - Added client %s. %s", k, b)
569 def remove_tap(self, tap):
570 """ Cleanup clients on a removed interface
574 for k, cl in self.clients.items():
578 logging.info("Removed client %s. %s", k, cl)
580 logging.debug("Client on %s disappeared!!!", tap)
583 def dhcp_response(self, arg1, arg2=None): # pylint: disable=W0613,R0914
584 """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
587 logging.info(" * DHCP: Processing pending request")
588 # Workaround for supporting both squeezy's nfqueue-bindings-python
589 # and wheezy's python-nfqueue because for some reason the function's
590 # signature has changed and has broken compatibility
591 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
596 # Decode the response - NFQUEUE relays IP packets
597 pkt = IP(payload.get_data())
598 #logging.debug(pkt.show())
600 # Get the client MAC address
601 resp = pkt.getlayer(BOOTP).copy()
603 mac = resp.chaddr[:hlen].encode("hex")
604 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
606 # Server responses are always BOOTREPLYs
607 resp.op = "BOOTREPLY"
610 indev = get_indev(payload)
612 binding = self.get_binding(indev, mac)
614 # We don't know anything about this interface, so accept the packet
615 # and return an let the kernel handle it
616 payload.set_verdict(nfqueue.NF_ACCEPT)
619 # Signal the kernel that it shouldn't further process the packet
620 payload.set_verdict(nfqueue.NF_DROP)
622 if mac != binding.mac:
623 logging.warn(" - DHCP: Recieved spoofed request from %s (and not %s)",
628 logging.info(" - DHCP: No IP found in binding file %s.", binding)
632 logging.warn(" - DHCP: Invalid request with no DHCP payload found. %s", binding)
635 resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
636 IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
637 UDP(sport=pkt.dport, dport=pkt.sport)/resp
641 requested_addr = binding.ip
642 for opt in pkt[DHCP].options:
643 if type(opt) is tuple and opt[0] == "message-type":
645 if type(opt) is tuple and opt[0] == "requested_addr":
646 requested_addr = opt[1]
648 logging.info(" - DHCP: %s from %s",
649 DHCP_TYPES.get(req_type, "UNKNOWN"), binding)
652 domainname = self.dhcp_domain
654 domainname = binding.hostname.split('.', 1)[-1]
656 if req_type == DHCPREQUEST and requested_addr != binding.ip:
658 logging.info(" - DHCP: Sending DHCPNAK to %s (because requested %s)",
659 binding, requested_addr)
661 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
662 resp_type = DHCP_REQRESP[req_type]
663 resp.yiaddr = binding.ip
665 ("hostname", binding.hostname),
666 ("domain", domainname),
667 ("broadcast_address", str(subnet.broadcast)),
668 ("subnet_mask", str(subnet.netmask)),
669 ("renewal_time", self.lease_renewal),
670 ("lease_time", self.lease_lifetime),
673 dhcp_options += [("router", subnet.gw)]
674 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
676 elif req_type == DHCPINFORM:
677 resp_type = DHCP_REQRESP[req_type]
679 ("hostname", binding.hostname),
680 ("domain", domainname),
682 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
684 elif req_type == DHCPRELEASE:
686 logging.info(" - DHCP: DHCPRELEASE from %s", binding)
689 # Finally, always add the server identifier and end options
691 ("message-type", resp_type),
692 ("server_id", DHCP_DUMMY_SERVER_IP),
695 resp /= DHCP(options=dhcp_options)
697 logging.info(" - RESPONSE: %s for %s", DHCP_TYPES[resp_type], binding)
700 except socket.error, e:
701 logging.warn(" - DHCP: Response on %s failed: %s", binding, str(e))
703 logging.warn(" - DHCP: Unkown error during response on %s: %s",
706 def dhcpv6_response(self, arg1, arg2=None): # pylint: disable=W0613
708 logging.info(" * DHCPv6: Processing pending request")
709 # Workaround for supporting both squeezy's nfqueue-bindings-python
710 # and wheezy's python-nfqueue because for some reason the function's
711 # signature has changed and has broken compatibility
712 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
717 pkt = IPv6(payload.get_data())
718 indev = get_indev(payload)
720 #TODO: figure out how to find the src mac
722 binding = self.get_binding(indev, mac)
724 # We don't know anything about this interface, so accept the packet
725 # and return and let the kernel handle it
726 payload.set_verdict(nfqueue.NF_ACCEPT)
729 # Signal the kernel that it shouldn't further process the packet
730 payload.set_verdict(nfqueue.NF_DROP)
732 subnet = binding.net6
734 if subnet.net is None:
735 logging.debug(" - DHCPv6: No IPv6 network assigned to %s", binding)
738 indevmac = self.get_iface_hw_addr(binding.indev)
739 ifll = subnet.make_ll64(indevmac)
743 ofll = subnet.make_ll64(binding.mac)
747 if self.dhcpv6_domains:
748 domains = self.dhcpv6_domains
750 domains = [binding.hostname.split('.', 1)[-1]]
752 # We do this in order not to caclulate optlen ourselves
753 dnsdomains = str(DHCP6OptDNSDomains(dnsdomains=domains))
754 dnsservers = str(DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers))
756 resp = Ether(src=indevmac, dst=binding.mac)/\
757 IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
758 UDP(sport=pkt.dport, dport=pkt.sport)/\
759 DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
760 DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
761 DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
762 DHCP6OptDNSDomains(dnsdomains)/\
763 DHCP6OptDNSServers(dnsservers)
765 logging.info(" - RESPONSE: DHCPv6 reply for %s", binding)
769 except socket.error, e:
770 logging.warn(" - DHCPv6: Response on %s failed: %s",
773 logging.warn(" - DHCPv6: Unkown error during response on %s: %s",
777 def rs_response(self, arg1, arg2=None): # pylint: disable=W0613
778 """ Generate a reply to an ICMPv6 router solicitation
781 logging.info(" * RS: Processing pending request")
782 # Workaround for supporting both squeezy's nfqueue-bindings-python
783 # and wheezy's python-nfqueue because for some reason the function's
784 # signature has changed and has broken compatibility
785 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
790 pkt = IPv6(payload.get_data())
791 #logging.debug(pkt.show())
795 logging.debug(" - RS: Cannot obtain lladdr")
798 indev = get_indev(payload)
800 binding = self.get_binding(indev, mac)
802 # We don't know anything about this interface, so accept the packet
803 # and return and let the kernel handle it
804 payload.set_verdict(nfqueue.NF_ACCEPT)
807 # Signal the kernel that it shouldn't further process the packet
808 payload.set_verdict(nfqueue.NF_DROP)
810 if mac != binding.mac:
811 logging.warn(" - RS: Received spoofed request from %s (and not %s)",
815 subnet = binding.net6
817 if subnet.net is None:
818 logging.debug(" - RS: No IPv6 network assigned to %s", binding)
821 indevmac = self.get_iface_hw_addr(binding.indev)
822 ifll = subnet.make_ll64(indevmac)
826 resp = Ether(src=indevmac)/\
827 IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
828 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
829 prefixlen=subnet.prefixlen)
831 if self.ipv6_nameservers:
832 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
833 lifetime=self.ra_period * 3)
835 logging.info(" - RESPONSE: RA for %s", binding)
839 except socket.error, e:
840 logging.warn(" - RS: RA failed on %s: %s",
843 logging.warn(" - RS: Unkown error during RA on %s: %s",
846 def ns_response(self, arg1, arg2=None): # pylint: disable=W0613
847 """ Generate a reply to an ICMPv6 neighbor solicitation
851 logging.info(" * NS: Processing pending request")
852 # Workaround for supporting both squeezy's nfqueue-bindings-python
853 # and wheezy's python-nfqueue because for some reason the function's
854 # signature has changed and has broken compatibility
855 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
861 ns = IPv6(payload.get_data())
862 #logging.debug(ns.show())
866 logging.debug(" - NS: Cannot obtain lladdr")
870 indev = get_indev(payload)
872 binding = self.get_binding(indev, mac)
874 # We don't know anything about this interface, so accept the packet
875 # and return and let the kernel handle it
876 payload.set_verdict(nfqueue.NF_ACCEPT)
879 payload.set_verdict(nfqueue.NF_DROP)
881 if mac != binding.mac:
882 logging.warn(" - NS: Received spoofed request from %s (and not %s)",
886 subnet = binding.net6
887 if subnet.net is None:
888 logging.debug(" - NS: No IPv6 network assigned to %s", binding)
891 indevmac = self.get_iface_hw_addr(binding.indev)
893 ifll = subnet.make_ll64(indevmac)
897 if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
898 logging.debug(" - NS: Received NS for a non-routable IP (%s)", ns.tgt)
901 resp = Ether(src=indevmac, dst=binding.mac)/\
902 IPv6(src=str(ifll), dst=ns.src)/\
903 ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
904 ICMPv6NDOptDstLLAddr(lladdr=indevmac)
906 logging.info(" - RESPONSE: NA for %s ", binding)
910 except socket.error, e:
911 logging.warn(" - NS: NA on %s failed: %s",
914 logging.warn(" - NS: Unkown error during NA to %s: %s",
917 def send_periodic_ra(self):
918 # Use a separate thread as this may take a _long_ time with
919 # many interfaces and we want to be responsive in the mean time
920 threading.Thread(target=self._send_periodic_ra).start()
922 def _send_periodic_ra(self):
923 logging.info(" * Periodic RA: Starting...")
926 for binding in self.clients.values():
928 indev = binding.indev
930 subnet = binding.net6
931 if subnet.net is None:
932 logging.debug(" - Periodic RA: Skipping %s", binding)
934 indevmac = self.get_iface_hw_addr(indev)
935 ifll = subnet.make_ll64(indevmac)
938 resp = Ether(src=indevmac)/\
939 IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
940 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
941 prefixlen=subnet.prefixlen)
942 if self.ipv6_nameservers:
943 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
944 lifetime=self.ra_period * 3)
945 logging.info(" - RESPONSE: NA for %s ", binding)
948 except socket.error, e:
949 logging.warn(" - Periodic RA: Failed on %s: %s",
952 logging.warn(" - Periodic RA: Unkown error on %s: %s",
955 logging.info(" - Periodic RA: Sent %d RAs in %.2f seconds", i, time.time() - start)
958 """ Safely perform the main loop, freeing all resources upon exit
967 """ Loop forever, serving DHCP requests
972 # Yes, we are accessing _fd directly, but it's the only way to have a
973 # single select() loop ;-)
974 iwfd = self.notifier._fd # pylint: disable=W0212
977 if self.ipv6_enabled:
978 timeout = self.ra_period
979 self.send_periodic_ra()
985 rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
987 except select.error, e:
988 if e[0] == errno.EINTR:
989 logging.debug("select() got interrupted")
993 logging.warn("Warning: Exception on %s",
994 ", ".join([str(fd) for fd in xlist]))
998 # First check if there are any inotify (= configuration change)
1000 self.notifier.read_events()
1001 self.notifier.process_events()
1004 logging.debug("Pending requests on fds %s", rlist)
1008 q, num = self.nfq[fd]
1009 cnt = q.process_pending(num)
1010 logging.debug(" * Processed %d requests on NFQUEUE"
1011 " with fd %d", cnt, fd)
1012 except RuntimeError, e:
1013 logging.warn("Error processing fd %d: %s", fd, str(e))
1014 except Exception, e:
1015 logging.warn("Unknown error processing fd %d: %s",
1018 if self.ipv6_enabled:
1019 # Calculate the new timeout
1020 timeout = self.ra_period - (time.time() - start)
1024 self.send_periodic_ra()
1025 timeout = self.ra_period - (time.time() - start)
1027 def print_clients(self):
1028 logging.info("%10s %20s %20s %10s %20s %40s",
1029 'Key', 'Client', 'MAC', 'TAP', 'IP', 'IPv6')
1030 for k, cl in self.clients.items():
1031 logging.info("%10s | %20s %20s %10s %20s %40s",
1032 k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
1036 if __name__ == "__main__":
1039 from cStringIO import StringIO
1040 from pwd import getpwnam, getpwuid
1041 from configobj import ConfigObj, ConfigObjError, flatten_errors
1045 validator = validate.Validator()
1047 def is_ip_list(value, family=4):
1049 family = int(family)
1051 raise validate.VdtParamError(family)
1052 if isinstance(value, (str, unicode)):
1054 if not isinstance(value, list):
1055 raise validate.VdtTypeError(value)
1061 raise validate.VdtValueError(entry)
1063 if ip.version() != family:
1064 raise validate.VdtValueError(entry)
1067 validator.functions["ip_addr_list"] = is_ip_list
1068 config_spec = StringIO(CONFIG_SPEC)
1070 parser = optparse.OptionParser()
1071 parser.add_option("-c", "--config", dest="config_file",
1072 help="The location of the data files", metavar="FILE",
1073 default=DEFAULT_CONFIG)
1074 parser.add_option("-d", "--debug", action="store_true", dest="debug",
1075 help="Turn on debugging messages")
1076 parser.add_option("-f", "--foreground", action="store_false",
1077 dest="daemonize", default=True,
1078 help="Do not daemonize, stay in the foreground")
1080 opts, args = parser.parse_args()
1083 config = ConfigObj(opts.config_file, configspec=config_spec)
1084 except ConfigObjError, err:
1085 sys.stderr.write("Failed to parse config file %s: %s" %
1086 (opts.config_file, str(err)))
1089 results = config.validate(validator)
1091 logging.fatal("Configuration file validation failed! See errors below:")
1092 for (section_list, key, unused) in flatten_errors(config, results):
1094 logging.fatal(" '%s' in section '%s' failed validation",
1095 key, ", ".join(section_list))
1097 logging.fatal(" Section '%s' is missing",
1098 ", ".join(section_list))
1102 uid = getpwuid(config["general"].as_int("user"))
1104 uid = getpwnam(config["general"]["user"])
1106 # Keep only the capabilities we need
1107 # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1108 # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1109 # CAP_SETPCAP: needed by capng_change_id()
1110 capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1111 capng.capng_update(capng.CAPNG_ADD,
1112 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1113 capng.CAP_NET_ADMIN)
1114 capng.capng_update(capng.CAPNG_ADD,
1115 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1117 capng.capng_update(capng.CAPNG_ADD,
1118 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1121 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1122 capng.CAPNG_DROP_SUPP_GRP | \
1123 capng.CAPNG_CLEAR_BOUNDING)
1125 logger = logging.getLogger()
1127 logger.setLevel(logging.DEBUG)
1129 logger.setLevel(logging.INFO)
1132 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1133 handler = logging.handlers.WatchedFileHandler(logfile)
1135 handler = logging.StreamHandler()
1137 handler.setFormatter(logging.Formatter(LOG_FORMAT))
1138 logger.addHandler(handler)
1140 # Rename this process so 'ps' output looks like
1141 # this is a native executable.
1142 # NOTE: due to a bug in python-setproctitle, one cannot yet
1143 # set individual values for command-line arguments, so only show
1144 # the name of the executable instead.
1145 # setproctitle.setproctitle("\x00".join(sys.argv))
1146 setproctitle.setproctitle(sys.argv[0])
1149 pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1150 config["general"]["pidfile"], 10)
1151 # Remove any stale PID files, left behind by previous invocations
1152 if daemon.runner.is_pidfile_stale(pidfile):
1153 logger.warning("Removing stale PID lock file %s", pidfile.path)
1154 pidfile.break_lock()
1156 d = daemon.DaemonContext(pidfile=pidfile,
1158 stdout=handler.stream,
1159 stderr=handler.stream,
1160 files_preserve=[handler.stream])
1163 except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1164 logger.critical("Failed to lock pidfile %s,"
1165 " another instance running?", pidfile.path)
1168 logging.info("Starting up")
1169 logging.info("Running as %s (uid:%d, gid: %d)",
1170 config["general"]["user"], uid.pw_uid, uid.pw_gid)
1173 if config["dhcp"].as_bool("enable_dhcp"):
1175 "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1176 "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1177 "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1178 "dhcp_server_ip": config["dhcp"]["server_ip"],
1179 "dhcp_nameservers": config["dhcp"]["nameservers"],
1180 "dhcp_domain": config["dhcp"]["domain"],
1183 if config["ipv6"].as_bool("enable_ipv6"):
1185 "dhcpv6_queue_num": config["ipv6"].as_int("dhcp_queue"),
1186 "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1187 "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1188 "ra_period": config["ipv6"].as_int("ra_period"),
1189 "ipv6_nameservers": config["ipv6"]["nameservers"],
1190 "dhcpv6_domains": config["ipv6"]["domains"],
1193 # pylint: disable=W0142
1194 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1196 logging.info("Ready to serve requests")
1199 def debug_handler(signum, _):
1200 logging.debug('Received signal %d. Printing proxy state...', signum)
1201 proxy.print_clients()
1203 # Set the signal handler for debuging clients
1204 signal.signal(signal.SIGUSR1, debug_handler)
1205 signal.siginterrupt(signal.SIGUSR1, False)
1211 exc = "".join(traceback.format_exception(*sys.exc_info()))
1212 logging.critical(exc)
1216 # vim: set ts=4 sts=4 sw=4 et :