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