nfdhcpd 37.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/env python
#

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

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

import daemon
35
import daemon.runner
36
import daemon.pidlockfile
37
38
import nfqueue
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
48
from scapy.data import ETH_P_ALL
from scapy.packet import BasePacket
49
50
from scapy.layers.l2 import Ether
from scapy.layers.inet import IP, UDP
51
52
53
54
from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
                               ICMPv6NDOptDstLLAddr, \
                               ICMPv6NDOptPrefixInfo, \
                               ICMPv6NDOptRDNSS
55
56
from scapy.layers.dhcp import BOOTP, DHCP

57

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

66
LOG_FILENAME = "nfdhcpd.log"
67
68
69

SYSFS_NET = "/sys/class/net"

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

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

[dhcp]
enable_dhcp = boolean(default=True)
lease_lifetime = integer(min=0, max=4294967295)
lease_renewal = integer(min=0, max=4294967295)
server_ip = ip_addr()
dhcp_queue = integer(min=0, max=65535)
nameservers = ip_addr_list(family=4)
87
domain = string(default=None)
88
89
90
91
92
93
94
95
96
97

[ipv6]
enable_ipv6 = boolean(default=True)
ra_period = integer(min=1, max=4294967295)
rs_queue = integer(min=0, max=65535)
ns_queue = integer(min=0, max=65535)
nameservers = ip_addr_list(family=6)
"""


98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
DHCPDISCOVER = 1
DHCPOFFER = 2
DHCPREQUEST = 3
DHCPDECLINE = 4
DHCPACK = 5
DHCPNAK = 6
DHCPRELEASE = 7
DHCPINFORM = 8

DHCP_TYPES = {
    DHCPDISCOVER: "DHCPDISCOVER",
    DHCPOFFER: "DHCPOFFER",
    DHCPREQUEST: "DHCPREQUEST",
    DHCPDECLINE: "DHCPDECLINE",
    DHCPACK: "DHCPACK",
    DHCPNAK: "DHCPNAK",
    DHCPRELEASE: "DHCPRELEASE",
    DHCPINFORM: "DHCPINFORM",
}

DHCP_REQRESP = {
    DHCPDISCOVER: DHCPOFFER,
    DHCPREQUEST: DHCPACK,
    DHCPINFORM: DHCPACK,
    }

Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
124

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

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

    return indev_ifindex

Christos Stavrakakis's avatar
Christos Stavrakakis committed
142

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

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
154
155
    tap = os.path.basename(path)
    indev = None
156
    mac = None
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
157
    ip = None
158
    hostname = None
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
159
160
161
162
163
164
165
166
167
    subnet = None
    gateway = None
    subnet6 = None
    gateway6 = None
    eui64 = None

    def get_value(line):
        v = line.strip().split('=')[1]
        if v == '':
Christos Stavrakakis's avatar
Christos Stavrakakis committed
168
            return None
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
169
        return v
170
171
172

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

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
200

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

Christos Stavrakakis's avatar
Christos Stavrakakis committed
206
    def process_IN_DELETE(self, event):  # pylint: disable=C0103
207
208
209
210
211
        """ Delete file handler

        Currently this removes an interface from the watch list

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
212
        self.server.remove_tap(event.name)
213

Christos Stavrakakis's avatar
Christos Stavrakakis committed
214
    def process_IN_CLOSE_WRITE(self, event):  # pylint: disable=C0103
215
216
217
218
219
        """ Add file handler

        Currently this adds an interface to the watch list

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
220
        self.server.add_tap(os.path.join(event.path, event.name))
221

222
223

class Client(object):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
224
225
226
227
    def __init__(self, tap=None, indev=None,
                 mac=None, ip=None, hostname=None,
                 subnet=None, gateway=None,
                 subnet6=None, gateway6=None, eui64=None):
228
        self.mac = mac
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
229
        self.ip = ip
230
        self.hostname = hostname
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
231
232
233
234
235
236
237
238
239
        self.indev = indev
        self.tap = tap
        self.subnet = subnet
        self.gateway = gateway
        self.net = Subnet(net=subnet, gw=gateway, dev=tap)
        self.subnet6 = subnet6
        self.gateway6 = gateway6
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
        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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
    def open_socket(self):

        logging.info(" - Opening L2 socket and binding to %s", self.tap)
        try:
            s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
            s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
            s.bind((self.tap, ETH_P_ALL))
            self.socket = s
        except socket.error, e:
            logging.warning(" - Cannot open socket %s", e)


    def sendp(self, data):

        if isinstance(data, BasePacket):
            data = str(data)

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

        try:
            count = self.socket.send(data, socket.MSG_DONTWAIT)
        except socket.error, e:
            logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
            self.socket.close()
            self.open_socket()
            raise e

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


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

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

        """
