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