Enable logging of unhandled exceptions
[snf-nfdhcpd] / nfdhcpd
1 #!/usr/bin/env python
2 #
3
4 # nfdcpd: A promiscuous, NFQUEUE-based DHCP server for virtual machine hosting
5 # Copyright (c) 2010 GRNET SA
6 #
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.
11 #
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.
16 #
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.
20 #
21
22 import os
23 import re
24 import sys
25 import glob
26 import time
27 import logging
28 import logging.handlers
29 import threading
30 import traceback
31 import subprocess
32
33 import daemon
34 import nfqueue
35 import pyinotify
36
37 import IPy
38 import socket
39 from select import select
40 from socket import AF_INET, AF_INET6
41
42 from scapy.data import ETH_P_ALL
43 from scapy.packet import BasePacket
44 from scapy.layers.l2 import Ether
45 from scapy.layers.inet import IP, UDP
46 from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
47                                ICMPv6NDOptDstLLAddr, \
48                                ICMPv6NDOptPrefixInfo, \
49                                ICMPv6NDOptRDNSS
50 from scapy.layers.dhcp import BOOTP, DHCP
51
52 DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
53 DEFAULT_PATH = "/var/run/ganeti-dhcpd"
54 DEFAULT_USER = "nobody"
55 DEFAULT_LEASE_LIFETIME = 604800 # 1 week
56 DEFAULT_LEASE_RENEWAL = 600  # 10 min
57 DEFAULT_RA_PERIOD = 300 # seconds
58 DHCP_DUMMY_SERVER_IP = "1.2.3.4"
59
60 LOG_FILENAME = "nfdhcpd.log"
61
62 SYSFS_NET = "/sys/class/net"
63
64 LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
65
66 # Configuration file specification (see configobj documentation)
67 CONFIG_SPEC = """
68 [general]
69 pidfile = string()
70 datapath = string()
71 logdir = string()
72 user = string()
73
74 [dhcp]
75 enable_dhcp = boolean(default=True)
76 lease_lifetime = integer(min=0, max=4294967295)
77 lease_renewal = integer(min=0, max=4294967295)
78 server_ip = ip_addr()
79 dhcp_queue = integer(min=0, max=65535)
80 nameservers = ip_addr_list(family=4)
81
82 [ipv6]
83 enable_ipv6 = boolean(default=True)
84 ra_period = integer(min=1, max=4294967295)
85 rs_queue = integer(min=0, max=65535)
86 ns_queue = integer(min=0, max=65535)
87 nameservers = ip_addr_list(family=6)
88 """
89
90
91 DHCPDISCOVER = 1
92 DHCPOFFER = 2
93 DHCPREQUEST = 3
94 DHCPDECLINE = 4
95 DHCPACK = 5
96 DHCPNAK = 6
97 DHCPRELEASE = 7
98 DHCPINFORM = 8
99
100 DHCP_TYPES = {
101     DHCPDISCOVER: "DHCPDISCOVER",
102     DHCPOFFER: "DHCPOFFER",
103     DHCPREQUEST: "DHCPREQUEST",
104     DHCPDECLINE: "DHCPDECLINE",
105     DHCPACK: "DHCPACK",
106     DHCPNAK: "DHCPNAK",
107     DHCPRELEASE: "DHCPRELEASE",
108     DHCPINFORM: "DHCPINFORM",
109 }
110
111 DHCP_REQRESP = {
112     DHCPDISCOVER: DHCPOFFER,
113     DHCPREQUEST: DHCPACK,
114     DHCPINFORM: DHCPACK,
115     }
116
117
118 def parse_routing_table(table="main", family=4):
119     """ Parse the given routing table to get connected route, gateway and
120     default device.
121
122     """
123     ipro = subprocess.Popen(["ip", "-%d" % family, "ro", "ls",
124                              "table", table], stdout=subprocess.PIPE)
125     routes = ipro.stdout.readlines()
126
127     def_gw = None
128     def_dev = None
129     def_net = None
130
131     for route in routes:
132         match = re.match(r'^default.*via ([^\s]+).*dev ([^\s]+)', route)
133         if match:
134             def_gw, def_dev = match.groups()
135             break
136
137     for route in routes:
138         # Find the least-specific connected route
139         m = re.match("^([^\\s]+) dev %s" % def_dev, route)
140         if not m:
141             continue
142
143         if family == 6 and m.group(1).startswith("fe80:"):
144             # Skip link-local declarations in "main" table
145             continue
146
147         def_net = m.group(1)
148
149         try:
150             def_net = IPy.IP(def_net)
151         except ValueError, e:
152             logging.warn("Unable to parse default route entry %s: %s",
153                          def_net, str(e))
154
155     return Subnet(net=def_net, gw=def_gw, dev=def_dev)
156
157
158 def parse_binding_file(path):
159     """ Read a client configuration from a tap file
160
161     """
162     try:
163         iffile = open(path, 'r')
164     except EnvironmentError, e:
165         logging.warn("Unable to open binding file %s: %s", path, str(e))
166         return None
167
168     mac = None
169     ips = None
170     link = None
171     hostname = None
172
173     for line in iffile:
174         if line.startswith("IP="):
175             ip = line.strip().split("=")[1]
176             ips = ip.split()
177         elif line.startswith("MAC="):
178             mac = line.strip().split("=")[1]
179         elif line.startswith("LINK="):
180             link = line.strip().split("=")[1]
181         elif line.startswith("HOSTNAME="):
182             hostname = line.strip().split("=")[1]
183
184     return Client(mac=mac, ips=ips, link=link, hostname=hostname)
185
186
187 class ClientFileHandler(pyinotify.ProcessEvent):
188     def __init__(self, server):
189         pyinotify.ProcessEvent.__init__(self)
190         self.server = server
191
192     def process_IN_DELETE(self, event): # pylint: disable=C0103
193         """ Delete file handler
194
195         Currently this removes an interface from the watch list
196
197         """
198         self.server.remove_iface(event.name)
199
200     def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
201         """ Add file handler
202
203         Currently this adds an interface to the watch list
204
205         """
206         self.server.add_iface(os.path.join(event.path, event.name))
207
208
209 class Client(object):
210     def __init__(self, mac=None, ips=None, link=None, hostname=None):
211         self.mac = mac
212         self.ips = ips
213         self.hostname = hostname
214         self.link = link
215         self.iface = None
216
217     @property
218     def ip(self):
219         return self.ips[0]
220
221     def is_valid(self):
222         return self.mac is not None and self.ips is not None\
223                and self.hostname is not None
224
225
226 class Subnet(object):
227     def __init__(self, net=None, gw=None, dev=None):
228         if isinstance(net, str):
229             self.net = IPy.IP(net)
230         else:
231             self.net = net
232         self.gw = gw
233         self.dev = dev
234
235     @property
236     def netmask(self):
237         """ Return the netmask in textual representation
238
239         """
240         return str(self.net.netmask())
241
242     @property
243     def broadcast(self):
244         """ Return the broadcast address in textual representation
245
246         """
247         return str(self.net.broadcast())
248
249     @property
250     def prefix(self):
251         """ Return the network as an IPy.IP
252
253         """
254         return self.net.net()
255
256     @property
257     def prefixlen(self):
258         """ Return the prefix length as an integer
259
260         """
261         return self.net.prefixlen()
262
263     @staticmethod
264     def _make_eui64(net, mac):
265         """ Compute an EUI-64 address from an EUI-48 (MAC) address
266
267         """
268         comp = mac.split(":")
269         prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
270         eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
271         eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
272         for l in range(0, len(eui64), 2):
273             prefix += ["".join(eui64[l:l+2])]
274         return IPy.IP(":".join(prefix))
275
276     def make_eui64(self, mac):
277         """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
278         subnet.
279
280         """
281         return self._make_eui64(self.net, mac)
282
283     def make_ll64(self, mac):
284         """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
285
286         """
287         return self._make_eui64("fe80::", mac)
288
289
290 class VMNetProxy(object): # pylint: disable=R0902
291     def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
292                  rs_queue_num=None, ns_queue_num=None,
293                  dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
294                  dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
295                  dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
296                  ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
297
298         self.data_path = data_path
299         self.lease_lifetime = dhcp_lease_lifetime
300         self.lease_renewal = dhcp_lease_renewal
301         self.dhcp_server_ip = dhcp_server_ip
302         self.ra_period = ra_period
303         if dhcp_nameservers is None:
304             self.dhcp_nameserver = []
305         else:
306             self.dhcp_nameservers = dhcp_nameservers
307
308         if ipv6_nameservers is None:
309             self.ipv6_nameservers = []
310         else:
311             self.ipv6_nameservers = ipv6_nameservers
312
313         self.ipv6_enabled = False
314
315         self.clients = {}
316         self.subnets = {}
317         self.ifaces = {}
318         self.v6nets = {}
319         self.nfq = {}
320         self.l2socket = socket.socket(socket.AF_PACKET,
321                                       socket.SOCK_RAW, ETH_P_ALL)
322         self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
323
324         # Inotify setup
325         self.wm = pyinotify.WatchManager()
326         mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
327         mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
328         inotify_handler = ClientFileHandler(self)
329         self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
330         self.wm.add_watch(self.data_path, mask, rec=True)
331
332         # NFQUEUE setup
333         if dhcp_queue_num is not None:
334             self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
335
336         if rs_queue_num is not None:
337             self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
338             self.ipv6_enabled = True
339
340         if ns_queue_num is not None:
341             self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
342             self.ipv6_enabled = True
343
344     def _cleanup(self):
345         """ Free all resources for a graceful exit
346
347         """
348         logging.info("Cleaning up")
349
350         logging.debug("Closing netfilter queues")
351         for q in self.nfq.values():
352             q.close()
353
354         logging.debug("Closing socket")
355         self.l2socket.close()
356
357         logging.debug("Stopping inotify watches")
358         self.notifier.stop()
359
360         logging.info("Cleanup finished")
361
362     def _setup_nfqueue(self, queue_num, family, callback):
363         logging.debug("Setting up NFQUEUE for queue %d, AF %s",
364                       queue_num, family)
365         q = nfqueue.queue()
366         q.set_callback(callback)
367         q.fast_open(queue_num, family)
368         q.set_queue_maxlen(5000)
369         # This is mandatory for the queue to operate
370         q.set_mode(nfqueue.NFQNL_COPY_PACKET)
371         self.nfq[q.get_fd()] = q
372
373     def sendp(self, data, iface):
374         """ Send a raw packet using a layer-2 socket
375
376         """
377         if isinstance(data, BasePacket):
378             data = str(data)
379
380         self.l2socket.bind((iface, ETH_P_ALL))
381         count = self.l2socket.send(data)
382         ldata = len(data)
383         if count != ldata:
384             logging.warn("Truncated send on %s (%d/%d bytes sent)",
385                          iface, count, ldata)
386
387     def build_config(self):
388         self.clients.clear()
389         self.subnets.clear()
390
391         for path in glob.glob(os.path.join(self.data_path, "*")):
392             self.add_iface(path)
393
394     def get_ifindex(self, iface):
395         """ Get the interface index from sysfs
396
397         """
398         path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
399         if not path.startswith(SYSFS_NET):
400             return None
401
402         ifindex = None
403
404         try:
405             f = open(path, 'r')
406         except EnvironmentError:
407             logging.debug("%s is probably down, removing", iface)
408             self.remove_iface(iface)
409
410             return ifindex
411
412         try:
413             ifindex = f.readline().strip()
414             try:
415                 ifindex = int(ifindex)
416             except ValueError, e:
417                 logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
418                              " output '%s'", iface, ifindex)
419         except EnvironmentError, e:
420             logging.warn("Error reading %s's ifindex from sysfs: %s",
421                          iface, str(e))
422             self.remove_iface(iface)
423         finally:
424             f.close()
425
426         return ifindex
427
428
429     def get_iface_hw_addr(self, iface):
430         """ Get the interface hardware address from sysfs
431
432         """
433         path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
434         if not path.startswith(SYSFS_NET):
435             return None
436
437         addr = None
438         try:
439             f = open(path, 'r')
440         except EnvironmentError:
441             logging.debug("%s is probably down, removing", iface)
442             self.remove_iface(iface)
443             return addr
444
445         try:
446             addr = f.readline().strip()
447         except EnvironmentError, e:
448             logging.warn("Failed to read hw address for %s from sysfs: %s",
449                          iface, str(e))
450         finally:
451             f.close()
452
453         return addr
454
455     def add_iface(self, path):
456         """ Add an interface to monitor
457
458         """
459         iface = os.path.basename(path)
460
461         logging.debug("Updating configuration for %s", iface)
462         binding = parse_binding_file(path)
463         if binding is None:
464             return
465         ifindex = self.get_ifindex(iface)
466
467         if ifindex is None:
468             logging.warn("Stale configuration for %s found", iface)
469         else:
470             if binding.is_valid():
471                 binding.iface = iface
472                 self.clients[binding.mac] = binding
473                 self.subnets[binding.link] = parse_routing_table(binding.link)
474                 logging.debug("Added client %s on %s", binding.hostname, iface)
475                 self.ifaces[ifindex] = iface
476                 self.v6nets[iface] = parse_routing_table(binding.link, 6)
477
478     def remove_iface(self, iface):
479         """ Cleanup clients on a removed interface
480
481         """
482         if iface in self.v6nets:
483             del self.v6nets[iface]
484
485         for mac in self.clients.keys():
486             if self.clients[mac].iface == iface:
487                 del self.clients[mac]
488
489         for ifindex in self.ifaces.keys():
490             if self.ifaces[ifindex] == iface:
491                 del self.ifaces[ifindex]
492
493         logging.debug("Removed interface %s", iface)
494
495     def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
496         """ Generate a reply to a BOOTP/DHCP request
497
498         """
499         indev = payload.get_indev()
500         try:
501             # Get the actual interface from the ifindex
502             iface = self.ifaces[indev]
503         except KeyError:
504             # We don't know anything about this interface, so accept the packet
505             # and return
506             logging.debug("Ignoring DHCP request on unknown iface %d", indev)
507             # We don't know what to do with this packet, so let the kernel
508             # handle it
509             payload.set_verdict(nfqueue.NF_ACCEPT)
510             return
511
512         # Decode the response - NFQUEUE relays IP packets
513         pkt = IP(payload.get_data())
514
515         # Signal the kernel that it shouldn't further process the packet
516         payload.set_verdict(nfqueue.NF_DROP)
517
518         # Get the client MAC address
519         resp = pkt.getlayer(BOOTP).copy()
520         hlen = resp.hlen
521         mac = resp.chaddr[:hlen].encode("hex")
522         mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
523
524         # Server responses are always BOOTREPLYs
525         resp.op = "BOOTREPLY"
526         del resp.payload
527
528         try:
529             binding = self.clients[mac]
530         except KeyError:
531             logging.warn("Invalid client %s on %s", mac, iface)
532             return
533
534         if iface != binding.iface:
535             logging.warn("Received spoofed DHCP request for %s from interface"
536                          " %s instead of %s", mac, iface, binding.iface)
537             return
538
539         resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
540                IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
541                UDP(sport=pkt.dport, dport=pkt.sport)/resp
542         subnet = self.subnets[binding.link]
543
544         if not DHCP in pkt:
545             logging.warn("Invalid request from %s on %s, no DHCP"
546                          " payload found", binding.mac, iface)
547             return
548
549         dhcp_options = []
550         requested_addr = binding.ip
551         for opt in pkt[DHCP].options:
552             if type(opt) is tuple and opt[0] == "message-type":
553                 req_type = opt[1]
554             if type(opt) is tuple and opt[0] == "requested_addr":
555                 requested_addr = opt[1]
556
557         logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
558                      binding.mac, iface)
559
560         if req_type == DHCPREQUEST and requested_addr != binding.ip:
561             resp_type = DHCPNAK
562             logging.info("Sending DHCPNAK to %s on %s: requested %s"
563                          " instead of %s", binding.mac, iface, requested_addr,
564                          binding.ip)
565
566         elif req_type in (DHCPDISCOVER, DHCPREQUEST):
567             resp_type = DHCP_REQRESP[req_type]
568             resp.yiaddr = self.clients[mac].ip
569             dhcp_options += [
570                  ("hostname", binding.hostname),
571                  ("domain", binding.hostname.split('.', 1)[-1]),
572                  ("router", subnet.gw),
573                  ("broadcast_address", str(subnet.broadcast)),
574                  ("subnet_mask", str(subnet.netmask)),
575                  ("renewal_time", self.lease_renewal),
576                  ("lease_time", self.lease_lifetime),
577             ]
578             dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
579
580         elif req_type == DHCPINFORM:
581             resp_type = DHCP_REQRESP[req_type]
582             dhcp_options += [
583                  ("hostname", binding.hostname),
584                  ("domain", binding.hostname.split('.', 1)[-1]),
585             ]
586             dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
587
588         elif req_type == DHCPRELEASE:
589             # Log and ignore
590             logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
591             return
592
593         # Finally, always add the server identifier and end options
594         dhcp_options += [
595             ("message-type", resp_type),
596             ("server_id", DHCP_DUMMY_SERVER_IP),
597             "end"
598         ]
599         resp /= DHCP(options=dhcp_options)
600
601         logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
602                      binding.ip, iface)
603         self.sendp(resp, iface)
604
605     def rs_response(self, i, payload): # pylint: disable=W0613
606         """ Generate a reply to a BOOTP/DHCP request
607
608         """
609         indev = payload.get_indev()
610         try:
611             # Get the actual interface from the ifindex
612             iface = self.ifaces[indev]
613         except KeyError:
614             logging.debug("Ignoring router solicitation on"
615                           " unknown interface %d", indev)
616             # We don't know what to do with this packet, so let the kernel
617             # handle it
618             payload.set_verdict(nfqueue.NF_ACCEPT)
619             return
620
621         ifmac = self.get_iface_hw_addr(iface)
622         subnet = self.v6nets[iface]
623         ifll = subnet.make_ll64(ifmac)
624
625         # Signal the kernel that it shouldn't further process the packet
626         payload.set_verdict(nfqueue.NF_DROP)
627
628         resp = Ether(src=self.get_iface_hw_addr(iface))/\
629                IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
630                ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
631                                      prefixlen=subnet.prefixlen)
632
633         if self.ipv6_nameservers:
634             resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
635                                      lifetime=self.ra_period * 3)
636
637         logging.info("RA on %s for %s", iface, subnet.net)
638         self.sendp(resp, iface)
639
640     def ns_response(self, i, payload): # pylint: disable=W0613
641         """ Generate a reply to an ICMPv6 neighbor solicitation
642
643         """
644         indev = payload.get_indev()
645         try:
646             # Get the actual interface from the ifindex
647             iface = self.ifaces[indev]
648         except KeyError:
649             logging.debug("Ignoring neighbour solicitation on"
650                           " unknown interface %d", indev)
651             # We don't know what to do with this packet, so let the kernel
652             # handle it
653             payload.set_verdict(nfqueue.NF_ACCEPT)
654             return
655
656         ifmac = self.get_iface_hw_addr(iface)
657         subnet = self.v6nets[iface]
658         ifll = subnet.make_ll64(ifmac)
659
660         ns = IPv6(payload.get_data())
661
662         if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
663             logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
664             payload.set_verdict(nfqueue.NF_ACCEPT)
665             return 1
666
667         payload.set_verdict(nfqueue.NF_DROP)
668
669         try:
670             client_lladdr = ns.lladdr
671         except AttributeError:
672             return 1
673
674         resp = Ether(src=ifmac, dst=client_lladdr)/\
675                IPv6(src=str(ifll), dst=ns.src)/\
676                ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
677                ICMPv6NDOptDstLLAddr(lladdr=ifmac)
678
679         logging.info("NA on %s for %s", iface, ns.tgt)
680         self.sendp(resp, iface)
681         return 1
682
683     def send_periodic_ra(self):
684         # Use a separate thread as this may take a _long_ time with
685         # many interfaces and we want to be responsive in the mean time
686         threading.Thread(target=self._send_periodic_ra).start()
687
688     def _send_periodic_ra(self):
689         logging.debug("Sending out periodic RAs")
690         start = time.time()
691         i = 0
692         for client in self.clients.values():
693             iface = client.iface
694             ifmac = self.get_iface_hw_addr(iface)
695             if not ifmac:
696                 continue
697
698             subnet = self.v6nets[iface]
699             if subnet.net is None:
700                 logging.debug("Skipping periodic RA on interface %s,"
701                               " as it is not IPv6-connected", iface)
702                 continue
703
704             ifll = subnet.make_ll64(ifmac)
705             resp = Ether(src=ifmac)/\
706                    IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
707                    ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
708                                          prefixlen=subnet.prefixlen)
709             if self.ipv6_nameservers:
710                 resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
711                                          lifetime=self.ra_period * 3)
712             try:
713                 self.sendp(resp, iface)
714             except socket.error, e:
715                 logging.warn("Periodic RA on %s failed: %s", iface, str(e))
716             except Exception, e:
717                 logging.warn("Unkown error during periodic RA on %s: %s",
718                              iface, str(e))
719             i += 1
720         logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
721
722     def serve(self):
723         """ Safely perform the main loop, freeing all resources upon exit
724
725         """
726         try:
727             self._serve()
728         finally:
729             self._cleanup()
730
731     def _serve(self):
732         """ Loop forever, serving DHCP requests
733
734         """
735         self.build_config()
736
737         # Yes, we are accessing _fd directly, but it's the only way to have a 
738         # single select() loop ;-)
739         iwfd = self.notifier._fd # pylint: disable=W0212
740
741         start = time.time()
742         if self.ipv6_enabled:
743             timeout = self.ra_period
744             self.send_periodic_ra()
745         else:
746             timeout = None
747
748         while True:
749             rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
750             if xlist:
751                 logging.warn("Warning: Exception on %s",
752                              ", ".join([ str(fd) for fd in xlist]))
753
754             if rlist:
755                 if iwfd in rlist:
756                 # First check if there are any inotify (= configuration change)
757                 # events
758                     self.notifier.read_events()
759                     self.notifier.process_events()
760                     rlist.remove(iwfd)
761
762                 for fd in rlist:
763                     try:
764                         self.nfq[fd].process_pending()
765                     except RuntimeError, e:
766                         logging.warn("Error processing fd %d: %s", fd, str(e))
767                     except Exception, e:
768                         logging.warn("Unknown error processing fd %d: %s",
769                                      fd, str(e))
770
771             if self.ipv6_enabled:
772                 # Calculate the new timeout
773                 timeout = self.ra_period - (time.time() - start)
774
775                 if timeout <= 0:
776                     start = time.time()
777                     self.send_periodic_ra()
778                     timeout = self.ra_period - (time.time() - start)
779
780
781 if __name__ == "__main__":
782     import capng
783     import optparse
784     from cStringIO import StringIO
785     from pwd import getpwnam, getpwuid
786     from configobj import ConfigObj, ConfigObjError, flatten_errors
787
788     import validate
789
790     validator = validate.Validator()
791
792     def is_ip_list(value, family=4):
793         try:
794             family = int(family)
795         except ValueError:
796             raise validate.VdtParamError(family)
797         if isinstance(value, (str, unicode)):
798             value = [value]
799         if not isinstance(value, list):
800             raise validate.VdtTypeError(value)
801
802         for entry in value:
803             try:
804                 ip = IPy.IP(entry)
805             except ValueError:
806                 raise validate.VdtValueError(entry)
807
808             if ip.version() != family:
809                 raise validate.VdtValueError(entry)
810         return value
811
812     validator.functions["ip_addr_list"] = is_ip_list
813     config_spec = StringIO(CONFIG_SPEC)
814
815
816     parser = optparse.OptionParser()
817     parser.add_option("-c", "--config", dest="config_file",
818                       help="The location of the data files", metavar="FILE",
819                       default=DEFAULT_CONFIG)
820     parser.add_option("-d", "--debug", action="store_true", dest="debug",
821                       help="Turn on debugging messages")
822     parser.add_option("-f", "--foreground", action="store_false",
823                       dest="daemonize", default=True,
824                       help="Do not daemonize, stay in the foreground")
825
826
827     opts, args = parser.parse_args()
828
829     if opts.daemonize:
830         d = daemon.DaemonContext()
831         d.umask = 0022
832         d.open()
833
834     try:
835         config = ConfigObj(opts.config_file, configspec=config_spec)
836     except ConfigObjError, err:
837         sys.stderr.write("Failed to parse config file %s: %s" %
838                          (opts.config_file, str(err)))
839         sys.exit(1)
840
841     results = config.validate(validator)
842     if results != True:
843         logging.fatal("Configuration file validation failed! See errors below:")
844         for (section_list, key, unused) in flatten_errors(config, results):
845             if key is not None:
846                 logging.fatal(" '%s' in section '%s' failed validation",
847                               key, ", ".join(section_list))
848             else:
849                 logging.fatal(" Section '%s' is missing",
850                               ", ".join(section_list))
851         sys.exit(1)
852
853     pidfile = open(config["general"]["pidfile"], "w")
854     pidfile.write("%s" % os.getpid())
855     pidfile.close()
856
857     logger = logging.getLogger()
858     if opts.debug:
859         logger.setLevel(logging.DEBUG)
860     else:
861         logger.setLevel(logging.INFO)
862
863     logging.info("Starting up")
864
865     proxy_opts = {}
866     if config["dhcp"].as_bool("enable_dhcp"):
867         proxy_opts.update({
868             "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
869             "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
870             "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
871             "dhcp_server_ip": config["dhcp"]["server_ip"],
872             "dhcp_nameservers": config["dhcp"]["nameservers"],
873         })
874
875     if config["ipv6"].as_bool("enable_ipv6"):
876         proxy_opts.update({
877             "rs_queue_num": config["ipv6"].as_int("rs_queue"),
878             "ns_queue_num": config["ipv6"].as_int("ns_queue"),
879             "ra_period": config["ipv6"].as_int("ra_period"),
880             "ipv6_nameservers": config["ipv6"]["nameservers"],
881         })
882
883     # pylint: disable=W0142
884     proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
885
886     # Drop all capabilities except CAP_NET_RAW and change uid
887     try:
888         uid = getpwuid(config["general"].as_int("user"))
889     except ValueError:
890         uid = getpwnam(config["general"]["user"])
891
892     logging.debug("Setting capabilities and changing uid")
893     logging.debug("User: %s, uid: %d, gid: %d",
894                   config["general"]["user"], uid.pw_uid, uid.pw_gid)
895
896     # Keep only the capabilities we need
897     # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
898     capng.capng_clear(capng.CAPNG_SELECT_BOTH)
899     capng.capng_update(capng.CAPNG_ADD,
900                        capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
901                        capng.CAP_NET_ADMIN)
902     capng.capng_change_id(uid.pw_uid, uid.pw_gid,
903                           capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
904
905     if opts.daemonize:
906         logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
907         handler = logging.handlers.RotatingFileHandler(logfile,
908                                                        maxBytes=2097152)
909     else:
910         handler = logging.StreamHandler()
911
912     handler.setFormatter(logging.Formatter(LOG_FORMAT))
913     logger.addHandler(handler)
914
915     logging.info("Ready to serve requests")
916     try:
917         proxy.serve()
918     except Exception:
919         if opts.daemonize:
920             exc = "".join(traceback.format_exception(*sys.exc_info()))
921             logging.critical(exc)
922         raise
923
924
925 # vim: set ts=4 sts=4 sw=4 et :