Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 98804a5e

History | View | Annotate | Download (41.6 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
from scapy.layers.dhcp6 import DHCP6_Reply, DHCP6OptDNSServers, \
57
                               DHCP6OptServerId, DHCP6OptClientId, \
58
                               DUID_LLT, DHCP6_InfoRequest, DHCP6OptDNSDomains
59

    
60

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

    
69
LOG_FILENAME = "nfdhcpd.log"
70

    
71
SYSFS_NET = "/sys/class/net"
72

    
73
LOG_FORMAT = "%(asctime)-15s %(levelname)-8s %(message)s"
74

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

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

    
92
[ipv6]
93
enable_ipv6 = boolean(default=True)
94
ra_period = integer(min=1, max=4294967295)
95
rs_queue = integer(min=0, max=65535)
96
ns_queue = integer(min=0, max=65535)
97
dhcp_queue = integer(min=0, max=65535)
98
nameservers = ip_addr_list(family=6)
99
domains = force_list(default=None)
100
"""
101

    
102

    
103
DHCPDISCOVER = 1
104
DHCPOFFER = 2
105
DHCPREQUEST = 3
106
DHCPDECLINE = 4
107
DHCPACK = 5
108
DHCPNAK = 6
109
DHCPRELEASE = 7
110
DHCPINFORM = 8
111

    
112
DHCP_TYPES = {
113
    DHCPDISCOVER: "DHCPDISCOVER",
114
    DHCPOFFER: "DHCPOFFER",
115
    DHCPREQUEST: "DHCPREQUEST",
116
    DHCPDECLINE: "DHCPDECLINE",
117
    DHCPACK: "DHCPACK",
118
    DHCPNAK: "DHCPNAK",
119
    DHCPRELEASE: "DHCPRELEASE",
120
    DHCPINFORM: "DHCPINFORM",
121
}
122

    
123
DHCP_REQRESP = {
124
    DHCPDISCOVER: DHCPOFFER,
125
    DHCPREQUEST: DHCPACK,
126
    DHCPINFORM: DHCPACK,
127
    }
128

    
129

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

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

    
145
    return indev_ifindex
146

    
147

    
148
def parse_binding_file(path):
149
    """ Read a client configuration from a tap file
150

    
151
    """
152
    logging.info("Parsing binding file %s", path)
153
    try:
154
        iffile = open(path, 'r')
155
    except EnvironmentError, e:
156
        logging.warn(" - Unable to open binding file %s: %s", path, str(e))
157
        return None
158

    
159
    tap = os.path.basename(path)
160
    indev = None
161
    mac = None
162
    ip = None
163
    hostname = None
164
    subnet = None
165
    gateway = None
166
    subnet6 = None
167
    gateway6 = None
168
    eui64 = None
169

    
170
    def get_value(line):
171
        v = line.strip().split('=')[1]
172
        if v == '':
173
            return None
174
        return v
175

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

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

    
205

    
206
class ClientFileHandler(pyinotify.ProcessEvent):
207
    def __init__(self, server):
208
        pyinotify.ProcessEvent.__init__(self)
209
        self.server = server
210

    
211
    def process_IN_DELETE(self, event):  # pylint: disable=C0103
212
        """ Delete file handler
213

    
214
        Currently this removes an interface from the watch list
215

    
216
        """
217
        self.server.remove_tap(event.name)
218

    
219
    def process_IN_CLOSE_WRITE(self, event):  # pylint: disable=C0103
220
        """ Add file handler
221

    
222
        Currently this adds an interface to the watch list
223

    
224
        """
225
        self.server.add_tap(os.path.join(event.path, event.name))
226

    
227

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

    
247
    def is_valid(self):
248
        return self.mac is not None and self.hostname is not None
249

    
250

    
251
    def open_socket(self):
252

    
253
        logging.info(" - Opening L2 socket and binding to %s", self.tap)
254
        try:
255
            s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, ETH_P_ALL)
256
            s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
257
            s.bind((self.tap, ETH_P_ALL))
258
            self.socket = s
259
        except socket.error, e:
260
            logging.warning(" - Cannot open socket %s", e)
261

    
262

    
263
    def sendp(self, data):
264

    
265
        if isinstance(data, BasePacket):
266
            data = str(data)
267

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

    
270
        try:
271
            count = self.socket.send(data, socket.MSG_DONTWAIT)
272
        except socket.error, e:
273
            logging.warn(" - Send with MSG_DONTWAIT failed: %s", str(e))
274
            self.socket.close()
275
            self.open_socket()
276
            raise e
277

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

    
284

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

    
298
    @property
299
    def netmask(self):
300
        """ Return the netmask in textual representation
301

    
302
        """
303
        return str(self.net.netmask())
304

    
305
    @property
306
    def broadcast(self):
307
        """ Return the broadcast address in textual representation
308

    
309
        """
310
        return str(self.net.broadcast())
311

    
312
    @property
313
    def prefix(self):
314
        """ Return the network as an IPy.IP
315

    
316
        """
317
        return self.net.net()
318

    
319
    @property
320
    def prefixlen(self):
321
        """ Return the prefix length as an integer
322

    
323
        """
324
        return self.net.prefixlen()
325

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

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

    
341
    def make_eui64(self, mac):
342
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
343
        subnet.
344

    
345
        """
346
        return self._make_eui64(self.net, mac)
347

    
348
    def make_ll64(self, mac):
349
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
350

    
351
        """
352
        return self._make_eui64("fe80::", mac)
353

    
354

    
355
class VMNetProxy(object):  # pylint: disable=R0902
356
    def __init__(self, data_path, dhcp_queue_num=None,  # pylint: disable=R0913
357
                 rs_queue_num=None, ns_queue_num=None, dhcpv6_queue_num=None,
358
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
359
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
360
                 dhcp_domain=None,
361
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
362
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None,
363
                 dhcpv6_domains=None):
364

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

    
381
        if ipv6_nameservers is None:
382
            self.ipv6_nameservers = []
383
        else:
384
            self.ipv6_nameservers = ipv6_nameservers
385

    
386
        if dhcpv6_domains is None:
387
            self.dhcpv6_domains = []
388
        else:
389
            self.dhcpv6_domains = dhcpv6_domains
390

    
391
        self.ipv6_enabled = False
392

    
393
        self.clients = {}
394
        #self.subnets = {}
395
        #self.ifaces = {}
396
        #self.v6nets = {}
397
        self.nfq = {}
398

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

    
407
        # NFQUEUE setup
408
        if dhcp_queue_num is not None:
409
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
410

    
411
        if rs_queue_num is not None:
412
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
413
            self.ipv6_enabled = True
414

    
415
        if ns_queue_num is not None:
416
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
417
            self.ipv6_enabled = True
418

    
419
        if dhcpv6_queue_num is not None:
420
            self._setup_nfqueue(dhcpv6_queue_num, AF_INET6, self.dhcpv6_response, 10)
421
            self.ipv6_enabled = True
422

    
423
    def get_binding(self, ifindex, mac):
424
        try:
425
            if self.mac_indexed_clients:
426
                logging.debug(" - Getting binding for mac %s", mac)
427
                b = self.clients[mac]
428
            else:
429
                logging.debug(" - Getting binding for ifindex %s", ifindex)
430
                b = self.clients[ifindex]
431
            return b
432
        except KeyError:
433
            logging.debug(" - No client found for mac / ifindex %s / %s",
434
                          mac, ifindex)
435
            return None
436

    
437
    def _cleanup(self):
438
        """ Free all resources for a graceful exit
439

    
440
        """
441
        logging.info("Cleaning up")
442

    
443
        logging.debug(" - Closing netfilter queues")
444
        for q, _ in self.nfq.values():
445
            q.close()
446

    
447
        logging.debug(" - Stopping inotify watches")
448
        self.notifier.stop()
449

    
450
        logging.info(" - Cleanup finished")
451

    
452
    def _setup_nfqueue(self, queue_num, family, callback, pending):
453
        logging.info("Setting up NFQUEUE for queue %d, AF %s",
454
                      queue_num, family)
455
        q = nfqueue.queue()
456
        q.set_callback(callback)
457
        q.fast_open(queue_num, family)
458
        q.set_queue_maxlen(5000)
459
        # This is mandatory for the queue to operate
460
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
461
        self.nfq[q.get_fd()] = (q, pending)
462
        logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
463

    
464
    def build_config(self):
465
        self.clients.clear()
466

    
467
        for path in glob.glob(os.path.join(self.data_path, "*")):
468
            self.add_tap(path)
469

    
470
        self.print_clients()
471

    
472
    def get_ifindex(self, iface):
473
        """ Get the interface index from sysfs
474

    
475
        """
476
        logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
477

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

    
482
        ifindex = None
483

    
484
        try:
485
            f = open(path, 'r')
486
        except EnvironmentError:
487
            logging.debug(" - %s is probably down, removing", iface)
488
            self.remove_tap(iface)
489

    
490
            return ifindex
491

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

    
506
        return ifindex
507

    
508
    def get_iface_hw_addr(self, iface):
509
        """ Get the interface hardware address from sysfs
510

    
511
        """
512
        logging.debug(" - Getting mac for iface %s", iface)
513
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
514
        if not path.startswith(SYSFS_NET):
515
            return None
516

    
517
        addr = None
518
        try:
519
            f = open(path, 'r')
520
        except EnvironmentError:
521
            logging.debug(" - %s is probably down, removing", iface)
522
            self.remove_tap(iface)
523
            return addr
524

    
525
        try:
526
            addr = f.readline().strip()
527
        except EnvironmentError, e:
528
            logging.warn(" - Failed to read hw address for %s from sysfs: %s",
529
                         iface, str(e))
530
        finally:
531
            f.close()
532

    
533
        return addr
534

    
535
    def add_tap(self, path):
536
        """ Add an interface to monitor
537

    
538
        """
539
        tap = os.path.basename(path)
540

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

    
547
        if ifindex is None:
548
            logging.warn(" - Stale configuration for %s found", tap)
549
        else:
550
            if b.is_valid():
551
                if self.mac_indexed_clients:
552
                    self.clients[b.mac] = b
553
                    k = b.mac
554
                else:
555
                    self.clients[ifindex] = b
556
                    k = ifindex
557
                logging.info(" - Added client:")
558
                logging.info(" + %10s | %20s %20s %10s %20s %40s",
559
                             k, b.hostname, b.mac, b.tap, b.ip, b.eui64)
560

    
561
    def remove_tap(self, tap):
562
        """ Cleanup clients on a removed interface
563

    
564
        """
565
        try:
566
            for k, cl in self.clients.items():
567
                if cl.tap == tap:
568
                    logging.info("Removing client %s and closing socket on %s",
569
                                 cl.hostname, cl.tap)
570
                    logging.info(" - %10s | %20s %20s %10s %20s %40s",
571
                                 k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
572
                    cl.socket.close()
573
                    del self.clients[k]
574
        except:
575
            logging.debug("Client on %s disappeared!!!", tap)
576

    
577

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

    
581
        """
582
        logging.info(" * Processing pending DHCP request")
583
        # Workaround for supporting both squeezy's nfqueue-bindings-python
584
        # and wheezy's python-nfqueue because for some reason the function's
585
        # signature has changed and has broken compatibility
586
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
587
        if arg2:
588
            payload = arg2
589
        else:
590
            payload = arg1
591
        # Decode the response - NFQUEUE relays IP packets
592
        pkt = IP(payload.get_data())
593
        #logging.debug(pkt.show())
594

    
595
        # Get the client MAC address
596
        resp = pkt.getlayer(BOOTP).copy()
597
        hlen = resp.hlen
598
        mac = resp.chaddr[:hlen].encode("hex")
599
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
600

    
601
        # Server responses are always BOOTREPLYs
602
        resp.op = "BOOTREPLY"
603
        del resp.payload
604

    
605
        indev = get_indev(payload)
606

    
607
        binding = self.get_binding(indev, mac)
608
        if binding is None:
609
            # We don't know anything about this interface, so accept the packet
610
            # and return
611
            logging.debug(" - Ignoring DHCP request on unknown iface %s", indev)
612
            # We don't know what to do with this packet, so let the kernel
613
            # handle it
614
            payload.set_verdict(nfqueue.NF_ACCEPT)
615
            return
616

    
617
        # Signal the kernel that it shouldn't further process the packet
618
        payload.set_verdict(nfqueue.NF_DROP)
619

    
620
        if mac != binding.mac:
621
            logging.warn(" - Recieved spoofed DHCP request: mac %s, indev %s",
622
                         mac, indev)
623
            return
624

    
625
        if not binding.ip:
626
            logging.info(" - No IP found in binding file.")
627
            return
628

    
629
        logging.info(" - Generating DHCP response:"
630
                     " host %s, mac %s, tap %s, indev %s",
631
                       binding.hostname, mac, binding.tap, indev)
632

    
633

    
634
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
635
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
636
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
637
        subnet = binding.net
638

    
639
        if not DHCP in pkt:
640
            logging.warn(" - Invalid request from %s on %s, no DHCP"
641
                         " payload found", binding.mac, binding.tap)
642
            return
643

    
644
        dhcp_options = []
645
        requested_addr = binding.ip
646
        for opt in pkt[DHCP].options:
647
            if type(opt) is tuple and opt[0] == "message-type":
648
                req_type = opt[1]
649
            if type(opt) is tuple and opt[0] == "requested_addr":
650
                requested_addr = opt[1]
651

    
652
        logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
653
                     binding.mac, binding.tap)
