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