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