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