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