654

    
655
        if self.dhcp_domain:
656
            domainname = self.dhcp_domain
657
        else:
658
            domainname = binding.hostname.split('.', 1)[-1]
659

    
660
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
661
            resp_type = DHCPNAK
662
            logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
663
                         " instead of %s", binding.mac, binding.tap,
664
                         requested_addr, binding.ip)
665

    
666
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
667
            resp_type = DHCP_REQRESP[req_type]
668
            resp.yiaddr = binding.ip
669
            dhcp_options += [
670
                 ("hostname", binding.hostname),
671
                 ("domain", domainname),
672
                 ("broadcast_address", str(subnet.broadcast)),
673
                 ("subnet_mask", str(subnet.netmask)),
674
                 ("renewal_time", self.lease_renewal),
675
                 ("lease_time", self.lease_lifetime),
676
            ]
677
            if subnet.gw:
678
                dhcp_options += [("router", subnet.gw)]
679
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
680

    
681
        elif req_type == DHCPINFORM:
682
            resp_type = DHCP_REQRESP[req_type]
683
            dhcp_options += [
684
                 ("hostname", binding.hostname),
685
                 ("domain", domainname),
686
            ]
687
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
688

    
689
        elif req_type == DHCPRELEASE:
690
            # Log and ignore
691
            logging.info(" - DHCPRELEASE from %s on %s",
692
                         binding.hostname, binding.tap)
