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
58 DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
59 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
60 DEFAULT_USER = "nobody"
61 DEFAULT_LEASE_LIFETIME = 604800 # 1 week
62 DEFAULT_LEASE_RENEWAL = 600 # 10 min
63 DEFAULT_RA_PERIOD = 300 # seconds
64 DHCP_DUMMY_SERVER_IP = "1.2.3.4"
66 LOG_FILENAME = "nfdhcpd.log"
68 SYSFS_NET = "/sys/class/net"
70 LOG_FORMAT = "%(asctime)-15s %(levelname)-8s %(message)s"
72 # Configuration file specification (see configobj documentation)
81 enable_dhcp = boolean(default=True)
82 lease_lifetime = integer(min=0, max=4294967295)
83 lease_renewal = integer(min=0, max=4294967295)
85 dhcp_queue = integer(min=0, max=65535)
86 nameservers = ip_addr_list(family=4)
87 domain = string(default=None)
90 enable_ipv6 = boolean(default=True)
91 ra_period = integer(min=1, max=4294967295)
92 rs_queue = integer(min=0, max=65535)
93 ns_queue = integer(min=0, max=65535)
94 nameservers = ip_addr_list(family=6)
108 DHCPDISCOVER: "DHCPDISCOVER",
109 DHCPOFFER: "DHCPOFFER",
110 DHCPREQUEST: "DHCPREQUEST",
111 DHCPDECLINE: "DHCPDECLINE",
114 DHCPRELEASE: "DHCPRELEASE",
115 DHCPINFORM: "DHCPINFORM",
119 DHCPDISCOVER: DHCPOFFER,
120 DHCPREQUEST: DHCPACK,
125 def get_indev(payload):
127 indev_ifindex = payload.get_physindev()
129 logging.debug(" - Incoming packet from bridge with ifindex %s",
132 except AttributeError:
133 #TODO: return error value
134 logging.debug("No get_physindev() supported")
137 indev_ifindex = payload.get_indev()
138 logging.debug(" - Incoming packet from tap with ifindex %s", indev_ifindex)
143 def parse_binding_file(path):
144 """ Read a client configuration from a tap file
147 logging.info("Parsing binding file %s", path)
149 iffile = open(path, 'r')
150 except EnvironmentError, e:
151 logging.warn(" - Unable to open binding file %s: %s", path, str(e))
154 tap = os.path.basename(path)
166 v = line.strip().split('=')[1]
172 if line.startswith("IP="):
174 elif line.startswith("MAC="):
175 mac = get_value(line)
176 elif line.startswith("HOSTNAME="):
177 hostname = get_value(line)
178 elif line.startswith("INDEV="):
179 indev = get_value(line)
180 elif line.startswith("SUBNET="):
181 subnet = get_value(line)
182 elif line.startswith("GATEWAY="):
183 gateway = get_value(line)
184 elif line.startswith("SUBNET6="):
185 subnet6 = get_value(line)
186 elif line.startswith("GATEWAY6="):
187 gateway6 = get_value(line)
188 elif line.startswith("EUI64="):
189 eui64 = get_value(line)
192 return Client(tap=tap, mac=mac, ip=ip, hostname=hostname,
193 indev=indev, subnet=subnet, gateway=gateway,
194 subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
196 logging.warning(" - Cannot add client for host %s and IP %s on tap %s",
201 class ClientFileHandler(pyinotify.ProcessEvent):
202 def __init__(self, server):
203 pyinotify.ProcessEvent.__init__(self)
206 def process_IN_DELETE(self, event): # pylint: disable=C0103
207 """ Delete file handler
209 Currently this removes an interface from the watch list
212 self.server.remove_tap(event.name)
214 def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
217 Currently this adds an interface to the watch list
220 self.server.add_tap(os.path.join(event.path, event.name))
223 class Client(object):
224 def __init__(self, tap=None, indev=None,
225 mac=None, ip=None, hostname=None,
226 subnet=None, gateway=None,
227 subnet6=None, gateway6=None, eui64=None):
230 self.hostname = hostname
234 self.gateway = gateway
235 self.net = Subnet(net=subnet, gw=gateway, dev=tap)
236 self.subnet6 = subnet6
237 self.gateway6 = gateway6
238 self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
243 return self.mac is not None and self.hostname is not None
246 def open_socket(self):
248 logging.info(" - Opening L2 socket and binding to %s", self.tap)
250 s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
251 s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
252 s.bind((self.tap, ETH_P_ALL))
254 except socket.error, e:
255 logging.warning(" - Cannot open socket %s", e)
258 def sendp(self, data):
260 if isinstance(data, BasePacket):
263 logging.debug(" - Sending raw packet %r", data)
266 count = self.socket.send(data, socket.MSG_DONTWAIT)
267 except socket.error, e:
268 logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
274 logging.debug(" - Sent %d bytes on %s", count, self.tap)
276 logging.warn(" - Truncated msg: %d/%d bytes sent",
280 class Subnet(object):
281 def __init__(self, net=None, gw=None, dev=None):
282 if isinstance(net, str):
284 self.net = IPy.IP(net)
285 except ValueError, e:
286 logging.warning(" - IPy error: %s", e)
295 """ Return the netmask in textual representation
298 return str(self.net.netmask())
302 """ Return the broadcast address in textual representation
305 return str(self.net.broadcast())
309 """ Return the network as an IPy.IP
312 return self.net.net()
316 """ Return the prefix length as an integer
319 return self.net.prefixlen()
322 def _make_eui64(net, mac):
323 """ Compute an EUI-64 address from an EUI-48 (MAC) address
328 comp = mac.split(":")
329 prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
330 eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
331 eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
332 for l in range(0, len(eui64), 2):
333 prefix += ["".join(eui64[l:l+2])]
334 return IPy.IP(":".join(prefix))
336 def make_eui64(self, mac):
337 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
341 return self._make_eui64(self.net, mac)
343 def make_ll64(self, mac):
344 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
347 return self._make_eui64("fe80::", mac)
350 class VMNetProxy(object): # pylint: disable=R0902
351 def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
352 rs_queue_num=None, ns_queue_num=None,
353 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
354 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
356 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
357 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
360 getattr(nfqueue.payload, 'get_physindev')
361 self.mac_indexed_clients = False
362 except AttributeError:
363 self.mac_indexed_clients = True
364 self.data_path = data_path
365 self.lease_lifetime = dhcp_lease_lifetime
366 self.lease_renewal = dhcp_lease_renewal
367 self.dhcp_domain = dhcp_domain
368 self.dhcp_server_ip = dhcp_server_ip
369 self.ra_period = ra_period
370 if dhcp_nameservers is None:
371 self.dhcp_nameserver = []
373 self.dhcp_nameservers = dhcp_nameservers
375 if ipv6_nameservers is None:
376 self.ipv6_nameservers = []
378 self.ipv6_nameservers = ipv6_nameservers
380 self.ipv6_enabled = False
389 self.wm = pyinotify.WatchManager()
390 mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
391 mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
392 inotify_handler = ClientFileHandler(self)
393 self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
394 self.wm.add_watch(self.data_path, mask, rec=True)
397 if dhcp_queue_num is not None:
398 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
400 if rs_queue_num is not None:
401 self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
402 self.ipv6_enabled = True
404 if ns_queue_num is not None:
405 self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
406 self.ipv6_enabled = True
408 def get_binding(self, ifindex, mac):
410 if self.mac_indexed_clients:
411 logging.debug(" - Getting binding for mac %s", mac)
412 b = self.clients[mac]
414 logging.debug(" - Getting binding for ifindex %s", ifindex)
415 b = self.clients[ifindex]
418 logging.debug(" - No client found for mac / ifindex %s / %s",
423 """ Free all resources for a graceful exit
426 logging.info("Cleaning up")
428 logging.debug(" - Closing netfilter queues")
429 for q, _ in self.nfq.values():
432 logging.debug(" - Stopping inotify watches")
435 logging.info(" - Cleanup finished")
437 def _setup_nfqueue(self, queue_num, family, callback, pending):
438 logging.info("Setting up NFQUEUE for queue %d, AF %s",
441 q.set_callback(callback)
442 q.fast_open(queue_num, family)
443 q.set_queue_maxlen(5000)
444 # This is mandatory for the queue to operate
445 q.set_mode(nfqueue.NFQNL_COPY_PACKET)
446 self.nfq[q.get_fd()] = (q, pending)
447 logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
449 def build_config(self):
452 for path in glob.glob(os.path.join(self.data_path, "*")):
457 def get_ifindex(self, iface):
458 """ Get the interface index from sysfs
461 logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
463 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
464 if not path.startswith(SYSFS_NET):
471 except EnvironmentError:
472 logging.debug(" - %s is probably down, removing", iface)
473 self.remove_tap(iface)
478 ifindex = f.readline().strip()
480 ifindex = int(ifindex)
481 except ValueError, e:
482 logging.warn(" - Failed to get ifindex for %s, cannot parse"
483 " sysfs output '%s'", iface, ifindex)
484 except EnvironmentError, e:
485 logging.warn(" - Error reading %s's ifindex from sysfs: %s",
487 self.remove_tap(iface)
493 def get_iface_hw_addr(self, iface):
494 """ Get the interface hardware address from sysfs
497 logging.debug(" - Getting mac for iface %s", iface)
498 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
499 if not path.startswith(SYSFS_NET):
505 except EnvironmentError:
506 logging.debug(" - %s is probably down, removing", iface)
507 self.remove_tap(iface)
511 addr = f.readline().strip()
512 except EnvironmentError, e:
513 logging.warn(" - Failed to read hw address for %s from sysfs: %s",
520 def add_tap(self, path):
521 """ Add an interface to monitor
524 tap = os.path.basename(path)
526 logging.info("Updating configuration for %s", tap)
527 b = parse_binding_file(path)
530 ifindex = self.get_ifindex(b.tap)
533 logging.warn(" - Stale configuration for %s found", tap)
536 if self.mac_indexed_clients:
537 self.clients[b.mac] = b
539 self.clients[ifindex] = b
540 logging.debug(" - Added client:")
541 logging.debug(" + %5s: %10s %20s %7s %15s",
542 ifindex, b.hostname, b.mac, b.tap, b.ip)
544 def remove_tap(self, tap):
545 """ Cleanup clients on a removed interface
549 for k, cl in self.clients.items():
551 logging.info("Removing client %s and closing socket on %s",
553 logging.debug(" - %10s | %10s %20s %10s %20s",
554 k, cl.hostname, cl.mac, cl.tap, cl.ip)
558 logging.debug("Client on %s disappeared!!!", tap)
561 def dhcp_response(self, arg1, arg2=None): # pylint: disable=W0613,R0914
562 """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
565 logging.info(" * Processing pending DHCP request")
566 # Workaround for supporting both squeezy's nfqueue-bindings-python
567 # and wheezy's python-nfqueue because for some reason the function's
568 # signature has changed and has broken compatibility
569 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
574 # Decode the response - NFQUEUE relays IP packets
575 pkt = IP(payload.get_data())
576 #logging.debug(pkt.show())
578 # Get the client MAC address
579 resp = pkt.getlayer(BOOTP).copy()
581 mac = resp.chaddr[:hlen].encode("hex")
582 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
584 # Server responses are always BOOTREPLYs
585 resp.op = "BOOTREPLY"
588 indev = get_indev(payload)
590 binding = self.get_binding(indev, mac)
592 # We don't know anything about this interface, so accept the packet
594 logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
595 # We don't know what to do with this packet, so let the kernel
597 payload.set_verdict(nfqueue.NF_ACCEPT)
600 # Signal the kernel that it shouldn't further process the packet
601 payload.set_verdict(nfqueue.NF_DROP)
603 if mac != binding.mac:
604 logging.warn(" - Recieved spoofed DHCP request: mac %s, indev %s",
609 logging.info(" - No IP found in binding file.")
612 logging.info(" - Generating DHCP response:"
613 " host %s, mac %s, tap %s, indev %s",
614 binding.hostname, mac, binding.tap, indev)
617 resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
618 IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
619 UDP(sport=pkt.dport, dport=pkt.sport)/resp
623 logging.warn(" - Invalid request from %s on %s, no DHCP"
624 " payload found", binding.mac, binding.tap)
628 requested_addr = binding.ip
629 for opt in pkt[DHCP].options:
630 if type(opt) is tuple and opt[0] == "message-type":
632 if type(opt) is tuple and opt[0] == "requested_addr":
633 requested_addr = opt[1]
635 logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
636 binding.mac, binding.tap)
639 domainname = self.dhcp_domain
641 domainname = binding.hostname.split('.', 1)[-1]
643 if req_type == DHCPREQUEST and requested_addr != binding.ip:
645 logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
646 " instead of %s", binding.mac, binding.tap,
647 requested_addr, binding.ip)
649 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
650 resp_type = DHCP_REQRESP[req_type]
651 resp.yiaddr = binding.ip
653 ("hostname", binding.hostname),
654 ("domain", domainname),
655 ("broadcast_address", str(subnet.broadcast)),
656 ("subnet_mask", str(subnet.netmask)),
657 ("renewal_time", self.lease_renewal),
658 ("lease_time", self.lease_lifetime),
661 dhcp_options += [("router", subnet.gw)]
662 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
664 elif req_type == DHCPINFORM:
665 resp_type = DHCP_REQRESP[req_type]
667 ("hostname", binding.hostname),
668 ("domain", domainname),
670 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
672 elif req_type == DHCPRELEASE:
674 logging.info(" - DHCPRELEASE from %s on %s",
675 binding.hostname, binding.tap)
678 # Finally, always add the server identifier and end options
680 ("message-type", resp_type),
681 ("server_id", DHCP_DUMMY_SERVER_IP),
684 resp /= DHCP(options=dhcp_options)
686 logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
687 binding.ip, binding.tap)
690 except socket.error, e:
691 logging.warn(" - DHCP response on %s (%s) failed: %s",
692 binding.tap, binding.hostname, str(e))
694 logging.warn(" - Unkown error during DHCP response on %s (%s): %s",
695 binding.tap, binding.hostname, str(e))
697 def rs_response(self, arg1, arg2=None): # pylint: disable=W0613
698 """ Generate a reply to a BOOTP/DHCP request
701 logging.info(" * Processing pending RS request")
702 # Workaround for supporting both squeezy's nfqueue-bindings-python
703 # and wheezy's python-nfqueue because for some reason the function's
704 # signature has changed and has broken compatibility
705 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
710 pkt = IPv6(payload.get_data())
711 #logging.debug(pkt.show())
715 logging.debug(" - Cannot obtain lladdr in rs")
718 indev = get_indev(payload)
720 binding = self.get_binding(indev, mac)
722 # We don't know anything about this interface, so accept the packet
724 logging.debug(" - Ignoring router solicitation on for mac %s", mac)
725 # We don't know what to do with this packet, so let the kernel
727 payload.set_verdict(nfqueue.NF_ACCEPT)
730 # Signal the kernel that it shouldn't further process the packet
731 payload.set_verdict(nfqueue.NF_DROP)
733 if mac != binding.mac:
734 logging.warn(" - Received spoofed RS request: mac %s, tap %s",
738 subnet = binding.net6
740 if subnet.net is None:
741 logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
744 indevmac = self.get_iface_hw_addr(binding.indev)
745 ifll = subnet.make_ll64(indevmac)
749 logging.info(" - Generating RA for host %s (mac %s) on tap %s",
750 binding.hostname, mac, binding.tap)
752 resp = Ether(src=indevmac)/\
753 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
754 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
755 prefixlen=subnet.prefixlen)
757 if self.ipv6_nameservers:
758 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
759 lifetime=self.ra_period * 3)
763 except socket.error, e:
764 logging.warn(" - RA on %s (%s) failed: %s",
765 binding.tap, binding.hostname, str(e))
767 logging.warn(" - Unkown error during RA on %s (%s): %s",
768 binding.tap, binding.hostname, str(e))
770 def ns_response(self, arg1, arg2=None): # pylint: disable=W0613
771 """ Generate a reply to an ICMPv6 neighbor solicitation
775 logging.info(" * Processing pending NS request")
776 # Workaround for supporting both squeezy's nfqueue-bindings-python
777 # and wheezy's python-nfqueue because for some reason the function's
778 # signature has changed and has broken compatibility
779 # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
785 ns = IPv6(payload.get_data())
786 #logging.debug(ns.show())
790 logging.debug(" - Cannot obtain lladdr from ns")
794 indev = get_indev(payload)
796 binding = self.get_binding(indev, mac)
798 # We don't know anything about this interface, so accept the packet
800 logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
802 # We don't know what to do with this packet, so let the kernel
804 payload.set_verdict(nfqueue.NF_ACCEPT)
807 payload.set_verdict(nfqueue.NF_DROP)
809 if mac != binding.mac:
810 logging.warn(" - Received spoofed NS request"
811 " for mac %s from tap %s", mac, binding.tap)
814 subnet = binding.net6
815 if subnet.net is None:
816 logging.debug(" - No IPv6 network assigned for the interface")
819 indevmac = self.get_iface_hw_addr(binding.indev)
821 ifll = subnet.make_ll64(indevmac)
825 if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
826 logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
829 logging.info(" - Generating NA for host %s (mac %s) on tap %s",
830 binding.hostname, mac, binding.tap)
832 resp = Ether(src=indevmac, dst=binding.mac)/\
833 IPv6(src=str(ifll), dst=ns.src)/\
834 ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
835 ICMPv6NDOptDstLLAddr(lladdr=indevmac)
839 except socket.error, e:
840 logging.warn(" - NA on %s (%s) failed: %s",
841 binding.tap, binding.hostname, str(e))
843 logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
844 binding.tap, binding.hostname, str(e))
846 def send_periodic_ra(self):
847 # Use a separate thread as this may take a _long_ time with
848 # many interfaces and we want to be responsive in the mean time
849 threading.Thread(target=self._send_periodic_ra).start()
851 def _send_periodic_ra(self):
852 logging.info("Sending out periodic RAs")
855 for binding in self.clients.values():
857 indev = binding.indev
859 subnet = binding.net6
860 if subnet.net is None:
861 logging.debug(" - Skipping periodic RA on interface %s,"
862 " as it is not IPv6-connected", tap)
864 indevmac = self.get_iface_hw_addr(indev)
865 ifll = subnet.make_ll64(indevmac)
868 resp = Ether(src=indevmac)/\
869 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
870 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
871 prefixlen=subnet.prefixlen)
872 if self.ipv6_nameservers:
873 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
874 lifetime=self.ra_period * 3)
877 except socket.error, e:
878 logging.warn(" - Periodic RA on %s (%s) failed: %s",
879 tap, binding.hostname, str(e))
881 logging.warn(" - Unkown error during periodic RA on %s (%s):"
882 " %s", tap, binding.hostname, str(e))
884 logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
887 """ Safely perform the main loop, freeing all resources upon exit
896 """ Loop forever, serving DHCP requests
901 # Yes, we are accessing _fd directly, but it's the only way to have a
902 # single select() loop ;-)
903 iwfd = self.notifier._fd # pylint: disable=W0212
906 if self.ipv6_enabled:
907 timeout = self.ra_period
908 self.send_periodic_ra()
914 rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
916 except select.error, e:
917 if e[0] == errno.EINTR:
918 logging.debug("select() got interrupted")
922 logging.warn("Warning: Exception on %s",
923 ", ".join([str(fd) for fd in xlist]))
927 # First check if there are any inotify (= configuration change)
929 self.notifier.read_events()
930 self.notifier.process_events()
933 logging.debug("Pending requests on fds %s", rlist)
937 q, num = self.nfq[fd]
938 cnt = q.process_pending(num)
939 logging.debug(" * Processed %d requests on NFQUEUE"
940 " with fd %d", cnt, fd)
941 except RuntimeError, e:
942 logging.warn("Error processing fd %d: %s", fd, str(e))
944 logging.warn("Unknown error processing fd %d: %s",
947 if self.ipv6_enabled:
948 # Calculate the new timeout
949 timeout = self.ra_period - (time.time() - start)
953 self.send_periodic_ra()
954 timeout = self.ra_period - (time.time() - start)
956 def print_clients(self):
957 logging.info("%10s %20s %20s %10s %20s",
958 'Key', 'Client', 'MAC', 'TAP', 'IP')
959 for k, cl in self.clients.items():
960 logging.info("%10s | %20s %20s %10s %20s",
961 k, cl.hostname, cl.mac, cl.tap, cl.ip)
965 if __name__ == "__main__":
968 from cStringIO import StringIO
969 from pwd import getpwnam, getpwuid
970 from configobj import ConfigObj, ConfigObjError, flatten_errors
974 validator = validate.Validator()
976 def is_ip_list(value, family=4):
980 raise validate.VdtParamError(family)
981 if isinstance(value, (str, unicode)):
983 if not isinstance(value, list):
984 raise validate.VdtTypeError(value)
990 raise validate.VdtValueError(entry)
992 if ip.version() != family:
993 raise validate.VdtValueError(entry)
996 validator.functions["ip_addr_list"] = is_ip_list
997 config_spec = StringIO(CONFIG_SPEC)
999 parser = optparse.OptionParser()
1000 parser.add_option("-c", "--config", dest="config_file",
1001 help="The location of the data files", metavar="FILE",
1002 default=DEFAULT_CONFIG)
1003 parser.add_option("-d", "--debug", action="store_true", dest="debug",
1004 help="Turn on debugging messages")
1005 parser.add_option("-f", "--foreground", action="store_false",
1006 dest="daemonize", default=True,
1007 help="Do not daemonize, stay in the foreground")
1009 opts, args = parser.parse_args()
1012 config = ConfigObj(opts.config_file, configspec=config_spec)
1013 except ConfigObjError, err:
1014 sys.stderr.write("Failed to parse config file %s: %s" %
1015 (opts.config_file, str(err)))
1018 results = config.validate(validator)
1020 logging.fatal("Configuration file validation failed! See errors below:")
1021 for (section_list, key, unused) in flatten_errors(config, results):
1023 logging.fatal(" '%s' in section '%s' failed validation",
1024 key, ", ".join(section_list))
1026 logging.fatal(" Section '%s' is missing",
1027 ", ".join(section_list))
1031 uid = getpwuid(config["general"].as_int("user"))
1033 uid = getpwnam(config["general"]["user"])
1035 # Keep only the capabilities we need
1036 # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1037 # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1038 # CAP_SETPCAP: needed by capng_change_id()
1039 capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1040 capng.capng_update(capng.CAPNG_ADD,
1041 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1042 capng.CAP_NET_ADMIN)
1043 capng.capng_update(capng.CAPNG_ADD,
1044 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1046 capng.capng_update(capng.CAPNG_ADD,
1047 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1050 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1051 capng.CAPNG_DROP_SUPP_GRP | \
1052 capng.CAPNG_CLEAR_BOUNDING)
1054 logger = logging.getLogger()
1056 logger.setLevel(logging.DEBUG)
1058 logger.setLevel(logging.INFO)
1061 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1062 handler = logging.handlers.WatchedFileHandler(logfile)
1064 handler = logging.StreamHandler()
1066 handler.setFormatter(logging.Formatter(LOG_FORMAT))
1067 logger.addHandler(handler)
1069 # Rename this process so 'ps' output looks like
1070 # this is a native executable.
1071 # NOTE: due to a bug in python-setproctitle, one cannot yet
1072 # set individual values for command-line arguments, so only show
1073 # the name of the executable instead.
1074 # setproctitle.setproctitle("\x00".join(sys.argv))
1075 setproctitle.setproctitle(sys.argv[0])
1078 pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1079 config["general"]["pidfile"], 10)
1080 # Remove any stale PID files, left behind by previous invocations
1081 if daemon.runner.is_pidfile_stale(pidfile):
1082 logger.warning("Removing stale PID lock file %s", pidfile.path)
1083 pidfile.break_lock()
1085 d = daemon.DaemonContext(pidfile=pidfile,
1087 stdout=handler.stream,
1088 stderr=handler.stream,
1089 files_preserve=[handler.stream])
1092 except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1093 logger.critical("Failed to lock pidfile %s,"
1094 " another instance running?", pidfile.path)
1097 logging.info("Starting up")
1098 logging.info("Running as %s (uid:%d, gid: %d)",
1099 config["general"]["user"], uid.pw_uid, uid.pw_gid)
1102 if config["dhcp"].as_bool("enable_dhcp"):
1104 "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1105 "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1106 "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1107 "dhcp_server_ip": config["dhcp"]["server_ip"],
1108 "dhcp_nameservers": config["dhcp"]["nameservers"],
1109 "dhcp_domain": config["dhcp"]["domain"],
1112 if config["ipv6"].as_bool("enable_ipv6"):
1114 "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1115 "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1116 "ra_period": config["ipv6"].as_int("ra_period"),
1117 "ipv6_nameservers": config["ipv6"]["nameservers"],
1120 # pylint: disable=W0142
1121 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1123 logging.info("Ready to serve requests")
1126 def debug_handler(signum, _):
1127 logging.debug('Received signal %d. Printing proxy state...', signum)
1128 proxy.print_clients()
1130 # Set the signal handler for debuging clients
1131 signal.signal(signal.SIGUSR1, debug_handler)
1132 signal.siginterrupt(signal.SIGUSR1, False)
1138 exc = "".join(traceback.format_exception(*sys.exc_info()))
1139 logging.critical(exc)
1143 # vim: set ts=4 sts=4 sw=4 et :