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