nfdhcpd 36.7 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
35
import subprocess

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

import IPy
44
import socket
45
import select
46
from socket import AF_INET, AF_INET6
47

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

58

59
DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
60
61
DEFAULT_PATH = "/var/run/ganeti-dhcpd"
DEFAULT_USER = "nobody"
62
63
64
65
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"
66

67
LOG_FILENAME = "nfdhcpd.log"
68
69
70

SYSFS_NET = "/sys/class/net"

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

73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# 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)
88
domain = string(default=None)
89
90
91
92
93
94
95
96
97
98

[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)
"""


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
124
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
125

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

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

    return indev_ifindex

Christos Stavrakakis's avatar
Christos Stavrakakis committed
143

144
145
146
def get_binding(proxy, ifindex, mac):
    try:
        if proxy.mac_indexed_clients:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
147
            logging.debug(" - Getting binding for mac %s", mac)
148
149
            b = proxy.clients[mac]
        else:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
150
            logging.debug(" - Getting binding for ifindex %s", ifindex)
151
152
153
            b = proxy.clients[ifindex]
        return b
    except KeyError:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
154
155
        logging.debug(" - No client found for mac / ifindex %s / %s",
                      mac, ifindex)
156
157
        return None

Christos Stavrakakis's avatar
Christos Stavrakakis committed
158

159
160
161
162
def parse_binding_file(path):
    """ Read a client configuration from a tap file

    """
Christos Stavrakakis's avatar
Christos Stavrakakis committed
163
    logging.info("Parsing binding file %s", path)
164
165
166
    try:
        iffile = open(path, 'r')
    except EnvironmentError, e:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
167
        logging.warn(" - Unable to open binding file %s: %s", path, str(e))
168
        return None
169

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
170
171
    tap = os.path.basename(path)
    indev = None
172
    mac = None
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
173
    ip = None
174
    hostname = None
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
175
176
177
178
179
180
181
182
183
    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
184
            return None
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
185
        return v
186
187
188

    for line in iffile:
        if line.startswith("IP="):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
189
            ip = get_value(line)
190
        elif line.startswith("MAC="):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
191
            mac = get_value(line)
192
        elif line.startswith("HOSTNAME="):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
193
194
195
196
197
198
199
200
201
202
203
204
205
206
            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)

207
    try:
208
209
210
211
        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
212
        logging.warning(" - Cannot add client for host %s and IP %s on tap %s",
213
                        hostname, ip, tap)
214
        return None
215

Christos Stavrakakis's avatar
Christos Stavrakakis committed
216

217
218
class ClientFileHandler(pyinotify.ProcessEvent):
    def __init__(self, server):
219
        pyinotify.ProcessEvent.__init__(self)
220
        self.server = server
221

Christos Stavrakakis's avatar
Christos Stavrakakis committed
222
    def process_IN_DELETE(self, event):  # pylint: disable=C0103
223
224
225
226
227
        """ Delete file handler

        Currently this removes an interface from the watch list

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
228
        self.server.remove_tap(event.name)
229

Christos Stavrakakis's avatar
Christos Stavrakakis committed
230
    def process_IN_CLOSE_WRITE(self, event):  # pylint: disable=C0103
231
232
233
234
235
        """ Add file handler

        Currently this adds an interface to the watch list

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

238
239

class Client(object):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
240
241
    def __init__(self, tap=None, indev=None, mac=None, ip=None, hostname=None,
                 subnet=None, gateway=None, subnet6=None, gateway6=None, eui64=None ):
242
        self.mac = mac
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
243
        self.ip = ip
244
        self.hostname = hostname
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
245
246
247
248
249
250
251
252
253
        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
254
        self.open_socket()
255
256

    def is_valid(self):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
257
        return self.mac is not None and self.ip is not None\
258
259
260
               and self.hostname is not None


261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
    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)


295
296
297
class Subnet(object):
    def __init__(self, net=None, gw=None, dev=None):
        if isinstance(net, str):
298
299
            try:
                self.net = IPy.IP(net)
300
            except ValueError, e:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
301
                logging.warning(" - IPy error: %s", e)
302
                raise e
303
304
305
306
307
308
309
        else:
            self.net = net
        self.gw = gw
        self.dev = dev

    @property
    def netmask(self):
310
311
312
        """ Return the netmask in textual representation

        """
313
314
315
316
        return str(self.net.netmask())

    @property
    def broadcast(self):
317
318
319
        """ Return the broadcast address in textual representation

        """
320
321
        return str(self.net.broadcast())

Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
322
323
    @property
    def prefix(self):
324
325
326
        """ Return the network as an IPy.IP

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
327
328
329
330
        return self.net.net()

    @property
    def prefixlen(self):
