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