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.
28 import logging.handlers
35 import daemon.pidlockfile
39 from lockfile import LockTimeout
43 from select import select
44 from socket import AF_INET, AF_INET6
46 from scapy.data import ETH_P_ALL
47 from scapy.packet import BasePacket
48 from scapy.layers.l2 import Ether
49 from scapy.layers.inet import IP, UDP
50 from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
51 ICMPv6NDOptDstLLAddr, \
52 ICMPv6NDOptPrefixInfo, \
54 from scapy.layers.dhcp import BOOTP, DHCP
57 DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
58 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
59 DEFAULT_USER = "nobody"
60 DEFAULT_LEASE_LIFETIME = 604800 # 1 week
61 DEFAULT_LEASE_RENEWAL = 600 # 10 min
62 DEFAULT_RA_PERIOD = 300 # seconds
63 DHCP_DUMMY_SERVER_IP = "1.2.3.4"
65 LOG_FILENAME = "nfdhcpd.log"
67 SYSFS_NET = "/sys/class/net"
69 LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
71 # Configuration file specification (see configobj documentation)
80 enable_dhcp = boolean(default=True)
81 lease_lifetime = integer(min=0, max=4294967295)
82 lease_renewal = integer(min=0, max=4294967295)
84 dhcp_queue = integer(min=0, max=65535)
85 nameservers = ip_addr_list(family=4)
86 domain = string(default=None)
89 enable_ipv6 = boolean(default=True)
90 ra_period = integer(min=1, max=4294967295)
91 rs_queue = integer(min=0, max=65535)
92 ns_queue = integer(min=0, max=65535)
93 nameservers = ip_addr_list(family=6)
107 DHCPDISCOVER: "DHCPDISCOVER",
108 DHCPOFFER: "DHCPOFFER",
109 DHCPREQUEST: "DHCPREQUEST",
110 DHCPDECLINE: "DHCPDECLINE",
113 DHCPRELEASE: "DHCPRELEASE",
114 DHCPINFORM: "DHCPINFORM",
118 DHCPDISCOVER: DHCPOFFER,
119 DHCPREQUEST: DHCPACK,
124 def get_indev(payload):
126 indev_ifindex = payload.get_physindev()
128 logging.debug(" - Incomming packet from bridge %s", indev_ifindex)
130 except AttributeError:
131 #TODO: return error value
132 logging.debug("No get_physindev() supported")
135 indev_ifindex = payload.get_indev()
136 logging.debug(" - Incomming packet from tap %s", indev_ifindex)
141 def get_binding(proxy, ifindex, mac):
143 if proxy.mac_indexed_clients:
144 logging.debug(" - Getting binding for mac %s", mac)
145 b = proxy.clients[mac]
147 logging.debug(" - Getting binding for ifindex %s", ifindex)
148 b = proxy.clients[ifindex]
151 logging.debug(" - No client found for mac / ifindex %s / %s",
156 def parse_binding_file(path):
157 """ Read a client configuration from a tap file
160 logging.info("Parsing binding file %s", path)
162 iffile = open(path, 'r')
163 except EnvironmentError, e:
164 logging.warn(" - Unable to open binding file %s: %s", path, str(e))
167 tap = os.path.basename(path)
179 v = line.strip().split('=')[1]
185 if line.startswith("IP="):
187 elif line.startswith("MAC="):
188 mac = get_value(line)
189 elif line.startswith("HOSTNAME="):
190 hostname = get_value(line)
191 elif line.startswith("INDEV="):
192 indev = get_value(line)
193 elif line.startswith("SUBNET="):
194 subnet = get_value(line)
195 elif line.startswith("GATEWAY="):
196 gateway = get_value(line)
197 elif line.startswith("SUBNET6="):
198 subnet6 = get_value(line)
199 elif line.startswith("GATEWAY6="):
200 gateway6 = get_value(line)
201 elif line.startswith("EUI64="):
202 eui64 = get_value(line)
205 return Client(tap=tap, mac=mac, ip=ip, hostname=hostname,
206 indev=indev, subnet=subnet, gateway=gateway,
207 subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
209 logging.warning(" - Cannot add client for host %s and IP %s on tap %s",
214 class ClientFileHandler(pyinotify.ProcessEvent):
215 def __init__(self, server):
216 pyinotify.ProcessEvent.__init__(self)
219 def process_IN_DELETE(self, event): # pylint: disable=C0103
220 """ Delete file handler
222 Currently this removes an interface from the watch list
225 self.server.remove_tap(event.name)
227 def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
230 Currently this adds an interface to the watch list
233 self.server.add_tap(os.path.join(event.path, event.name))
236 class Client(object):
237 def __init__(self, tap=None, indev=None, mac=None, ip=None, hostname=None,
238 subnet=None, gateway=None, subnet6=None, gateway6=None, eui64=None ):
241 self.hostname = hostname
245 self.gateway = gateway
246 self.net = Subnet(net=subnet, gw=gateway, dev=tap)
247 self.subnet6 = subnet6
248 self.gateway6 = gateway6
249 self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
253 return self.mac is not None and self.ip is not None\
254 and self.hostname is not None
257 class Subnet(object):
258 def __init__(self, net=None, gw=None, dev=None):
259 if isinstance(net, str):
261 self.net = IPy.IP(net)
262 except ValueError, e:
263 logging.warning(" - IPy error: %s", e)
272 """ Return the netmask in textual representation
275 return str(self.net.netmask())
279 """ Return the broadcast address in textual representation
282 return str(self.net.broadcast())
286 """ Return the network as an IPy.IP
289 return self.net.net()
293 """ Return the prefix length as an integer
296 return self.net.prefixlen()
299 def _make_eui64(net, mac):
300 """ Compute an EUI-64 address from an EUI-48 (MAC) address
305 comp = mac.split(":")
306 prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
307 eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
308 eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
309 for l in range(0, len(eui64), 2):
310 prefix += ["".join(eui64[l:l+2])]
311 return IPy.IP(":".join(prefix))
313 def make_eui64(self, mac):
314 """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
318 return self._make_eui64(self.net, mac)
320 def make_ll64(self, mac):
321 """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
324 return self._make_eui64("fe80::", mac)
327 class VMNetProxy(object): # pylint: disable=R0902
328 def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
329 rs_queue_num=None, ns_queue_num=None,
330 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
331 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
333 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
334 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
337 getattr(nfqueue.payload, 'get_physindev')
338 self.mac_indexed_clients = False
339 except AttributeError:
340 self.mac_indexed_clients = True
341 self.data_path = data_path
342 self.lease_lifetime = dhcp_lease_lifetime
343 self.lease_renewal = dhcp_lease_renewal
344 self.dhcp_domain = dhcp_domain
345 self.dhcp_server_ip = dhcp_server_ip
346 self.ra_period = ra_period
347 if dhcp_nameservers is None:
348 self.dhcp_nameserver = []
350 self.dhcp_nameservers = dhcp_nameservers
352 if ipv6_nameservers is None:
353 self.ipv6_nameservers = []
355 self.ipv6_nameservers = ipv6_nameservers
357 self.ipv6_enabled = False
364 self.l2socket = self._socket()
367 self.wm = pyinotify.WatchManager()
368 mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
369 mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
370 inotify_handler = ClientFileHandler(self)
371 self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
372 self.wm.add_watch(self.data_path, mask, rec=True)
375 if dhcp_queue_num is not None:
376 self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
378 if rs_queue_num is not None:
379 self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
380 self.ipv6_enabled = True
382 if ns_queue_num is not None:
383 self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
384 self.ipv6_enabled = True
387 logging.info("Opening L2 socket")
389 s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
390 s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
391 except socket.error, e:
392 logging.warning(" - Cannot open socket %s", e)
396 """ Free all resources for a graceful exit
399 logging.info("Cleaning up")
401 logging.debug(" - Closing netfilter queues")
402 for q in self.nfq.values():
405 logging.debug(" - Closing socket")
406 self.l2socket.close()
408 logging.debug(" - Stopping inotify watches")
411 logging.info(" - Cleanup finished")
413 def _setup_nfqueue(self, queue_num, family, callback):
414 logging.info("Setting up NFQUEUE for queue %d, AF %s",
417 q.set_callback(callback)
418 q.fast_open(queue_num, family)
419 q.set_queue_maxlen(5000)
420 # This is mandatory for the queue to operate
421 q.set_mode(nfqueue.NFQNL_COPY_PACKET)
422 self.nfq[q.get_fd()] = q
423 logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
425 def sendp(self, data, dev):
426 """ Send a raw packet using a layer-2 socket
429 if isinstance(data, BasePacket):
432 logging.debug(" - Sending raw packet %r", data)
434 self.l2socket.bind((dev, ETH_P_ALL))
436 count = self.l2socket.send(data, socket.MSG_DONTWAIT)
437 except socket.error, e:
438 logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
439 self.l2socket.close()
440 self.l2socket = self._socket()
444 logging.debug(" - Sent %d bytes to device %s", count, dev)
446 logging.warn(" - Truncated send on %s (%d/%d bytes sent)",
449 def build_config(self):
452 for path in glob.glob(os.path.join(self.data_path, "*")):
455 logging.debug("%15s %20s %7s %15s", 'Client', 'MAC', 'TAP', 'IP')
456 for b in self.clients.values():
457 logging.debug("%15s %20s %7s %15s", b.hostname, b.mac, b.tap, b.ip)
459 def get_ifindex(self, iface):
460 """ Get the interface index from sysfs
463 logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
465 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
466 if not path.startswith(SYSFS_NET):
473 except EnvironmentError:
474 logging.debug(" - %s is probably down, removing", iface)
475 self.remove_tap(iface)
480 ifindex = f.readline().strip()
482 ifindex = int(ifindex)
483 except ValueError, e:
484 logging.warn(" - Failed to get ifindex for %s, cannot parse"
485 " sysfs output '%s'", iface, ifindex)
486 except EnvironmentError, e:
487 logging.warn(" - Error reading %s's ifindex from sysfs: %s",
489 self.remove_tap(iface)
495 def get_iface_hw_addr(self, iface):
496 """ Get the interface hardware address from sysfs
499 logging.debug(" - Getting mac for iface %s", iface)
500 path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
501 if not path.startswith(SYSFS_NET):
507 except EnvironmentError:
508 logging.debug(" - %s is probably down, removing", iface)
509 self.remove_tap(iface)
513 addr = f.readline().strip()
514 except EnvironmentError, e:
515 logging.warn(" - Failed to read hw address for %s from sysfs: %s",
522 def add_tap(self, path):
523 """ Add an interface to monitor
526 tap = os.path.basename(path)
528 logging.debug("Updating configuration for %s", tap)
529 b = parse_binding_file(path)
532 ifindex = self.get_ifindex(b.tap)
535 logging.warn(" - Stale configuration for %s found", tap)
538 if self.mac_indexed_clients:
539 self.clients[b.mac] = b
541 self.clients[ifindex] = b
542 logging.debug(" - Added client:")
543 logging.debug(" + %5s: %10s %20s %7s %15s",
544 ifindex, b.hostname, b.mac, b.tap, b.ip)
546 def remove_tap(self, tap):
547 """ Cleanup clients on a removed interface
551 for k in self.clients.keys():
553 if self.clients[k].tap == tap:
554 logging.debug("Removing client on interface %s", tap)
555 logging.debug(" - %5s: %10s %20s %7s %15s",
556 k, b.hostname, b.mac, b.tap, b.ip)
559 logging.debug("Client on %s disappeared!!!", tap)
561 def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
562 """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
565 logging.debug(" * Processing pending DHCP request (NFQUEUE %d)", i)
566 # Decode the response - NFQUEUE relays IP packets
567 pkt = IP(payload.get_data())
568 #logging.debug(pkt.show())
570 # Get the client MAC address
571 resp = pkt.getlayer(BOOTP).copy()
573 mac = resp.chaddr[:hlen].encode("hex")
574 mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
576 # Server responses are always BOOTREPLYs
577 resp.op = "BOOTREPLY"
580 indev = get_indev(payload)
582 binding = get_binding(self, indev, mac)
584 # We don't know anything about this interface, so accept the packet
586 logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
587 # We don't know what to do with this packet, so let the kernel
589 payload.set_verdict(nfqueue.NF_ACCEPT)
592 # Signal the kernel that it shouldn't further process the packet
593 payload.set_verdict(nfqueue.NF_DROP)
595 if mac != binding.mac:
596 logging.warn(" - Recieved spoofed DHCP request for mac %s from tap %s", mac, indev)
599 logging.debug(" - Generating DHCP response for host %s (mac %s) on tap %s",
600 binding.hostname, mac, binding.tap)
603 resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
604 IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
605 UDP(sport=pkt.dport, dport=pkt.sport)/resp
609 logging.warn(" - Invalid request from %s on %s, no DHCP"
610 " payload found", binding.mac, binding.tap)
614 requested_addr = binding.ip
615 for opt in pkt[DHCP].options:
616 if type(opt) is tuple and opt[0] == "message-type":
618 if type(opt) is tuple and opt[0] == "requested_addr":
619 requested_addr = opt[1]
621 logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
622 binding.mac, binding.tap)
625 domainname = self.dhcp_domain
627 domainname = binding.hostname.split('.', 1)[-1]
629 if req_type == DHCPREQUEST and requested_addr != binding.ip:
631 logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
632 " instead of %s", binding.mac, binding.tap,
633 requested_addr, binding.ip)
635 elif req_type in (DHCPDISCOVER, DHCPREQUEST):
636 resp_type = DHCP_REQRESP[req_type]
637 resp.yiaddr = binding.ip
639 ("hostname", binding.hostname),
640 ("domain", domainname),
641 ("broadcast_address", str(subnet.broadcast)),
642 ("subnet_mask", str(subnet.netmask)),
643 ("renewal_time", self.lease_renewal),
644 ("lease_time", self.lease_lifetime),
647 dhcp_options += [("router", subnet.gw)]
648 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
650 elif req_type == DHCPINFORM:
651 resp_type = DHCP_REQRESP[req_type]
653 ("hostname", binding.hostname),
654 ("domain", domainname),
656 dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
658 elif req_type == DHCPRELEASE:
660 logging.info(" - DHCPRELEASE from %s on %s", binding.mac, binding.tap)
663 # Finally, always add the server identifier and end options
665 ("message-type", resp_type),
666 ("server_id", DHCP_DUMMY_SERVER_IP),
669 resp /= DHCP(options=dhcp_options)
671 logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
672 binding.ip, binding.tap)
674 self.sendp(resp, binding.indev)
675 except socket.error, e:
676 logging.warn(" - DHCP response on %s failed: %s", binding.indev, str(e))
678 logging.warn(" - Unkown error during DHCP response on %s: %s",
679 binding.indev, str(e))
681 def rs_response(self, i, payload): # pylint: disable=W0613
682 """ Generate a reply to a BOOTP/DHCP request
685 logging.debug(" * Processing pending RS request (NFQUEUE %d)", i)
686 pkt = IPv6(payload.get_data())
687 #logging.debug(pkt.show())
691 logging.debug(" - Cannot obtain lladdr in rs")
694 indev = get_indev(payload)
696 binding = get_binding(self, indev, mac)
698 # We don't know anything about this interface, so accept the packet
700 logging.debug(" - Ignoring router solicitation on for mac %s", mac)
701 # We don't know what to do with this packet, so let the kernel
703 payload.set_verdict(nfqueue.NF_ACCEPT)
706 # Signal the kernel that it shouldn't further process the packet
707 payload.set_verdict(nfqueue.NF_DROP)
709 if mac != binding.mac:
710 logging.warn(" - Received spoofed RS request for mac %s from tap %s",
714 subnet = binding.net6
716 if subnet.net is None:
717 logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
720 indevmac = self.get_iface_hw_addr(binding.indev)
721 ifll = subnet.make_ll64(indevmac)
725 logging.debug(" - Generating RA for host %s (mac %s) on tap %s",
726 binding.hostname, mac, binding.tap)
728 resp = Ether(src=indevmac)/\
729 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
730 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
731 prefixlen=subnet.prefixlen)
733 if self.ipv6_nameservers:
734 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
735 lifetime=self.ra_period * 3)
738 self.sendp(resp, binding.indev)
739 except socket.error, e:
740 logging.warn(" - RA on %s failed: %s", binding.indev, str(e))
742 logging.warn(" - Unkown error during RA on %s: %s",
743 binding.indev, str(e))
745 def ns_response(self, i, payload): # pylint: disable=W0613
746 """ Generate a reply to an ICMPv6 neighbor solicitation
750 logging.debug(" * Processing pending NS request (NFQuEUE %d)", i)
752 ns = IPv6(payload.get_data())
753 #logging.debug(ns.show())
757 logging.debug(" - Cannot obtain lladdr from ns")
761 indev = get_indev(payload)
763 binding = get_binding(self, indev, mac)
765 # We don't know anything about this interface, so accept the packet
767 logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
769 # We don't know what to do with this packet, so let the kernel
771 payload.set_verdict(nfqueue.NF_ACCEPT)
774 payload.set_verdict(nfqueue.NF_DROP)
776 if mac != binding.mac:
777 logging.warn(" - Received spoofed NS request"
778 " for mac %s from tap %s", mac, binding.tap)
781 subnet = binding.net6
782 if subnet.net is None:
783 logging.debug(" - No IPv6 network assigned for the interface")
786 indevmac = self.get_iface_hw_addr(binding.indev)
788 ifll = subnet.make_ll64(indevmac)
792 if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
793 logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
796 logging.debug(" - Generating NA for host %s (mac %s) on tap %s",
797 binding.hostname, mac, binding.tap)
799 resp = Ether(src=indevmac, dst=binding.mac)/\
800 IPv6(src=str(ifll), dst=ns.src)/\
801 ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
802 ICMPv6NDOptDstLLAddr(lladdr=indevmac)
805 self.sendp(resp, binding.indev)
806 except socket.error, e:
807 logging.warn(" - NA on %s failed: %s", binding.indev, str(e))
809 logging.warn(" - Unkown error during periodic NA on %s: %s",
810 binding.indev, str(e))
812 def send_periodic_ra(self):
813 # Use a separate thread as this may take a _long_ time with
814 # many interfaces and we want to be responsive in the mean time
815 threading.Thread(target=self._send_periodic_ra).start()
817 def _send_periodic_ra(self):
818 logging.debug("Sending out periodic RAs")
821 for binding in self.clients.values():
823 indev = binding.indev
825 subnet = binding.net6
826 if subnet.net is None:
827 logging.debug(" - Skipping periodic RA on interface %s,"
828 " as it is not IPv6-connected", tap)
830 indevmac = self.get_iface_hw_addr(indev)
831 ifll = subnet.make_ll64(indevmac)
834 resp = Ether(src=indevmac)/\
835 IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
836 ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
837 prefixlen=subnet.prefixlen)
838 if self.ipv6_nameservers:
839 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
840 lifetime=self.ra_period * 3)
842 self.sendp(resp, indev)
843 except socket.error, e:
844 logging.warn(" - Periodic RA on %s failed: %s", tap, str(e))
846 logging.warn(" - Unkown error during periodic RA on %s: %s",
849 logging.debug(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
852 """ Safely perform the main loop, freeing all resources upon exit
861 """ Loop forever, serving DHCP requests
866 # Yes, we are accessing _fd directly, but it's the only way to have a
867 # single select() loop ;-)
868 iwfd = self.notifier._fd # pylint: disable=W0212
871 if self.ipv6_enabled:
872 timeout = self.ra_period
873 self.send_periodic_ra()
878 rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
880 logging.warn("Warning: Exception on %s",
881 ", ".join([str(fd) for fd in xlist]))
885 # First check if there are any inotify (= configuration change)
887 self.notifier.read_events()
888 self.notifier.process_events()
891 logging.debug("Pending requests on fds %s", rlist)
895 cnt = self.nfq[fd].process_pending()
896 logging.debug(" * Processed %d requests on NFQUEUE"
897 " with fd %d", cnt, fd)
898 except RuntimeError, e:
899 logging.warn("Error processing fd %d: %s", fd, str(e))
901 logging.warn("Unknown error processing fd %d: %s",
904 if self.ipv6_enabled:
905 # Calculate the new timeout
906 timeout = self.ra_period - (time.time() - start)
910 self.send_periodic_ra()
911 timeout = self.ra_period - (time.time() - start)
914 if __name__ == "__main__":
917 from cStringIO import StringIO
918 from pwd import getpwnam, getpwuid
919 from configobj import ConfigObj, ConfigObjError, flatten_errors
923 validator = validate.Validator()
925 def is_ip_list(value, family=4):
929 raise validate.VdtParamError(family)
930 if isinstance(value, (str, unicode)):
932 if not isinstance(value, list):
933 raise validate.VdtTypeError(value)
939 raise validate.VdtValueError(entry)
941 if ip.version() != family:
942 raise validate.VdtValueError(entry)
945 validator.functions["ip_addr_list"] = is_ip_list
946 config_spec = StringIO(CONFIG_SPEC)
948 parser = optparse.OptionParser()
949 parser.add_option("-c", "--config", dest="config_file",
950 help="The location of the data files", metavar="FILE",
951 default=DEFAULT_CONFIG)
952 parser.add_option("-d", "--debug", action="store_true", dest="debug",
953 help="Turn on debugging messages")
954 parser.add_option("-f", "--foreground", action="store_false",
955 dest="daemonize", default=True,
956 help="Do not daemonize, stay in the foreground")
958 opts, args = parser.parse_args()
961 config = ConfigObj(opts.config_file, configspec=config_spec)
962 except ConfigObjError, err:
963 sys.stderr.write("Failed to parse config file %s: %s" %
964 (opts.config_file, str(err)))
967 results = config.validate(validator)
969 logging.fatal("Configuration file validation failed! See errors below:")
970 for (section_list, key, unused) in flatten_errors(config, results):
972 logging.fatal(" '%s' in section '%s' failed validation",
973 key, ", ".join(section_list))
975 logging.fatal(" Section '%s' is missing",
976 ", ".join(section_list))
979 logger = logging.getLogger()
981 logger.setLevel(logging.DEBUG)
983 logger.setLevel(logging.INFO)
986 logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
987 handler = logging.handlers.RotatingFileHandler(logfile,
990 handler = logging.StreamHandler()
992 handler.setFormatter(logging.Formatter(LOG_FORMAT))
993 logger.addHandler(handler)
995 # Rename this process so 'ps' output looks like
996 # this is a native executable
997 setproctitle.setproctitle(" ".join(sys.argv))
1000 pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1001 config["general"]["pidfile"], 10)
1002 # Remove any stale PID files, left behind by previous invocations
1003 if daemon.runner.is_pidfile_stale(pidfile):
1004 logger.warning("Removing stale PID lock file %s", pidfile.path)
1005 pidfile.break_lock()
1007 d = daemon.DaemonContext(pidfile=pidfile,
1009 stdout=handler.stream,
1010 stderr=handler.stream,
1011 files_preserve=[handler.stream])
1014 except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1015 logger.critical("Failed to lock pidfile %s,"
1016 " another instance running?", pidfile.path)
1019 logging.info("Starting up")
1022 if config["dhcp"].as_bool("enable_dhcp"):
1024 "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1025 "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1026 "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1027 "dhcp_server_ip": config["dhcp"]["server_ip"],
1028 "dhcp_nameservers": config["dhcp"]["nameservers"],
1029 "dhcp_domain": config["dhcp"]["domain"],
1032 if config["ipv6"].as_bool("enable_ipv6"):
1034 "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1035 "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1036 "ra_period": config["ipv6"].as_int("ra_period"),
1037 "ipv6_nameservers": config["ipv6"]["nameservers"],
1040 # pylint: disable=W0142
1041 proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1043 # Drop all capabilities except CAP_NET_RAW and change uid
1045 uid = getpwuid(config["general"].as_int("user"))
1047 uid = getpwnam(config["general"]["user"])
1049 logging.debug("Setting capabilities and changing uid")
1050 logging.debug("User: %s, uid: %d, gid: %d",
1051 config["general"]["user"], uid.pw_uid, uid.pw_gid)
1053 # Keep only the capabilities we need
1054 # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1055 # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1056 # CAP_SETPCAP: needed by capng_change_id()
1057 capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1058 capng.capng_update(capng.CAPNG_ADD,
1059 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1060 capng.CAP_NET_ADMIN)
1061 capng.capng_update(capng.CAPNG_ADD,
1062 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1064 capng.capng_update(capng.CAPNG_ADD,
1065 capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1067 capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1068 capng.CAPNG_DROP_SUPP_GRP | capng.CAPNG_CLEAR_BOUNDING)
1070 logging.info("Ready to serve requests")
1075 exc = "".join(traceback.format_exception(*sys.exc_info()))
1076 logging.critical(exc)
1080 # vim: set ts=4 sts=4 sw=4 et :