331
332
333
        """ Return the prefix length as an integer

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
334
335
336
337
338
339
340
        return self.net.prefixlen()

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

        """
341
342
        if mac is None:
            return None
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
343
344
345
346
347
348
349
350
351
        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):
352
353
354
355
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
        subnet.

        """
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
356
357
358
        return self._make_eui64(self.net, mac)

    def make_ll64(self, mac):
359
360
361
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address

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

364

Christos Stavrakakis's avatar
Christos Stavrakakis committed
365
366
class VMNetProxy(object):  # pylint: disable=R0902
    def __init__(self, data_path, dhcp_queue_num=None,  # pylint: disable=R0913
367
368
369
                 rs_queue_num=None, ns_queue_num=None,
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
370
                 dhcp_domain='',
371
372
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
373

374
375
376
377
378
        try:
            getattr(nfqueue.payload, 'get_physindev')
            self.mac_indexed_clients = False
        except AttributeError:
            self.mac_indexed_clients = True
379
        self.data_path = data_path
380
381
        self.lease_lifetime = dhcp_lease_lifetime
        self.lease_renewal = dhcp_lease_renewal
382
        self.dhcp_domain = dhcp_domain
383
384
        self.dhcp_server_ip = dhcp_server_ip
        self.ra_period = ra_period
385
386
387
388
389
390
391
392
393
394
        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

395
        self.ipv6_enabled = False
396

397
        self.clients = {}
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
398
399
400
        #self.subnets = {}
        #self.ifaces = {}
        #self.v6nets = {}
401
402
        self.nfq = {}

403
404
405
406
        # Inotify setup
        self.wm = pyinotify.WatchManager()
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
407
408
        inotify_handler = ClientFileHandler(self)
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
409
410
        self.wm.add_watch(self.data_path, mask, rec=True)

411
412
        # NFQUEUE setup
        if dhcp_queue_num is not None:
413
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
414
415

        if rs_queue_num is not None:
416
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
417
            self.ipv6_enabled = True
418
419

        if ns_queue_num is not None:
420
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
421
            self.ipv6_enabled = True
422

423

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

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

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

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
437
        logging.info(" - Cleanup finished")
438

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

451
    def sendp(self, data, binding):
452
453
454
        """ Send a raw packet using a layer-2 socket

        """
455
456
457
        logging.info(" - Sending raw packet on %s (%s)",
                     binding.tap, binding.hostname)
        binding.sendp(data)
458
459


460
461
462
    def build_config(self):
        self.clients.clear()

463
        for path in glob.glob(os.path.join(self.data_path, "*")):
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
464
            self.add_tap(path)
465

466
        self.print_clients()
467

468
469
470
471
    def get_ifindex(self, iface):
        """ Get the interface index from sysfs

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

474
475
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
        if not path.startswith(SYSFS_NET):
476
477
478
479
480
            return None

        ifindex = None

        try:
481
            f = open(path, 'r')
482
        except EnvironmentError:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
483
            logging.debug(" - %s is probably down, removing", iface)
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
484
            self.remove_tap(iface)
485

486
487
488
489
490
491
492
            return ifindex

        try:
            ifindex = f.readline().strip()
            try:
                ifindex = int(ifindex)
            except ValueError, e:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
493
494
                logging.warn(" - Failed to get ifindex for %s, cannot parse"
                             " sysfs output '%s'", iface, ifindex)
495
        except EnvironmentError, e:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