693
            return
694

    
695
        # Finally, always add the server identifier and end options
696
        dhcp_options += [
697
            ("message-type", resp_type),
698
            ("server_id", DHCP_DUMMY_SERVER_IP),
699
            "end"
700
        ]
701
        resp /= DHCP(options=dhcp_options)
702

    
703
        logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
704
                     binding.ip, binding.tap)
705
        try:
706
            binding.sendp(resp)
707
        except socket.error, e:
708
            logging.warn(" - DHCP response on %s (%s) failed: %s",
709
                         binding.tap, binding.hostname, str(e))
710
        except Exception, e:
711
            logging.warn(" - Unkown error during DHCP response on %s (%s): %s",
712
                         binding.tap, binding.hostname, str(e))
713

    
714
    def dhcpv6_response(self, arg1, arg2=None):  # pylint: disable=W0613
715

    
716
        logging.info(" * Processing pending DHCPv6 request")
717
        # Workaround for supporting both squeezy's nfqueue-bindings-python
718
        # and wheezy's python-nfqueue because for some reason the function's
719
        # signature has changed and has broken compatibility
720
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
721
        if arg2:
722
            payload = arg2
723
        else:
724
            payload = arg1
725
        pkt = IPv6(payload.get_data())
726
        indev = get_indev(payload)