298
299
300
301
        return str(self.net.netmask())

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

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

Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
307
308
    @property
    def prefix(self):
309
310
311
        """ Return the network as an IPy.IP

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
312
313
314
315
        return self.net.net()

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

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
319
320
321
322
323
324
325
        return self.net.prefixlen()

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

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

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

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
341
342
343
        return self._make_eui64(self.net, mac)

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

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
347
348
        return self._make_eui64("fe80::", mac)

349

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

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

        if ipv6_nameservers is None:
            self.ipv6_nameservers = []
        else:
            self.ipv6_nameservers = ipv6_nameservers

380
        self.ipv6_enabled = False
381

382
        self.clients = {}
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
383
384
385
        #self.subnets = {}
        #self.ifaces = {}
        #self.v6nets = {}
386
387
        self.nfq = {}

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

396
397
        # NFQUEUE setup
        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

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

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

        """
        logging.info("Cleaning up")

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
432
        logging.debug(" - Stopping inotify watches")
433
434
        self.notifier.stop()

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
435
        logging.info(" - Cleanup finished")
436

437
    def _setup_nfqueue(self, queue_num, family, callback, pending):
Christos Stavrakakis's avatar
Christos Stavrakakis committed
438
        logging.info("Setting up NFQUEUE for queue %d, AF %s",
439
                      queue_num, family)
440
441
442
        q = nfqueue.queue()
        q.set_callback(callback)
        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)
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
447
        logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
448
449
450
451

    def build_config(self):
        self.clients.clear()

452
        for path in glob.glob(os.path.join(self.data_path, "*")):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
453
            self.add_tap(path)
454

455
        self.print_clients()
456

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

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
461
        logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
462

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

        ifindex = None

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

475
476
477
478
479
480
481
            return ifindex

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

491
        return ifindex
492

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

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

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

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

518
519
        return addr

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
520
    def add_tap(self, path):
521
522
523
        """ Add an interface to monitor

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
524
        tap = os.path.basename(path)
525

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

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
544
    def remove_tap(self, tap):
545
546
547
        """ Cleanup clients on a removed interface

        """
548
        try:
549
            for k, cl in self.clients.items():
550
                if cl.tap == tap:
551
552
553
                    logging.info("Removing client %s and closing socket on %s",
                                 cl.hostname, cl.tap)
                    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
558
        except:
            logging.debug("Client on %s disappeared!!!", tap)
559

560

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

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

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

        # Server responses are always BOOTREPLYs
        resp.op = "BOOTREPLY"
        del resp.payload

588
589
        indev = get_indev(payload)

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

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

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

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

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


Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
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
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
620
        subnet = binding.net
621
622

        if not DHCP in pkt:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
623
            logging.warn(" - Invalid request from %s on %s, no DHCP"
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
624
                         " payload found", binding.mac, binding.tap)
625
626
627
628
629
630
631
632
633
634
            return

        dhcp_options = []
        requested_addr = binding.ip
        for opt in pkt[DHCP].options:
            if type(opt) is tuple and opt[0] == "message-type":
                req_type = opt[1]
            if type(opt) is tuple and opt[0] == "requested_addr":
                requested_addr = opt[1]

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

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

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

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

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

        elif req_type == DHCPRELEASE:
            # Log and ignore
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
674
675
            logging.info(" - DHCPRELEASE from %s on %s",
                         binding.hostname, binding.tap)
676
677
678
679
680
            return

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

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

697
    def rs_response(self, arg1, arg2=None):  # pylint: disable=W0613
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
698
699
700
        """ Generate a reply to a BOOTP/DHCP request

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

718
719
        indev = get_indev(payload)

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

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

733
        if mac != binding.mac:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