496
            logging.warn(" - Error reading %s's ifindex from sysfs: %s",
497
                         iface, str(e))
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
498
            self.remove_tap(iface)
499
500
501
        finally:
            f.close()

502
        return ifindex
503

504
505
506
507
    def get_iface_hw_addr(self, iface):
        """ Get the interface hardware address from sysfs

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
508
        logging.debug(" - Getting mac for iface %s", iface)
509
510
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
        if not path.startswith(SYSFS_NET):
511
512
513
514
            return None

        addr = None
        try:
515
            f = open(path, 'r')
516
        except EnvironmentError:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
517
            logging.debug(" - %s is probably down, removing", iface)
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
518
            self.remove_tap(iface)
519
520
521
            return addr

        try:
522
            addr = f.readline().strip()
523
        except EnvironmentError, e:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
524
            logging.warn(" - Failed to read hw address for %s from sysfs: %s",
525
                         iface, str(e))
526
        finally:
527
            f.close()
528

529
530
        return addr

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
531
    def add_tap(self, path):
532
533
534
        """ Add an interface to monitor

        """
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
535
        tap = os.path.basename(path)
536

537
        logging.info("Updating configuration for %s", tap)
538
539
        b = parse_binding_file(path)
        if b is None:
540
            return
541
        ifindex = self.get_ifindex(b.tap)
542
543

        if ifindex is None:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
544
            logging.warn(" - Stale configuration for %s found", tap)
545
        else:
546
            if b.is_valid():
547
548
549
550
                if self.mac_indexed_clients:
                    self.clients[b.mac] = b
                else:
                    self.clients[ifindex] = b
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
551
552
                logging.debug(" - Added client:")
                logging.debug(" + %5s: %10s %20s %7s %15s",
553
                               ifindex, b.hostname, b.mac, b.tap, b.ip)
554

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
555
    def remove_tap(self, tap):
556
557
558
        """ Cleanup clients on a removed interface

        """
559
        try:
560
            for k, cl in self.clients.items():
561
                if cl.tap == tap:
562
563
564
                    logging.info("Removing client %s and closing socket on %s",
                                 cl.hostname, cl.tap)
                    logging.debug(" - %10s | %10s %20s %10s %20s",
565
                                  k, cl.hostname, cl.mac, cl.tap, cl.ip)
566
                    cl.socket.close()
567
                    del self.clients[k]
568
569
        except:
            logging.debug("Client on %s disappeared!!!", tap)
570

571

572
    def dhcp_response(self, dummy, payload):  # pylint: disable=W0613,R0914
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
573
        """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