727

    
728
        #TODO: figure out how to find the src mac
729
        mac = None
730
        binding = self.get_binding(indev, mac)
731
        if binding is None:
732
            # We don't know anything about this interface, so accept the packet
733
            # and return
734
            logging.debug(" - Ignoring dhcpv6 request for mac %s", mac)
735
            # We don't know what to do with this packet, so let the kernel
736
            # handle it
737
            payload.set_verdict(nfqueue.NF_ACCEPT)
738
            return
739

    
740
        # Signal the kernel that it shouldn't further process the packet
741
        payload.set_verdict(nfqueue.NF_DROP)
742

    
743
        subnet = binding.net6
744

    
745
        if subnet.net is None:
746
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
747
            return
748

    
749
        indevmac = self.get_iface_hw_addr(binding.indev)
750
        ifll = subnet.make_ll64(indevmac)
751
        if ifll is None:
752
            return
753

    
754
        ofll = subnet.make_ll64(binding.mac)
755
        if ofll is None:
756
            return
757

    
758
        logging.info(" - Generating DHCPv6 response for host %s (mac %s) on tap %s",
759
                      binding.hostname, binding.mac, binding.tap)
760

    
761
        if self.dhcpv6_domains:
762
            domains = self.dhcpv6_domains
763
        else:
764
            domains = [binding.hostname.split('.', 1)[-1]]
765

    
766
        # We do this in order not to caclulate optlen ourselves
767
        dnsdomains = str(DHCP6OptDNSDomains(dnsdomains=domains))
768
        dnsservers = str(DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers))
769

    
770
        resp = Ether(src=indevmac, dst=binding.mac)/\
771
               IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
772
               UDP(sport=pkt.dport, dport=pkt.sport)/\
773
               DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
774
               DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
775
               DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
776
               DHCP6OptDNSDomains(dnsdomains)/\
777
               DHCP6OptDNSServers(dnsservers)
778

    
779
        try:
780
            binding.sendp(resp)
781
        except socket.error, e:
782
            logging.warn(" - DHCPv6 on %s (%s) failed: %s",
783
                         binding.tap, binding.hostname, str(e))
784
        except Exception, e:
785
            logging.warn(" - Unkown error during DHCPv6 on %s (%s): %s",
786
                         binding.tap, binding.hostname, str(e))
787

    
788

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

    
792
        """
793
        logging.info(" * Processing pending RS request")
794
        # Workaround for supporting both squeezy's nfqueue-bindings-python
795
        # and wheezy's python-nfqueue because for some reason the function's
796
        # signature has changed and has broken compatibility
797
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
798
        if arg2:
799
            payload = arg2
800
        else:
801
            payload = arg1
802
        pkt = IPv6(payload.get_data())
803
        #logging.debug(pkt.show())
804
        try:
805
            mac = pkt.lladdr
806
        except:
807
            logging.debug(" - Cannot obtain lladdr in rs")
808
            return
809

    
810
        indev = get_indev(payload)
811

    
812
        binding = self.get_binding(indev, mac)
813
        if binding is None:
814
            # We don't know anything about this interface, so accept the packet
815
            # and return
816
            logging.debug(" - Ignoring router solicitation on for mac %s", mac)
817
            # We don't know what to do with this packet, so let the kernel
818
            # handle it
819
            payload.set_verdict(nfqueue.NF_ACCEPT)
820
            return
821

    
822
        # Signal the kernel that it shouldn't further process the packet
823
        payload.set_verdict(nfqueue.NF_DROP)
824

    
825
        if mac != binding.mac:
826
            logging.warn(" - Received spoofed RS request: mac %s, tap %s",
827
                         mac, binding.tap)
828
            return
829

    
830
        subnet = binding.net6
831

    
832
        if subnet.net is None:
833
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
834
            return
835

    
836
        indevmac = self.get_iface_hw_addr(binding.indev)
837
        ifll = subnet.make_ll64(indevmac)
838
        if ifll is None:
839
            return
840

    
841
        logging.info(" - Generating RA for host %s (mac %s) on tap %s",
842
                      binding.hostname, mac, binding.tap)
843

    
844
        resp = Ether(src=indevmac)/\
845
               IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
846
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
847
                                     prefixlen=subnet.prefixlen)
848

    
849
        if self.ipv6_nameservers:
850
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
851
                                     lifetime=self.ra_period * 3)
852

    
853
        try:
854
            binding.sendp(resp)
855
        except socket.error, e:
856
            logging.warn(" - RA on %s (%s) failed: %s",
857
                         binding.tap, binding.hostname, str(e))
858
        except Exception, e:
859
            logging.warn(" - Unkown error during RA on %s (%s): %s",
860
                         binding.tap, binding.hostname, str(e))
861

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

    
865
        """
866

    
867
        logging.info(" * Processing pending NS request")
868
        # Workaround for supporting both squeezy's nfqueue-bindings-python
869
        # and wheezy's python-nfqueue because for some reason the function's
