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