574
575

        """
576
        logging.info(" * Processing pending DHCP request")
577
578
        # Decode the response - NFQUEUE relays IP packets
        pkt = IP(payload.get_data())
579
        #logging.debug(pkt.show())
580

581
582
583
584
        # 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
585
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
586
587
588
589
590

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

591
592
        indev = get_indev(payload)

593
        binding = get_binding(self, indev, mac)
594
595
596
        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
597
            logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
598
599
            # We don't know what to do with this packet, so let the kernel
            # handle it
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
600
            payload.set_verdict(nfqueue.NF_ACCEPT)
601
602
            return

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

606
        if mac != binding.mac:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
607
            logging.warn(" - Recieved spoofed DHCP request for mac %s from tap %s", mac, indev)
608
609
            return

610
        logging.info(" - Generating DHCP response for host %s (mac %s) on tap %s",
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
611
612
613
                       binding.hostname, mac, binding.tap)


Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
614
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
615
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
616
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
617
        subnet = binding.net
618
619

        if not DHCP in pkt:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
620
            logging.warn(" - Invalid request from %s on %s, no DHCP"
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
621
                         " payload found", binding.mac, binding.tap)
622
623
624
625
626
627
628
629
630
631
            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
632
        logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
633
                     binding.mac, binding.tap)
634

635
636
637
638
639
        if self.dhcp_domain:
            domainname = self.dhcp_domain
        else:
            domainname = binding.hostname.split('.', 1)[-1]

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

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

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

        elif req_type == DHCPRELEASE:
            # Log and ignore
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
671
            logging.info(" - DHCPRELEASE from %s on %s", binding.mac, binding.tap)
672
673
674
675
676
            return

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

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

693
    def rs_response(self, dummy, payload):  # pylint: disable=W0613
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
694
695
696
        """ Generate a reply to a BOOTP/DHCP request

        """
697
        logging.info(" * Processing pending RS request")
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
698
        pkt = IPv6(payload.get_data())
699
        #logging.debug(pkt.show())
700
701
702
        try:
            mac = pkt.lladdr
        except:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
703
            logging.debug(" - Cannot obtain lladdr in rs")
704
705
            return

706
707
708
709
710
711
        indev = get_indev(payload)

        binding = get_binding(self, indev, mac)
        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
712
            logging.debug(" - Ignoring router solicitation on for mac %s", mac)
713
714
715
716
717
            # 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
718
719
720
        # Signal the kernel that it shouldn't further process the packet
        payload.set_verdict(nfqueue.NF_DROP)

721
        if mac != binding.mac:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
722
            logging.warn(" - Received spoofed RS request for mac %s from tap %s",
Christos Stavrakakis's avatar
Christos Stavrakakis committed
723
                         mac, binding.tap)
724
725
            return

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
726
727
728
        subnet = binding.net6

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

732
733
        indevmac = self.get_iface_hw_addr(binding.indev)
        ifll = subnet.make_ll64(indevmac)
734
735
        if ifll is None:
            return
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
736

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

740
        resp = Ether(src=indevmac)/\
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
741
742
743
744
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                     prefixlen=subnet.prefixlen)

Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
745
746
747
748
        if self.ipv6_nameservers:
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
                                     lifetime=self.ra_period * 3)

749
        try:
750
            self.sendp(resp, binding)
751
        except socket.error, e:
752
753
            logging.warn(" - RA on %s (%s) failed: %s",
                         binding.tap, binding.hostname, str(e))
754
        except Exception, e:
755
756
            logging.warn(" - Unkown error during RA on %s (%s): %s",
                         binding.tap, binding.hostname, str(e))
757

758
    def ns_response(self, dummy, payload):  # pylint: disable=W0613
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
759
760
761
        """ Generate a reply to an ICMPv6 neighbor solicitation

        """
Christos Stavrakakis's avatar
Christos Stavrakakis committed
762

763
        logging.info(" * Processing pending NS request")
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
764

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
765
        ns = IPv6(payload.get_data())
766
767
768
769
        #logging.debug(ns.show())
        try:
            mac = ns.lladdr
        except:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
770
            logging.debug(" - Cannot obtain lladdr from ns")
771
            return
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
772
773


774
775
776
777
778
779
        indev = get_indev(payload)

        binding = get_binding(self, indev, mac)
        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
780
781
            logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
                          ns.tgt)
782
783
784
785
786
787
788
789
            # 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
790
791
            logging.warn(" - Received spoofed NS request"
                         " for mac %s from tap %s", mac, binding.tap)
792
            return
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
793
794
795

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
799
800
801
        indevmac = self.get_iface_hw_addr(binding.indev)

        ifll = subnet.make_ll64(indevmac)
802
803
        if ifll is None:
            return
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
804
805

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

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

Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
812
        resp = Ether(src=indevmac, dst=binding.mac)/\
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
813
814
               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
815
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
816

817
        try:
818
            self.sendp(resp, binding)
819
        except socket.error, e:
820
821
            logging.warn(" - NA on %s (%s) failed: %s",
                         bindig.tap, binding.hostname, str(e))
822
        except Exception, e:
823
824
            logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
                         binding.tap, binding.hostname, str(e))
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
825

826
    def send_periodic_ra(self):
827
828
829
830
831
        # 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):
832
        logging.info("Sending out periodic RAs")
833
834
        start = time.time()
        i = 0
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
835
836
837
        for binding in self.clients.values():
            tap = binding.tap
            indev = binding.indev
Christos Stavrakakis's avatar
Christos Stavrakakis committed
838
            # mac = binding.mac
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
839
            subnet = binding.net6
840
            if subnet.net is None:
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
841
                logging.debug(" - Skipping periodic RA on interface %s,"
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
842
                              " as it is not IPv6-connected", tap)
843
                continue
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
844
845
            indevmac = self.get_iface_hw_addr(indev)
            ifll = subnet.make_ll64(indevmac)
846
847
            if ifll is None:
                continue
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
848
            resp = Ether(src=indevmac)/\
849
850
851
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
                                         prefixlen=subnet.prefixlen)
Apollon Oikonomopoulos's avatar
Apollon Oikonomopoulos committed
852
853
854
            if self.ipv6_nameservers:
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
                                         lifetime=self.ra_period * 3)
855
            try:
856
                self.sendp(resp, binding)
857
            except socket.error, e:
858
859
                logging.warn(" - Periodic RA on %s (%s) failed: %s",
                             tap, binding.hostname, str(e))
860
            except Exception, e:
861
862
                logging.warn(" - Unkown error during periodic RA on %s (%s):"
                             " %s", tap, binding.hostname, str(e))
863
            i += 1
864
        logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
865

866
    def serve(self):
867
868
869
870
871
872
873
874
875
        """ Safely perform the main loop, freeing all resources upon exit

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

    def _serve(self):
