Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ fac9f928

History | View | Annotate | Download (37.8 kB)

1
#!/usr/bin/env python
2
#
3

    
4
# nfdcpd: A promiscuous, NFQUEUE-based DHCP server for virtual machine hosting
5
# Copyright (c) 2010 GRNET SA
6
#
7
#    This program is free software; you can redistribute it and/or modify
8
#    it under the terms of the GNU General Public License as published by
9
#    the Free Software Foundation; either version 2 of the License, or
10
#    (at your option) any later version.
11
#
12
#    This program is distributed in the hope that it will be useful,
13
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
#    GNU General Public License for more details.
16
#
17
#    You should have received a copy of the GNU General Public License along
18
#    with this program; if not, write to the Free Software Foundation, Inc.,
19
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
#
21

    
22
import os
23
import signal
24
import errno
25
import re
26
import sys
27
import glob
28
import time
29
import logging
30
import logging.handlers
31
import threading
32
import traceback
33

    
34
import daemon
35
import daemon.runner
36
import daemon.pidlockfile
37
import nfqueue
38
import pyinotify
39
import setproctitle
40
from lockfile import LockTimeout
41

    
42
import IPy
43
import socket
44
import select
45
from socket import AF_INET, AF_INET6
46

    
47
from scapy.data import ETH_P_ALL
48
from scapy.packet import BasePacket
49
from scapy.layers.l2 import Ether
50
from scapy.layers.inet import IP, UDP
51
from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
52
                               ICMPv6NDOptDstLLAddr, \
53
                               ICMPv6NDOptPrefixInfo, \
54
                               ICMPv6NDOptRDNSS
55
from scapy.layers.dhcp import BOOTP, DHCP
56

    
57

    
58
DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
59
DEFAULT_PATH = "/var/run/ganeti-dhcpd"
60
DEFAULT_USER = "nobody"
61
DEFAULT_LEASE_LIFETIME = 604800 # 1 week
62
DEFAULT_LEASE_RENEWAL = 600  # 10 min
63
DEFAULT_RA_PERIOD = 300 # seconds
64
DHCP_DUMMY_SERVER_IP = "1.2.3.4"
65

    
66
LOG_FILENAME = "nfdhcpd.log"
67

    
68
SYSFS_NET = "/sys/class/net"
69

    
70
LOG_FORMAT = "%(asctime)-15s %(levelname)-8s %(message)s"
71

    
72
# Configuration file specification (see configobj documentation)
73
CONFIG_SPEC = """
74
[general]
75
pidfile = string()
76
datapath = string()
77
logdir = string()
78
user = string()
79

    
80
[dhcp]
81
enable_dhcp = boolean(default=True)
82
lease_lifetime = integer(min=0, max=4294967295)
83
lease_renewal = integer(min=0, max=4294967295)
84
server_ip = ip_addr()
85
dhcp_queue = integer(min=0, max=65535)
86
nameservers = ip_addr_list(family=4)
87
domain = string(default=None)
88

    
89
[ipv6]
90
enable_ipv6 = boolean(default=True)
91
ra_period = integer(min=1, max=4294967295)
92
rs_queue = integer(min=0, max=65535)
93
ns_queue = integer(min=0, max=65535)
94
nameservers = ip_addr_list(family=6)
95
"""
96

    
97

    
98
DHCPDISCOVER = 1
99
DHCPOFFER = 2
100
DHCPREQUEST = 3
101
DHCPDECLINE = 4
102
DHCPACK = 5
103
DHCPNAK = 6
104
DHCPRELEASE = 7
105
DHCPINFORM = 8
106

    
107
DHCP_TYPES = {
108
    DHCPDISCOVER: "DHCPDISCOVER",
109
    DHCPOFFER: "DHCPOFFER",
110
    DHCPREQUEST: "DHCPREQUEST",
111
    DHCPDECLINE: "DHCPDECLINE",
112
    DHCPACK: "DHCPACK",
113
    DHCPNAK: "DHCPNAK",
114
    DHCPRELEASE: "DHCPRELEASE",
115
    DHCPINFORM: "DHCPINFORM",
116
}
117

    
118
DHCP_REQRESP = {
119
    DHCPDISCOVER: DHCPOFFER,
120
    DHCPREQUEST: DHCPACK,
121
    DHCPINFORM: DHCPACK,
122
    }
123

    
124

    
125
def get_indev(payload):
126
    try:
127
        indev_ifindex = payload.get_physindev()
128
        if indev_ifindex:
129
            logging.debug(" - Incoming packet from bridge with ifindex %s",
130
                          indev_ifindex)
131
            return indev_ifindex
132
    except AttributeError:
133
        #TODO: return error value
134
        logging.debug("No get_physindev() supported")
135
        return 0
136

    
137
    indev_ifindex = payload.get_indev()
