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