876
877
878
879
880
        """ Loop forever, serving DHCP requests

        """
        self.build_config()

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

885
        start = time.time()
886
887
888
889
890
        if self.ipv6_enabled:
            timeout = self.ra_period
            self.send_periodic_ra()
        else:
            timeout = None
891

892
        while True:
893
894
895
896
897
898
899
            try:
                rlist, _, xlist = select.select(self.nfq.keys() + [iwfd], [], [], timeout)
            except select.error, e:
                if e[0] == errno.EINTR:
                    logging.debug("select() got interrupted")
                    continue

900
            if xlist:
901
                logging.warn("Warning: Exception on %s",
Christos Stavrakakis's avatar
Christos Stavrakakis committed
902
                             ", ".join([str(fd) for fd in xlist]))
903

904
            if rlist:
905
                if iwfd in rlist:
906
907
                # First check if there are any inotify (= configuration change)
                # events
908
909
910
                    self.notifier.read_events()
                    self.notifier.process_events()
                    rlist.remove(iwfd)
911

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

914
                for fd in rlist:
915
                    try:
916
917
                        q, num = self.nfq[fd]
                        cnt = q.process_pending(num)
Dimitris Aragiorgis's avatar
Dimitris Aragiorgis committed
918
919
                        logging.debug(" * Processed %d requests on NFQUEUE"
                                      " with fd %d", cnt, fd)
920
921
                    except RuntimeError, e:
                        logging.warn("Error processing fd %d: %s", fd, str(e))
922
                    except Exception, e:
923
924
                        logging.warn("Unknown error processing fd %d: %s",
                                     fd, str(e))
925

926
927
            if self.ipv6_enabled:
                # Calculate the new timeout
928
                timeout = self.ra_period - (time.time() - start)
929

930
931
932
933
                if timeout <= 0:
                    start = time.time()
                    self.send_periodic_ra()
                    timeout = self.ra_period - (time.time() - start)
934

935
936
937
938
939
940
    def print_clients(self):
        logging.info("%10s   %20s %20s %10s %20s",'Key', 'Client', 'MAC', 'TAP', 'IP')
        for k, cl in self.clients.items():
            logging.info("%10s | %20s %20s %10s %20s", k, cl.hostname, cl.mac, cl.tap, cl.ip)


941

942
if __name__ == "__main__":
943
    import capng
944
    import optparse
945
    from cStringIO import StringIO
946
    from pwd import getpwnam, getpwuid
947
    from configobj import ConfigObj, ConfigObjError, flatten_errors
948
949
950
951
952
953
954
955
956

    import validate

    validator = validate.Validator()

    def is_ip_list(value, family=4):
        try:
            family = int(family)
        except ValueError:
957
            raise validate