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