870
        # signature has changed and has broken compatibility
871
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
872
        if arg2:
873
            payload = arg2
874
        else:
875
            payload = arg1
876

    
877
        ns = IPv6(payload.get_data())
878
        #logging.debug(ns.show())
879
        try:
880
            mac = ns.lladdr
881
        except:
882
            logging.debug(" - Cannot obtain lladdr from ns")
883
            return
884

    
885

    
886
        indev = get_indev(payload)
887

    
888
        binding = self.get_binding(indev, mac)
889
        if binding is None:
890
            # We don't know anything about this interface, so accept the packet
891
            # and return
892
            logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
893
                          ns.tgt)
894
            # We don't know what to do with this packet, so let the kernel
895
            # handle it
896
            payload.set_verdict(nfqueue.NF_ACCEPT)
897
            return
898

    
899
        payload.set_verdict(nfqueue.NF_DROP)
900

    
901
        if mac != binding.mac:
902
            logging.warn(" - Received spoofed NS request"
903
                         " for mac %s from tap %s", mac, binding.tap)
904
            return
905

    
906
        subnet = binding.net6
907
        if subnet.net is None:
908
            logging.debug(" - No IPv6 network assigned for the interface")
909
            return
910

    
911
        indevmac = self.get_iface_hw_addr(binding.indev)
912

    
913
        ifll = subnet.make_ll64(indevmac)
914
        if ifll is None:
915
            return
916

    
917
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
918
            logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
919
            return 1
920

    
921
        logging.info(" - Generating NA for host %s (mac %s) on tap %s",
922
                     binding.hostname, mac, binding.tap)
923

    
924
        resp = Ether(src=indevmac, dst=binding.mac)/\
925
               IPv6(src=str(ifll), dst=ns.src)/\
926
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
927
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
928

    
929
        try:
930
            binding.sendp(resp)
931
        except socket.error, e:
932
            logging.warn(" - NA on %s (%s) failed: %s",
933
                         binding.tap, binding.hostname, str(e))
934
        except Exception, e:
935
            logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
936
                         binding.tap, binding.hostname, str(e))
937

    
938
    def send_periodic_ra(self):
939
        # Use a separate thread as this may take a _long_ time with
940
        # many interfaces and we want to be responsive in the mean time
941
        threading.Thread(target=self._send_periodic_ra).start()
942

    
943
    def _send_periodic_ra(self):
944
        logging.info("Sending out periodic RAs")
945
        start = time.time()
946
        i = 0
947
        for binding in self.clients.values():
948
            tap = binding.tap
949
            indev = binding.indev
950
            # mac = binding.mac
951
            subnet = binding.net6
952
            if subnet.net is None:
953
                logging.debug(" - Skipping periodic RA on interface %s,"
954
                              " as it is not IPv6-connected", tap)
955
                continue
956
            indevmac = self.get_iface_hw_addr(indev)
957
            ifll = subnet.make_ll64(indevmac)
958
            if ifll is None:
959
                continue
960
            resp = Ether(src=indevmac)/\
961
                   IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
962
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
963
                                         prefixlen=subnet.prefixlen)
964
            if self.ipv6_nameservers:
965
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
966
                                         lifetime=self.ra_period * 3)
967
            try:
968
                binding.sendp(resp)
969
            except socket.error, e:
970
                logging.warn(" - Periodic RA on %s (%s) failed: %s",
971
                             tap, binding.hostname, str(e))
972
            except Exception, e:
973
                logging.warn(" - Unkown error during periodic RA on %s (%s):"
974
                             " %s", tap, binding.hostname, str(e))
975
            i += 1
976
        logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
977

    
978
    def serve(self):
979
        """ Safely perform the main loop, freeing all resources upon exit
980

    
981
        """
982
        try:
983
            self._serve()
984
        finally:
985
            self._cleanup()
986

    
987
    def _serve(self):
988
        """ Loop forever, serving DHCP requests
989

    
990
        """
991
        self.build_config()
992

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

    
997
        start = time.time()
998
        if self.ipv6_enabled:
999
            timeout = self.ra_period
1000
            self.send_periodic_ra()
1001
        else:
1002
            timeout = None
1003

    
1004
        while True:
1005
            try:
1006
                rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
1007
                                                [], [], timeout)
1008
            except select.error, e:
1009
                if e[0] == errno.EINTR:
1010
                    logging.debug("select() got interrupted")
1011
                    continue
1012

    
1013
            if xlist:
1014
                logging.warn("Warning: Exception on %s",
1015
                             ", ".join([str(fd) for fd in xlist]))
1016

    
1017
            if rlist:
1018
                if iwfd in rlist:
1019
                # First check if there are any inotify (= configuration change)
1020
                # events
1021
                    self.notifier.read_events()
1022
                    self.notifier.process_events()
1023
                    rlist.remove(iwfd)
1024

    
1025
                logging.debug("Pending requests on fds %s", rlist)
1026

    
1027
                for fd in rlist:
1028
                    try:
1029
                        q, num = self.nfq[fd]
1030
                        cnt = q.process_pending(num)
1031
                        logging.debug(" * Processed %d requests on NFQUEUE"
1032
                                      " with fd %d", cnt, fd)
1033
                    except RuntimeError, e:
1034
                        logging.warn("Error processing fd %d: %s", fd, str(e))
1035
                    except Exception, e:
1036
                        logging.warn("Unknown error processing fd %d: %s",
1037
                                     fd, str(e))
1038

    
1039
            if self.ipv6_enabled:
1040
                # Calculate the new timeout
1041
                timeout = self.ra_period - (time.time() - start)
1042

    
1043
                if timeout <= 0:
1044
                    start = time.time()
1045
                    self.send_periodic_ra()
1046
                    timeout = self.ra_period - (time.time() - start)
1047

    
1048
    def print_clients(self):
1049
        logging.info("%10s   %20s %20s %10s %20s %40s",
1050
                     'Key', 'Client', 'MAC', 'TAP', 'IP', 'IPv6')
1051
        for k, cl in self.clients.items():
1052
            logging.info("%10s | %20s %20s %10s %20s %40s",
1053
                         k, cl.hostname, cl.mac, cl.tap, cl.ip, cl.eui64)
1054

    
1055

    
1056

    
1057
if __name__ == "__main__":
1058
    import capng
1059
    import optparse
1060
    from cStringIO import StringIO
1061
    from pwd import getpwnam, getpwuid
1062
    from configobj import ConfigObj, ConfigObjError, flatten_errors
1063

    
1064
    import validate
1065

    
1066
    validator = validate.Validator()
1067

    
1068
    def is_ip_list(value, family=4):
1069
        try:
1070
            family = int(family)
1071
        except ValueError:
1072
            raise validate.VdtParamError(family)
1073
        if isinstance(value, (str, unicode)):
1074
            value = [value]
1075
        if not isinstance(value, list):
1076
            raise validate.VdtTypeError(value)
1077

    
1078
        for entry in value:
1079
            try:
1080
                ip = IPy.IP(entry)
1081
            except ValueError:
1082
                raise validate.VdtValueError(entry)
1083

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

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

    
1091
    parser = optparse.OptionParser()
1092
    parser.add_option("-c", "--config", dest="config_file",
1093
                      help="The location of the data files", metavar="FILE",
1094
                      default=DEFAULT_CONFIG)
1095
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
1096
                      help="Turn on debugging messages")
1097
    parser.add_option("-f", "--foreground", action="store_false",
1098
                      dest="daemonize", default=True,
1099
                      help="Do not daemonize, stay in the foreground")
1100

    
1101
    opts, args = parser.parse_args()
1102

    
1103
    try:
1104
        config = ConfigObj(opts.config_file, configspec=config_spec)
1105
    except ConfigObjError, err:
1106
        sys.stderr.write("Failed to parse config file %s: %s" %
1107
                         (opts.config_file, str(err)))
1108
        sys.exit(1)
1109

    
1110
    results = config.validate(validator)
1111
    if results != True:
1112
        logging.fatal("Configuration file validation failed! See errors below:")
1113
        for (section_list, key, unused) in flatten_errors(config, results):
1114
            if key is not None:
1115
                logging.fatal(" '%s' in section '%s' failed validation",
1116
                              key, ", ".join(section_list))
1117
            else:
1118
                logging.fatal(" Section '%s' is missing",
1119
                              ", ".join(section_list))
1120
        sys.exit(1)
1121

    
1122
    try:
1123
        uid = getpwuid(config["general"].as_int("user"))
