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