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