1124
    except ValueError:
1125
        uid = getpwnam(config["general"]["user"])
1126

    
1127
    # Keep only the capabilities we need
1128
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1129
    # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1130
    # CAP_SETPCAP: needed by capng_change_id()
1131
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1132
    capng.capng_update(capng.CAPNG_ADD,
1133
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1134
                       capng.CAP_NET_ADMIN)
1135
    capng.capng_update(capng.CAPNG_ADD,
1136
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1137
                       capng.CAP_NET_RAW)
1138
    capng.capng_update(capng.CAPNG_ADD,
1139
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1140
                       capng.CAP_SETPCAP)
1141
    # change uid
1142
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1143
                          capng.CAPNG_DROP_SUPP_GRP | \
1144
                          capng.CAPNG_CLEAR_BOUNDING)
1145

    
1146
    logger = logging.getLogger()
1147
    if opts.debug:
1148
        logger.setLevel(logging.DEBUG)
1149
    else:
1150
        logger.setLevel(logging.INFO)
1151

    
1152
    if opts.daemonize:
1153
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1154
        handler = logging.handlers.WatchedFileHandler(logfile)
1155
    else:
1156
        handler = logging.StreamHandler()
1157

    
1158
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
1159
    logger.addHandler(handler)
1160

    
1161
    # Rename this process so 'ps' output looks like
1162
    # this is a native executable.
1163
    # NOTE: due to a bug in python-setproctitle, one cannot yet
1164
    # set individual values for command-line arguments, so only show
1165
    # the name of the executable instead.
1166
    # setproctitle.setproctitle("\x00".join(sys.argv))
1167
    setproctitle.setproctitle(sys.argv[0])
1168

    
1169
    if opts.daemonize:
1170
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1171
            config["general"]["pidfile"], 10)
1172
        # Remove any stale PID files, left behind by previous invocations
1173
        if daemon.runner.is_pidfile_stale(pidfile):
1174
            logger.warning("Removing stale PID lock file %s", pidfile.path)
1175
            pidfile.break_lock()
1176

    
1177
        d = daemon.DaemonContext(pidfile=pidfile,
1178
                                 umask=0022,
1179
                                 stdout=handler.stream,
1180
                                 stderr=handler.stream,
1181
                                 files_preserve=[handler.stream])
1182
        try:
1183
            d.open()
1184
        except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1185
            logger.critical("Failed to lock pidfile %s,"
1186
                            " another instance running?", pidfile.path)
1187
            sys.exit(1)
1188

    
1189
    logging.info("Starting up")
1190
    logging.info("Running as %s (uid:%d, gid: %d)",
1191
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
1192

    
1193
    proxy_opts = {}
1194
    if config["dhcp"].as_bool("enable_dhcp"):
1195
        proxy_opts.update({
1196
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1197
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1198
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1199
            "dhcp_server_ip": config["dhcp"]["server_ip"],
1200
            "dhcp_nameservers": config["dhcp"]["nameservers"],
1201
            "dhcp_domain": config["dhcp"]["domain"],
1202
        })
1203

    
1204
    if config["ipv6"].as_bool("enable_ipv6"):
1205
        proxy_opts.update({
1206
            "dhcpv6_queue_num": config["ipv6"].as_int("dhcp_queue"),
1207
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1208
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1209
            "ra_period": config["ipv6"].as_int("ra_period"),
1210
            "ipv6_nameservers": config["ipv6"]["nameservers"],
1211
            "dhcpv6_domains": config["ipv6"]["domains"],
1212
        })
1213

    
1214
    # pylint: disable=W0142
1215
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1216

    
1217
    logging.info("Ready to serve requests")
1218

    
1219

    
1220
    def debug_handler(signum, _):
1221
        logging.debug('Received signal %d. Printing proxy state...', signum)
1222
        proxy.print_clients()
1223

    
1224
    # Set the signal handler for debuging clients
1225
    signal.signal(signal.SIGUSR1, debug_handler)
1226
    signal.siginterrupt(signal.SIGUSR1, False)
1227

    
1228
    try:
1229
        proxy.serve()
1230
    except Exception:
1231
        if opts.daemonize:
1232
            exc = "".join(traceback.format_exception(*sys.exc_info()))
1233
            logging.critical(exc)
1234
        raise
1235

    
1236

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