734
            logging.warn(" - Received spoofed RS request: mac %s, tap %s",
Christos Stavrakakis's avatar
Christos Stavrakakis committed
735
                         mac, binding.tap)
736
737
            return

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
738
739
740
        subnet = binding.net6

        if subnet.net is None:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
741
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
Christos Stavrakakis's avatar
Christos Stavrakakis committed
742
            return
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
743

744
745
        indevmac = self.get_iface_hw_addr(binding.indev)
        ifll = subnet.make_ll64(indevmac)
746
747
        if ifll is None:
            return
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
748

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

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

Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
757
758
759
760
        if self.ipv6_nameservers:
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
                                     lifetime=self.ra_period * 3)

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

770
    def ns_response(self, arg1, arg2=None):  # pylint: disable=W0613
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
771
772
773
        """ Generate a reply to an ICMPv6 neighbor solicitation

        """
Christos Stavrakakis's avatar
Christos Stavrakakis committed
774

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

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


794
795
        indev = get_indev(payload)

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

        payload.set_verdict(nfqueue.NF_DROP)

        if mac != binding.mac:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
810
811
            logging.warn(" - Received spoofed NS request"
                         " for mac %s from tap %s", mac, binding.tap)
812
            return
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
813
814
815

        subnet = binding.net6
        if subnet.net is None:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
816
            logging.debug(" - No IPv6 network assigned for the interface")
Christos Stavrakakis's avatar
Christos Stavrakakis committed
817
            return
818

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
819
820
821
        indevmac = self.get_iface_hw_addr(binding.indev)

        ifll = subnet.make_ll64(indevmac)
822
823
        if ifll is None:
            return
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
824
825

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

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

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

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

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

    def _send_periodic_ra(self):
852
        logging.info("Sending out periodic RAs")
853
854
        start = time.time()
        i = 0
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
855
856
857
        for binding in self.clients.values():
            tap = binding.tap
            indev = binding.indev
Christos Stavrakakis's avatar
Christos Stavrakakis committed
858
            # mac = binding.mac
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
859
            subnet = binding.net6
860
            if subnet.net is None:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
861
                logging.debug(" - Skipping periodic RA on interface %s,"
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
862
                              " as it is not IPv6-connected", tap)
863
                continue
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
864
865
            indevmac = self.get_iface_hw_addr(indev)
            ifll = subnet.make_ll64(indevmac)
866
867
            if ifll is None:
                continue
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
868
            resp = Ether(src=indevmac)/\
869
870
871
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                         prefixlen=subnet.prefixlen)
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
872
873
874
            if self.ipv6_nameservers:
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
                                         lifetime=self.ra_period * 3)
875
            try:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
876
                binding.sendp(resp)
877
            except socket.error, e:
878
879
                logging.warn(" - Periodic RA on %s (%s) failed: %s",
                             tap, binding.hostname, str(e))
880
            except Exception, e:
881
882
                logging.warn(" - Unkown error during periodic RA on %s (%s):"
                             " %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
888
889
890
891
892
893
894
895
        """ Safely perform the main loop, freeing all resources upon exit

        """
        try:
            self._serve()
        finally:
            self._cleanup()

    def _serve(self):
896
897
898
899
900
        """ Loop forever, serving DHCP requests

        """
        self.build_config()

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

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

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

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

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
933
934
                logging.debug("Pending requests on fds %s", rlist)

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

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

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

956
    def print_clients(self):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
957
958
        logging.info("%10s   %20s %20s %10s %20s",
                     'Key', 'Client', 'MAC', 'TAP', 'IP')
959
        for k, cl in self.clients.items():
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
960
961
            logging.info("%10s | %20s %20s %10s %20s",
                         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
973
974
975
976
977
978
979

    import validate

    validator = validate.Validator()

    def is_ip_list(value, family=4):
        try:
            family = int(family)
        except ValueError:
980
            raise validate.VdtParamError(family)
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
        if isinstance(value, (str, unicode)):
            value = [value]
        if not isinstance(value, list):
            raise validate.VdtTypeError(value)

        for entry in value:
            try:
                ip = IPy.IP(entry)
            except ValueError:
                raise validate.VdtValueError(entry)

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

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

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

    opts, args = parser.parse_args()