138
    logging.debug(" - Incoming packet from tap with ifindex %s", indev_ifindex)
139

    
140
    return indev_ifindex
141

    
142

    
143
def parse_binding_file(path):
144
    """ Read a client configuration from a tap file
145

    
146
    """
147
    logging.info("Parsing binding file %s", path)
148
    try:
149
        iffile = open(path, 'r')
150
    except EnvironmentError, e:
151
        logging.warn(" - Unable to open binding file %s: %s", path, str(e))
152
        return None
153

    
154
    tap = os.path.basename(path)
155
    indev = None
156
    mac = None
157
    ip = None
158
    hostname = None
159
    subnet = None
160
    gateway = None
161
    subnet6 = None
162
    gateway6 = None
163
    eui64 = None
164

    
165
    def get_value(line):
166
        v = line.strip().split('=')[1]
167
        if v == '':
168
            return None
169
        return v
170

    
171
    for line in iffile:
172
        if line.startswith("IP="):
173
            ip = get_value(line)
174
        elif line.startswith("MAC="):
175
            mac = get_value(line)
176
        elif line.startswith("HOSTNAME="):
177
            hostname = get_value(line)
178
        elif line.startswith("INDEV="):
179
            indev = get_value(line)
180
        elif line.startswith("SUBNET="):
181
            subnet = get_value(line)
182
        elif line.startswith("GATEWAY="):
183
            gateway = get_value(line)
184
        elif line.startswith("SUBNET6="):
185
            subnet6 = get_value(line)
186
        elif line.startswith("GATEWAY6="):
187
            gateway6 = get_value(line)
188
        elif line.startswith("EUI64="):
189
            eui64 = get_value(line)
190

    
191
    try:
192
        return Client(tap=tap, mac=mac, ip=ip, hostname=hostname,
193
                      indev=indev, subnet=subnet, gateway=gateway,
194
                      subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
195
    except ValueError:
196
        logging.warning(" - Cannot add client for host %s and IP %s on tap %s",
197
                        hostname, ip, tap)
198
        return None
199

    
200

    
201
class ClientFileHandler(pyinotify.ProcessEvent):
202
    def __init__(self, server):
203
        pyinotify.ProcessEvent.__init__(self)
204
        self.server = server
205

    
206
    def process_IN_DELETE(self, event):  # pylint: disable=C0103
207
        """ Delete file handler
208

    
209
        Currently this removes an interface from the watch list
210

    
211
        """
212
        self.server.remove_tap(event.name)
213

    
214
    def process_IN_CLOSE_WRITE(self, event):  # pylint: disable=C0103
215
        """ Add file handler
216

    
217
        Currently this adds an interface to the watch list
218

    
219
        """
220
        self.server.add_tap(os.path.join(event.path, event.name))
221

    
222

    
223
class Client(object):
224
    def __init__(self, tap=None, indev=None,
225
                 mac=None, ip=None, hostname=None,
226
                 subnet=None, gateway=None,
227
                 subnet6=None, gateway6=None, eui64=None):
228
        self.mac = mac
229
        self.ip = ip
230
        self.hostname = hostname
231
        self.indev = indev
232
        self.tap = tap
233
        self.subnet = subnet
234
        self.gateway = gateway
235
        self.net = Subnet(net=subnet, gw=gateway, dev=tap)
236
        self.subnet6 = subnet6
237
        self.gateway6 = gateway6
238
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
239
        self.eui64 = eui64
240
        self.open_socket()
241

    
242
    def is_valid(self):
243
        return self.mac is not None and self.hostname is not None
244

    
245

    
246
    def open_socket(self):
247

    
248
        logging.info(" - Opening L2 socket and binding to %s", self.tap)
249
        try:
250
            s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
251
            s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
252
            s.bind((self.tap, ETH_P_ALL))
253
            self.socket = s
254
        except socket.error, e:
255
            logging.warning(" - Cannot open socket %s", e)
256

    
257

    
258
    def sendp(self, data):
259

    
260
        if isinstance(data, BasePacket):
261
            data = str(data)
262

    
263
        logging.debug(" - Sending raw packet %r", data)
264

    
265
        try:
266
            count = self.socket.send(data, socket.MSG_DONTWAIT)
267
        except socket.error, e:
268
            logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
269
            self.socket.close()
270
            self.open_socket()
271
            raise e
272

    
273
        ldata = len(data)
274
        logging.debug(" - Sent %d bytes on %s", count, self.tap)
275
        if count != ldata:
276
            logging.warn(" - Truncated msg: %d/%d bytes sent",
277
                         count, ldata)
278

    
279

    
280
class Subnet(object):
281
    def __init__(self, net=None, gw=None, dev=None):
282
        if isinstance(net, str):
283
            try:
284
                self.net = IPy.IP(net)
285
            except ValueError, e:
286
                logging.warning(" - IPy error: %s", e)
287
                raise e
288
        else:
289
            self.net = net
290
        self.gw = gw
291
        self.dev = dev
292

    
293
    @property
294
    def netmask(self):
295
        """ Return the netmask in textual representation
296

    
297
        """
298
        return str(self.net.netmask())
299

    
300
    @property
301
    def broadcast(self):
302
        """ Return the broadcast address in textual representation
303

    
304
        """
305
        return str(self.net.broadcast())
306

    
307
    @property
308
    def prefix(self):
309
        """ Return the network as an IPy.IP
310

    
311
        """
312
        return self.net.net()
313

    
314
    @property
315
    def prefixlen(self):
316
        """ Return the prefix length as an integer
317

    
318
        """
319
        return self.net.prefixlen()
320

    
321
    @staticmethod
322
    def _make_eui64(net, mac):
323
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
324

    
325
        """
326
        if mac is None:
327
            return None
328
        comp = mac.split(":")
329
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
330
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
331
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
332
        for l in range(0, len(eui64), 2):
333
            prefix += ["".join(eui64[l:l+2])]
334
        return IPy.IP(":".join(prefix))
335

    
336
    def make_eui64(self, mac):
337
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
338
        subnet.
339

    
340
        """
341
        return self._make_eui64(self.net, mac)
342

    
343
    def make_ll64(self, mac):
344
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
345

    
346
        """
347
        return self._make_eui64("fe80::", mac)
348

    
349

    
350
class VMNetProxy(object):  # pylint: disable=R0902
351
    def __init__(self, data_path, dhcp_queue_num=None,  # pylint: disable=R0913
352
                 rs_queue_num=None, ns_queue_num=None,
353
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
354
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
355
                 dhcp_domain='',
356
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
357
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
358

    
359
        try:
360
            getattr(nfqueue.payload, 'get_physindev')
361
            self.mac_indexed_clients = False
362
        except AttributeError:
363
            self.mac_indexed_clients = True
364
        self.data_path = data_path
365
        self.lease_lifetime = dhcp_lease_lifetime
366
        self.lease_renewal = dhcp_lease_renewal
367
        self.dhcp_domain = dhcp_domain
368
        self.dhcp_server_ip = dhcp_server_ip
369
        self.ra_period = ra_period
370
        if dhcp_nameservers is None:
371
            self.dhcp_nameserver = []
372
        else:
373
            self.dhcp_nameservers = dhcp_nameservers
374

    
375
        if ipv6_nameservers is None:
376
            self.ipv6_nameservers = []
377
        else:
378
            self.ipv6_nameservers = ipv6_nameservers
379

    
380
        self.ipv6_enabled = False
381

    
382
        self.clients = {}
383
        #self.subnets = {}
384
        #self.ifaces = {}
385
        #self.v6nets = {}
386
        self.nfq = {}
387

    
388
        # Inotify setup
389
        self.wm = pyinotify.WatchManager()
390
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
391
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
392
        inotify_handler = ClientFileHandler(self)
393
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
394
        self.wm.add_watch(self.data_path, mask, rec=True)
395

    
396
        # NFQUEUE setup
397
        if dhcp_queue_num is not None:
398
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
399

    
400
        if rs_queue_num is not None:
401
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
402
            self.ipv6_enabled = True
403

    
404
        if ns_queue_num is not None:
405
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
406
            self.ipv6_enabled = True
407

    
408
    def get_binding(self, ifindex, mac):
409
        try:
410
            if self.mac_indexed_clients:
411
                logging.debug(" - Getting binding for mac %s", mac)
412
                b = self.clients[mac]
413
            else:
414
                logging.debug(" - Getting binding for ifindex %s", ifindex)
415
                b = self.clients[ifindex]
416
            return b
417
        except KeyError:
418
            logging.debug(" - No client found for mac / ifindex %s / %s",
419
                          mac, ifindex)
420
            return None
421

    
422
    def _cleanup(self):
423
        """ Free all resources for a graceful exit
424

    
425
        """
426
        logging.info("Cleaning up")
427

    
428
        logging.debug(" - Closing netfilter queues")
429
        for q, _ in self.nfq.values():
430
            q.close()
431

    
432
        logging.debug(" - Stopping inotify watches")
433
        self.notifier.stop()
434

    
435
        logging.info(" - Cleanup finished")
436

    
437
    def _setup_nfqueue(self, queue_num, family, callback, pending):
438
        logging.info("Setting up NFQUEUE for queue %d, AF %s",
439
                      queue_num, family)
440
        q = nfqueue.queue()
441
        q.set_callback(callback)
442
        q.fast_open(queue_num, family)
443
        q.set_queue_maxlen(5000)
444
        # This is mandatory for the queue to operate
445
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
446
        self.nfq[q.get_fd()] = (q, pending)
447
        logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
448

    
449
    def build_config(self):
450
        self.clients.clear()
451

    
452
        for path in glob.glob(os.path.join(self.data_path, "*")):
453
            self.add_tap(path)
454

    
455
        self.print_clients()
456

    
457
    def get_ifindex(self, iface):
458
        """ Get the interface index from sysfs
459

    
460
        """
461
        logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
462

    
463
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
464
        if not path.startswith(SYSFS_NET):
465
            return None
466

    
467
        ifindex = None
468

    
469
        try:
470
            f = open(path, 'r')
471
        except EnvironmentError:
472
            logging.debug(" - %s is probably down, removing", iface)
473
            self.remove_tap(iface)
474

    
475
            return ifindex
476

    
477
        try:
478
            ifindex = f.readline().strip()
479
            try:
480
                ifindex = int(ifindex)
481
            except ValueError, e:
482
                logging.warn(" - Failed to get ifindex for %s, cannot parse"
483
                             " sysfs output '%s'", iface, ifindex)
484
        except EnvironmentError, e:
485
            logging.warn(" - Error reading %s's ifindex from sysfs: %s",
486
                         iface, str(e))
487
            self.remove_tap(iface)
488
        finally:
489
            f.close()
490

    
491
        return ifindex
492

    
493
    def get_iface_hw_addr(self, iface):
494
        """ Get the interface hardware address from sysfs
495

    
496
        """
497
        logging.debug(" - Getting mac for iface %s", iface)
498
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
499
        if not path.startswith(SYSFS_NET):
500
            return None
501

    
502
        addr = None
503
        try:
504
            f = open(path, 'r')
505
        except EnvironmentError:
506
            logging.debug(" - %s is probably down, removing", iface)
507
            self.remove_tap(iface)
508
            return addr
509

    
510
        try:
511
            addr = f.readline().strip()
512
        except EnvironmentError, e:
513
            logging.warn(" - Failed to read hw address for %s from sysfs: %s",
514
                         iface, str(e))
515
        finally:
516
            f.close()
517

    
518
        return addr
519

    
520
    def add_tap(self, path):
521
        """ Add an interface to monitor
522

    
523
        """
524
        tap = os.path.basename(path)
525

    
526
        logging.info("Updating configuration for %s", tap)
527
        b = parse_binding_file(path)
528
        if b is None:
529
            return
530
        ifindex = self.get_ifindex(b.tap)
531

    
532
        if ifindex is None:
533
            logging.warn(" - Stale configuration for %s found", tap)
534
        else:
535
            if b.is_valid():
536
                if self.mac_indexed_clients:
537
                    self.clients[b.mac] = b
538
                else:
539
                    self.clients[ifindex] = b
540
                logging.debug(" - Added client:")
541
                logging.debug(" + %5s: %10s %20s %7s %15s",
542
                               ifindex, b.hostname, b.mac, b.tap, b.ip)
543

    
544
    def remove_tap(self, tap):
545
        """ Cleanup clients on a removed interface
546

    
547
        """
548
        try:
549
            for k, cl in self.clients.items():
550
                if cl.tap == tap:
551
                    logging.info("Removing client %s and closing socket on %s",
552
                                 cl.hostname, cl.tap)
553
                    logging.debug(" - %10s | %10s %20s %10s %20s",
554
                                  k, cl.hostname, cl.mac, cl.tap, cl.ip)
555
                    cl.socket.close()
556
                    del self.clients[k]
557
        except:
558
            logging.debug("Client on %s disappeared!!!", tap)
559

    
560

    
561
    def dhcp_response(self, arg1, arg2=None):  # pylint: disable=W0613,R0914
562
        """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
563

    
564
        """
565
        logging.info(" * Processing pending DHCP request")
566
        # Workaround for supporting both squeezy's nfqueue-bindings-python
567
        # and wheezy's python-nfqueue because for some reason the function's
568
        # signature has changed and has broken compatibility
569
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
570
        if arg2:
571
            payload = arg2
572
        else:
573
            payload = arg1
574
        # Decode the response - NFQUEUE relays IP packets
575
        pkt = IP(payload.get_data())
576
        #logging.debug(pkt.show())
577

    
578
        # Get the client MAC address
579
        resp = pkt.getlayer(BOOTP).copy()
580
        hlen = resp.hlen
581
        mac = resp.chaddr[:hlen].encode("hex")
582
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
583

    
584
        # Server responses are always BOOTREPLYs
585
        resp.op = "BOOTREPLY"
586
        del resp.payload
587

    
588
        indev = get_indev(payload)
589

    
590
        binding = self.get_binding(indev, mac)
591
        if binding is None:
592
            # We don't know anything about this interface, so accept the packet
593
            # and return
594
            logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
595
            # We don't know what to do with this packet, so let the kernel
596
            # handle it
597
            payload.set_verdict(nfqueue.NF_ACCEPT)
598
            return
599

    
600
        # Signal the kernel that it shouldn't further process the packet
601
        payload.set_verdict(nfqueue.NF_DROP)
602

    
603
        if mac != binding.mac:
604
            logging.warn(" - Recieved spoofed DHCP request: mac %s, indev %s",
605
                         mac, indev)
606
            return
607

    
608
        if not binding.ip:
609
            logging.info(" - No IP found in binding file.")
610
            return
611

    
612
        logging.info(" - Generating DHCP response:"
613
                     " host %s, mac %s, tap %s, indev %s",
614
                       binding.hostname, mac, binding.tap, indev)
615

    
616

    
617
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
618
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
619
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
620
        subnet = binding.net
621

    
622
        if not DHCP in pkt:
623
            logging.warn(" - Invalid request from %s on %s, no DHCP"
624
                         " payload found", binding.mac, binding.tap)
625
            return
626

    
627
        dhcp_options = []
628
        requested_addr = binding.ip
629
        for opt in pkt[DHCP].options:
630
            if type(opt) is tuple and opt[0] == "message-type":
631
                req_type = opt[1]
632
            if type(opt) is tuple and opt[0] == "requested_addr":
633
                requested_addr = opt[1]
634

    
635
        logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
636
                     binding.mac, binding.tap)
637

    
638
        if self.dhcp_domain:
639
            domainname = self.dhcp_domain
640
        else:
641
            domainname = binding.hostname.split('.', 1)[-1]
642

    
643
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
644
            resp_type = DHCPNAK
645
            logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
646
                         " instead of %s", binding.mac, binding.tap,
647
                         requested_addr, binding.ip)
648

    
649
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
650
            resp_type = DHCP_REQRESP[req_type]
651
            resp.yiaddr = binding.ip
652
            dhcp_options += [
653
                 ("hostname", binding.hostname),
654
                 ("domain", domainname),
655
                 ("broadcast_address", str(subnet.broadcast)),
656
                 ("subnet_mask", str(subnet.netmask)),
657
                 ("renewal_time", self.lease_renewal),
658
                 ("lease_time", self.lease_lifetime),
659
            ]
660
            if subnet.gw:
661
                dhcp_options += [("router", subnet.gw)]
662
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
663

    
664
        elif req_type == DHCPINFORM:
665
            resp_type = DHCP_REQRESP[req_type]
666
            dhcp_options += [
667
                 ("hostname", binding.hostname),
668
                 ("domain", domainname),
669
            ]
670
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
671

    
672
        elif req_type == DHCPRELEASE:
673
            # Log and ignore
674
            logging.info(" - DHCPRELEASE from %s on %s",
675
                         binding.hostname, binding.tap)
676
            return
677

    
678
        # Finally, always add the server identifier and end options
679
        dhcp_options += [
680
            ("message-type", resp_type),
681
            ("server_id", DHCP_DUMMY_SERVER_IP),
682
            "end"
683
        ]
684
        resp /= DHCP(options=dhcp_options)
685

    
686
        logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
687
                     binding.ip, binding.tap)
688
        try:
689
            binding.sendp(resp)
690
        except socket.error, e:
691
            logging.warn(" - DHCP response on %s (%s) failed: %s",
692
                         binding.tap, binding.hostname, str(e))
693
        except Exception, e:
694
            logging.warn(" - Unkown error during DHCP response on %s (%s): %s",
695
                         binding.tap, binding.hostname, str(e))
696

    
697
    def rs_response(self, arg1, arg2=None):  # pylint: disable=W0613
698
        """ Generate a reply to a BOOTP/DHCP request
699

    
700
        """
701
        logging.info(" * Processing pending RS request")
702
        # Workaround for supporting both squeezy's nfqueue-bindings-python
703
        # and wheezy's python-nfqueue because for some reason the function's
704
        # signature has changed and has broken compatibility
705
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
706
        if arg2:
707
            payload = arg2
708
        else:
709
            payload = arg1
710
        pkt = IPv6(payload.get_data())
711
        #logging.debug(pkt.show())
712
        try:
713
            mac = pkt.lladdr
714
        except:
715
            logging.debug(" - Cannot obtain lladdr in rs")
716
            return
717

    
718
        indev = get_indev(payload)
719

    
720
        binding = self.get_binding(indev, mac)
721
        if binding is None:
722
            # We don't know anything about this interface, so accept the packet
723
            # and return
724
            logging.debug(" - Ignoring router solicitation on for mac %s", mac)
725
            # We don't know what to do with this packet, so let the kernel
726
            # handle it
727
            payload.set_verdict(nfqueue.NF_ACCEPT)
728
            return
729

    
730
        # Signal the kernel that it shouldn't further process the packet
731
        payload.set_verdict(nfqueue.NF_DROP)
732

    
733
        if mac != binding.mac:
734
            logging.warn(" - Received spoofed RS request: mac %s, tap %s",
735
                         mac, binding.tap)
736
            return
737

    
738
        subnet = binding.net6
739

    
740
        if subnet.net is None:
741
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
742
            return
743

    
744
        indevmac = self.get_iface_hw_addr(binding.indev)
745
        ifll = subnet.make_ll64(indevmac)
746
        if ifll is None:
747
            return
748

    
749
        logging.info(" - Generating RA for host %s (mac %s) on tap %s",
750
                      binding.hostname, mac, binding.tap)
751

    
752
        resp = Ether(src=indevmac)/\
753
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
754
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
755
                                     prefixlen=subnet.prefixlen)
756

    
757
        if self.ipv6_nameservers:
758
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
759
                                     lifetime=self.ra_period * 3)
760

    
761
        try:
762
            binding.sendp(resp)
763
        except socket.error, e:
764
            logging.warn(" - RA on %s (%s) failed: %s",
765
                         binding.tap, binding.hostname, str(e))
766
        except Exception, e:
767
            logging.warn(" - Unkown error during RA on %s (%s): %s",
768
                         binding.tap, binding.hostname, str(e))
769

    
770
    def ns_response(self, arg1, arg2=None):  # pylint: disable=W0613
771
        """ Generate a reply to an ICMPv6 neighbor solicitation
772

    
773
        """
774

    
775
        logging.info(" * Processing pending NS request")
776
        # Workaround for supporting both squeezy's nfqueue-bindings-python
777
        # and wheezy's python-nfqueue because for some reason the function's
778
        # signature has changed and has broken compatibility
779
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
780
        if arg2:
781
            payload = arg2
782
        else:
783
            payload = arg1
784

    
785
        ns = IPv6(payload.get_data())
786
        #logging.debug(ns.show())
787
        try:
788
            mac = ns.lladdr
789
        except:
790
            logging.debug(" - Cannot obtain lladdr from ns")
791
            return
792

    
793

    
794
        indev = get_indev(payload)
795

    
796
        binding = self.get_binding(indev, mac)
797
        if binding is None:
798
            # We don't know anything about this interface, so accept the packet
799
            # and return
800
            logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
801
                          ns.tgt)
802
            # We don't know what to do with this packet, so let the kernel
803
            # handle it
804
            payload.set_verdict(nfqueue.NF_ACCEPT)
805
            return
806

    
807
        payload.set_verdict(nfqueue.NF_DROP)
808

    
809
        if mac != binding.mac:
810
            logging.warn(" - Received spoofed NS request"
811
                         " for mac %s from tap %s", mac, binding.tap)
812
            return
813

    
814
        subnet = binding.net6
815
        if subnet.net is None:
816
            logging.debug(" - No IPv6 network assigned for the interface")
817
            return
818

    
819
        indevmac = self.get_iface_hw_addr(binding.indev)
820

    
821
        ifll = subnet.make_ll64(indevmac)
822
        if ifll is None:
823
            return
824

    
825
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
826
            logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
827
            return 1
828

    
829
        logging.info(" - Generating NA for host %s (mac %s) on tap %s",
830
                     binding.hostname, mac, binding.tap)
831

    
832
        resp = Ether(src=indevmac, dst=binding.mac)/\
833
               IPv6(src=str(ifll), dst=ns.src)/\
834
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
835
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
836

    
837
        try:
838
            binding.sendp(resp)
839
        except socket.error, e:
840
            logging.warn(" - NA on %s (%s) failed: %s",
841
                         binding.tap, binding.hostname, str(e))
842
        except Exception, e:
843
            logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
844
                         binding.tap, binding.hostname, str(e))
845

    
846
    def send_periodic_ra(self):
847
        # Use a separate thread as this may take a _long_ time with
848
        # many interfaces and we want to be responsive in the mean time
849
        threading.Thread(target=self._send_periodic_ra).start()
850

    
851
    def _send_periodic_ra(self):
852
        logging.info("Sending out periodic RAs")
853
        start = time.time()
854
        i = 0
855
        for binding in self.clients.values():
856
            tap = binding.tap
857
            indev = binding.indev
858
            # mac = binding.mac
859
            subnet = binding.net6
860
            if subnet.net is None:
861
                logging.debug(" - Skipping periodic RA on interface %s,"
862
                              " as it is not IPv6-connected", tap)
863
                continue
864
            indevmac = self.get_iface_hw_addr(indev)
865
            ifll = subnet.make_ll64(indevmac)
866
            if ifll is None:
867
                continue
868
            resp = Ether(src=indevmac)/\
869
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
870
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
871
                                         prefixlen=subnet.prefixlen)
872
            if self.ipv6_nameservers:
873
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
874
                                         lifetime=self.ra_period * 3)
875
            try:
876
                binding.sendp(resp)
877
            except socket.error, e:
878
                logging.warn(" - Periodic RA on %s (%s) failed: %s",
879
                             tap, binding.hostname, str(e))
880
            except Exception, e:
881
                logging.warn(" - Unkown error during periodic RA on %s (%s):"
882
                             " %s", tap, binding.hostname, str(e))
883
            i += 1
884
        logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
885

    
886
    def serve(self):
887
        """ Safely perform the main loop, freeing all resources upon exit
888

    
889
        """
890
        try:
891
            self._serve()
892
        finally:
893
            self._cleanup()
894

    
895
    def _serve(self):
896
        """ Loop forever, serving DHCP requests
897

    
898
        """
899
        self.build_config()
900

    
901
        # Yes, we are accessing _fd directly, but it's the only way to have a
902
        # single select() loop ;-)
903
        iwfd = self.notifier._fd  # pylint: disable=W0212
904

    
905
        start = time.time()
906
        if self.ipv6_enabled:
907
            timeout = self.ra_period
908
            self.send_periodic_ra()
909
        else:
910
            timeout = None
911

    
912
        while True:
913
            try:
914
                rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
915
                                                [], [], timeout)
916
            except select.error, e:
917
                if e[0] == errno.EINTR:
918
                    logging.debug("select() got interrupted")
919
                    continue
920

    
921
            if xlist:
922
                logging.warn("Warning: Exception on %s",
923
                             ", ".join([str(fd) for fd in xlist]))
924

    
925
            if rlist:
926
                if iwfd in rlist:
927
                # First check if there are any inotify (= configuration change)
928
                # events
929
                    self.notifier.read_events()
930
                    self.notifier.process_events()
931
                    rlist.remove(iwfd)
932

    
933
                logging.debug("Pending requests on fds %s", rlist)
934

    
935
                for fd in rlist:
936
                    try:
937
                        q, num = self.nfq[fd]
938
                        cnt = q.process_pending(num)
939
                        logging.debug(" * Processed %d requests on NFQUEUE"
940
                                      " with fd %d", cnt, fd)
941
                    except RuntimeError, e:
942
                        logging.warn("Error processing fd %d: %s", fd, str(e))
943
                    except Exception, e:
944
                        logging.warn("Unknown error processing fd %d: %s",
945
                                     fd, str(e))
946

    
947
            if self.ipv6_enabled:
948
                # Calculate the new timeout
949
                timeout = self.ra_period - (time.time() - start)
950

    
951
                if timeout <= 0:
952
                    start = time.time()
953
                    self.send_periodic_ra()
954
                    timeout = self.ra_period - (time.time() - start)
955

    
956
    def print_clients(self):
957
        logging.info("%10s   %20s %20s %10s %20s",
958
                     'Key', 'Client', 'MAC', 'TAP', 'IP')
959
        for k, cl in self.clients.items():
960
            logging.info("%10s | %20s %20s %10s %20s",
961
                         k, cl.hostname, cl.mac, cl.tap, cl.ip)
962

    
963

    
964

    
965
if __name__ == "__main__":
966
    import capng
967
    import optparse
968
    from cStringIO import StringIO
969
    from pwd import getpwnam, getpwuid
970
    from configobj import ConfigObj, ConfigObjError, flatten_errors
971

    
972
    import validate
973

    
974
    validator = validate.Validator()
975

    
976
    def is_ip_list(value, family=4):
977
        try:
978
            family = int(family)
979
        except ValueError:
980
            raise validate.VdtParamError(family)
981
        if isinstance(value, (str, unicode)):
982
            value = [value]
983
        if not isinstance(value, list):
984
            raise validate.VdtTypeError(value)
985

    
986
        for entry in value:
987
            try:
988
                ip = IPy.IP(entry)
989
            except ValueError:
990
                raise validate.VdtValueError(entry)
991

    
992
            if ip.version() != family:
993
                raise validate.VdtValueError(entry)
994
        return value
995

    
996
    validator.functions["ip_addr_list"] = is_ip_list
997
    config_spec = StringIO(CONFIG_SPEC)
998

    
999
    parser = optparse.OptionParser()
1000
    parser.add_option("-c", "--config", dest="config_file",
1001
                      help="The location of the data files", metavar="FILE",
1002
                      default=DEFAULT_CONFIG)
1003
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
1004
                      help="Turn on debugging messages")
1005
    parser.add_option("-f", "--foreground", action="store_false",
1006
                      dest="daemonize", default=True,
1007
                      help="Do not daemonize, stay in the foreground")
1008

    
1009
    opts, args = parser.parse_args()
1010

    
1011
    try:
1012
        config = ConfigObj(opts.config_file, configspec=config_spec)
1013
    except ConfigObjError, err:
1014
        sys.stderr.write("Failed to parse config file %s: %s" %
1015
                         (opts.config_file, str(err)))
1016
        sys.exit(1)
1017

    
1018
    results = config.validate(validator)
1019
    if results != True:
1020
        logging.fatal("Configuration file validation failed! See errors below:")
1021
        for (section_list, key, unused) in flatten_errors(config, results):
1022
            if key is not None:
1023
                logging.fatal(" '%s' in section '%s' failed validation",
1024
                              key, ", ".join(section_list))
1025
            else:
1026
                logging.fatal(" Section '%s' is missing",
1027
                              ", ".join(section_list))
1028
        sys.exit(1)
1029

    
1030
    try:
1031
        uid = getpwuid(config["general"].as_int("user"))
1032
    except ValueError:
1033
        uid = getpwnam(config["general"]["user"])
1034

    
1035
    # Keep only the capabilities we need
1036
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1037
    # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1038
    # CAP_SETPCAP: needed by capng_change_id()
1039
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1040
    capng.capng_update(capng.CAPNG_ADD,
1041
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1042
                       capng.CAP_NET_ADMIN)
1043
    capng.capng_update(capng.CAPNG_ADD,
1044
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1045
                       capng.CAP_NET_RAW)
1046
    capng.capng_update(capng.CAPNG_ADD,
1047
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1048
                       capng.CAP_SETPCAP)
1049
    # change uid
1050
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1051
                          capng.CAPNG_DROP_SUPP_GRP | \
1052
                          capng.CAPNG_CLEAR_BOUNDING)
1053

    
1054
    logger = logging.getLogger()
1055
    if opts.debug:
1056
        logger.setLevel(logging.DEBUG)
1057
    else:
1058
        logger.setLevel(logging.INFO)
1059

    
1060
    if opts.daemonize:
1061
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1062
        handler = logging.handlers.WatchedFileHandler(logfile)
1063
    else:
1064
        handler = logging.StreamHandler()
1065

    
1066
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
1067
    logger.addHandler(handler)
1068

    
1069
    # Rename this process so 'ps' output looks like
1070
    # this is a native executable.
1071
    # NOTE: due to a bug in python-setproctitle, one cannot yet
1072
    # set individual values for command-line arguments, so only show
1073
    # the name of the executable instead.
1074
    # setproctitle.setproctitle("\x00".join(sys.argv))
1075
    setproctitle.setproctitle(sys.argv[0])
1076

    
1077
    if opts.daemonize:
1078
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1079
            config["general"]["pidfile"], 10)
1080
        # Remove any stale PID files, left behind by previous invocations
1081
        if daemon.runner.is_pidfile_stale(pidfile):
1082
            logger.warning("Removing stale PID lock file %s", pidfile.path)
1083
            pidfile.break_lock()
1084

    
1085
        d = daemon.DaemonContext(pidfile=pidfile,
1086
                                 umask=0022,
1087
                                 stdout=handler.stream,
1088
                                 stderr=handler.stream,
1089
                                 files_preserve=[handler.stream])
1090
        try:
1091
            d.open()
1092
        except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1093
            logger.critical("Failed to lock pidfile %s,"
1094
                            " another instance running?", pidfile.path)
1095
            sys.exit(1)
1096

    
1097
    logging.info("Starting up")
1098
    logging.info("Running as %s (uid:%d, gid: %d)",
1099
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
1100

    
1101
    proxy_opts = {}
1102
    if config["dhcp"].as_bool("enable_dhcp"):
1103
        proxy_opts.update({
1104
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1105
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1106
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1107
            "dhcp_server_ip": config["dhcp"]["server_ip"],
1108
            "dhcp_nameservers": config["dhcp"]["nameservers"],
1109
            "dhcp_domain": config["dhcp"]["domain"],
1110
        })
1111

    
1112
    if config["ipv6"].as_bool("enable_ipv6"):
1113
        proxy_opts.update({
1114
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1115
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1116
            "ra_period": config["ipv6"].as_int("ra_period"),
1117
            "ipv6_nameservers": config["ipv6"]["nameservers"],
1118
        })
1119

    
1120
    # pylint: disable=W0142
1121
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1122

    
1123
    logging.info("Ready to serve requests")
1124

    
1125

    
1126
    def debug_handler(signum, _):
1127
        logging.debug('Received signal %d. Printing proxy state...', signum)
1128
        proxy.print_clients()
1129

    
1130
    # Set the signal handler for debuging clients
1131
    signal.signal(signal.SIGUSR1, debug_handler)
1132
    signal.siginterrupt(signal.SIGUSR1, False)
1133

    
1134
    try:
1135
        proxy.serve()
1136
    except Exception:
1137
        if opts.daemonize:
1138
            exc = "".join(traceback.format_exception(*sys.exc_info()))
1139
            logging.critical(exc)
1140
        raise
1141

    
1142

    
1143
# vim: set ts=4 sts=4 sw=4 et :