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