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