Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 7b0ebdd0

History | View | Annotate | Download (41.3 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
                else:
554
                    self.clients[ifindex] = b
555
                logging.debug(" - Added client:")
556
                logging.debug(" + %5s: %10s %20s %7s %15s",
557
                               ifindex, b.hostname, b.mac, b.tap, b.ip)
558

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

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

    
575

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

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

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

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

    
603
        indev = get_indev(payload)
604

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

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

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

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

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

    
631

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

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

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

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

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

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

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

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

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

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

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

    
712
    def dhcpv6_response(self, arg1, arg2=None):  # pylint: disable=W0613
713

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

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

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

    
741
        subnet = binding.net6
742

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

    
748
        ofll = subnet.make_ll64(binding.mac)
749
        if ofll is None:
750
            return
751

    
752
        logging.info(" - Generating DHCPv6 response for host %s (mac %s) on tap %s",
753
                      binding.hostname, binding.mac, binding.tap)
754

    
755
        if self.dhcpv6_domains:
756
            domains = self.dhcpv6_domains
757
        else:
758
            domains = [binding.hostname.split('.', 1)[-1]]
759

    
760
        # We do this in order not to caclulate optlen ourselves
761
        dnsdomains = str(DHCP6OptDNSDomains(dnsdomains=domains))
762
        dnsservers = str(DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers))
763

    
764
        resp = Ether(src=indevmac, dst=binding.mac)/\
765
               IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
766
               UDP(sport=pkt.dport, dport=pkt.sport)/\
767
               DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
768
               DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
769
               DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
770
               DHCP6OptDNSDomains(dnsdomains)/\
771
               DHCP6OptDNSServers(dnsservers)
772

    
773
        try:
774
            binding.sendp(resp)
775
        except socket.error, e:
776
            logging.warn(" - DHCPv6 on %s (%s) failed: %s",
777
                         binding.tap, binding.hostname, str(e))
778
        except Exception, e:
779
            logging.warn(" - Unkown error during DHCPv6 on %s (%s): %s",
780
                         binding.tap, binding.hostname, str(e))
781

    
782

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

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

    
804
        indev = get_indev(payload)
805

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

    
816
        # Signal the kernel that it shouldn't further process the packet
817
        payload.set_verdict(nfqueue.NF_DROP)
818

    
819
        if mac != binding.mac:
820
            logging.warn(" - Received spoofed RS request: mac %s, tap %s",
821
                         mac, binding.tap)
822
            return
823

    
824
        subnet = binding.net6
825

    
826
        if subnet.net is None:
827
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
828
            return
829

    
830
        indevmac = self.get_iface_hw_addr(binding.indev)
831
        ifll = subnet.make_ll64(indevmac)
832
        if ifll is None:
833
            return
834

    
835
        logging.info(" - Generating RA for host %s (mac %s) on tap %s",
836
                      binding.hostname, mac, binding.tap)
837

    
838
        resp = Ether(src=indevmac)/\
839
               IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
840
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
841
                                     prefixlen=subnet.prefixlen)
842

    
843
        if self.ipv6_nameservers:
844
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
845
                                     lifetime=self.ra_period * 3)
846

    
847
        try:
848
            binding.sendp(resp)
849
        except socket.error, e:
850
            logging.warn(" - RA on %s (%s) failed: %s",
851
                         binding.tap, binding.hostname, str(e))
852
        except Exception, e:
853
            logging.warn(" - Unkown error during RA on %s (%s): %s",
854
                         binding.tap, binding.hostname, str(e))
855

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

    
859
        """
860

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

    
871
        ns = IPv6(payload.get_data())
872
        #logging.debug(ns.show())
873
        try:
874
            mac = ns.lladdr
875
        except:
876
            logging.debug(" - Cannot obtain lladdr from ns")
877
            return
878

    
879

    
880
        indev = get_indev(payload)
881

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

    
893
        payload.set_verdict(nfqueue.NF_DROP)
894

    
895
        if mac != binding.mac:
896
            logging.warn(" - Received spoofed NS request"
897
                         " for mac %s from tap %s", mac, binding.tap)
898
            return
899

    
900
        subnet = binding.net6
901
        if subnet.net is None:
902
            logging.debug(" - No IPv6 network assigned for the interface")
903
            return
904

    
905
        indevmac = self.get_iface_hw_addr(binding.indev)
906

    
907
        ifll = subnet.make_ll64(indevmac)
908
        if ifll is None:
909
            return
910

    
911
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
912
            logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
913
            return 1
914

    
915
        logging.info(" - Generating NA for host %s (mac %s) on tap %s",
916
                     binding.hostname, mac, binding.tap)
917

    
918
        resp = Ether(src=indevmac, dst=binding.mac)/\
919
               IPv6(src=str(ifll), dst=ns.src)/\
920
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
921
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
922

    
923
        try:
924
            binding.sendp(resp)
925
        except socket.error, e:
926
            logging.warn(" - NA on %s (%s) failed: %s",
927
                         binding.tap, binding.hostname, str(e))
928
        except Exception, e:
929
            logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
930
                         binding.tap, binding.hostname, str(e))
931

    
932
    def send_periodic_ra(self):
933
        # Use a separate thread as this may take a _long_ time with
934
        # many interfaces and we want to be responsive in the mean time
935
        threading.Thread(target=self._send_periodic_ra).start()
936

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

    
972
    def serve(self):
973
        """ Safely perform the main loop, freeing all resources upon exit
974

    
975
        """
976
        try:
977
            self._serve()
978
        finally:
979
            self._cleanup()
980

    
981
    def _serve(self):
982
        """ Loop forever, serving DHCP requests
983

    
984
        """
985
        self.build_config()
986

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

    
991
        start = time.time()
992
        if self.ipv6_enabled:
993
            timeout = self.ra_period
994
            self.send_periodic_ra()
995
        else:
996
            timeout = None
997

    
998
        while True:
999
            try:
1000
                rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
1001
                                                [], [], timeout)
1002
            except select.error, e:
1003
                if e[0] == errno.EINTR:
1004
                    logging.debug("select() got interrupted")
1005
                    continue
1006

    
1007
            if xlist:
1008
                logging.warn("Warning: Exception on %s",
1009
                             ", ".join([str(fd) for fd in xlist]))
1010

    
1011
            if rlist:
1012
                if iwfd in rlist:
1013
                # First check if there are any inotify (= configuration change)
1014
                # events
1015
                    self.notifier.read_events()
1016
                    self.notifier.process_events()
1017
                    rlist.remove(iwfd)
1018

    
1019
                logging.debug("Pending requests on fds %s", rlist)
1020

    
1021
                for fd in rlist:
1022
                    try:
1023
                        q, num = self.nfq[fd]
1024
                        cnt = q.process_pending(num)
1025
                        logging.debug(" * Processed %d requests on NFQUEUE"
1026
                                      " with fd %d", cnt, fd)
1027
                    except RuntimeError, e:
1028
                        logging.warn("Error processing fd %d: %s", fd, str(e))
1029
                    except Exception, e:
1030
                        logging.warn("Unknown error processing fd %d: %s",
1031
                                     fd, str(e))
1032

    
1033
            if self.ipv6_enabled:
1034
                # Calculate the new timeout
1035
                timeout = self.ra_period - (time.time() - start)
1036

    
1037
                if timeout <= 0:
1038
                    start = time.time()
1039
                    self.send_periodic_ra()
1040
                    timeout = self.ra_period - (time.time() - start)
1041

    
1042
    def print_clients(self):
1043
        logging.info("%10s   %20s %20s %10s %20s",
1044
                     'Key', 'Client', 'MAC', 'TAP', 'IP')
1045
        for k, cl in self.clients.items():
1046
            logging.info("%10s | %20s %20s %10s %20s",
1047
                         k, cl.hostname, cl.mac, cl.tap, cl.ip)
1048

    
1049

    
1050

    
1051
if __name__ == "__main__":
1052
    import capng
1053
    import optparse
1054
    from cStringIO import StringIO
1055
    from pwd import getpwnam, getpwuid
1056
    from configobj import ConfigObj, ConfigObjError, flatten_errors
1057

    
1058
    import validate
1059

    
1060
    validator = validate.Validator()
1061

    
1062
    def is_ip_list(value, family=4):
1063
        try:
1064
            family = int(family)
1065
        except ValueError:
1066
            raise validate.VdtParamError(family)
1067
        if isinstance(value, (str, unicode)):
1068
            value = [value]
1069
        if not isinstance(value, list):
1070
            raise validate.VdtTypeError(value)
1071

    
1072
        for entry in value:
1073
            try:
1074
                ip = IPy.IP(entry)
1075
            except ValueError:
1076
                raise validate.VdtValueError(entry)
1077

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

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

    
1085
    parser = optparse.OptionParser()
1086
    parser.add_option("-c", "--config", dest="config_file",
1087
                      help="The location of the data files", metavar="FILE",
1088
                      default=DEFAULT_CONFIG)
1089
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
1090
                      help="Turn on debugging messages")
1091
    parser.add_option("-f", "--foreground", action="store_false",
1092
                      dest="daemonize", default=True,
1093
                      help="Do not daemonize, stay in the foreground")
1094

    
1095
    opts, args = parser.parse_args()
1096

    
1097
    try:
1098
        config = ConfigObj(opts.config_file, configspec=config_spec)
1099
    except ConfigObjError, err:
1100
        sys.stderr.write("Failed to parse config file %s: %s" %
1101
                         (opts.config_file, str(err)))
1102
        sys.exit(1)
1103

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

    
1116
    try:
1117
        uid = getpwuid(config["general"].as_int("user"))
1118
    except ValueError:
1119
        uid = getpwnam(config["general"]["user"])
1120

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

    
1140
    logger = logging.getLogger()
1141
    if opts.debug:
1142
        logger.setLevel(logging.DEBUG)
1143
    else:
1144
        logger.setLevel(logging.INFO)
1145

    
1146
    if opts.daemonize:
1147
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1148
        handler = logging.handlers.WatchedFileHandler(logfile)
1149
    else:
1150
        handler = logging.StreamHandler()
1151

    
1152
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
1153
    logger.addHandler(handler)
1154

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

    
1163
    if opts.daemonize:
1164
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1165
            config["general"]["pidfile"], 10)
1166
        # Remove any stale PID files, left behind by previous invocations
1167
        if daemon.runner.is_pidfile_stale(pidfile):
1168
            logger.warning("Removing stale PID lock file %s", pidfile.path)
1169
            pidfile.break_lock()
1170

    
1171
        d = daemon.DaemonContext(pidfile=pidfile,
1172
                                 umask=0022,
1173
                                 stdout=handler.stream,
1174
                                 stderr=handler.stream,
1175
                                 files_preserve=[handler.stream])
1176
        try:
1177
            d.open()
1178
        except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1179
            logger.critical("Failed to lock pidfile %s,"
1180
                            " another instance running?", pidfile.path)
1181
            sys.exit(1)
1182

    
1183
    logging.info("Starting up")
1184
    logging.info("Running as %s (uid:%d, gid: %d)",
1185
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
1186

    
1187
    proxy_opts = {}
1188
    if config["dhcp"].as_bool("enable_dhcp"):
1189
        proxy_opts.update({
1190
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1191
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1192
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1193
            "dhcp_server_ip": config["dhcp"]["server_ip"],
1194
            "dhcp_nameservers": config["dhcp"]["nameservers"],
1195
            "dhcp_domain": config["dhcp"]["domain"],
1196
        })
1197

    
1198
    if config["ipv6"].as_bool("enable_ipv6"):
1199
        proxy_opts.update({
1200
            "dhcpv6_queue_num": config["ipv6"].as_int("dhcp_queue"),
1201
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1202
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1203
            "ra_period": config["ipv6"].as_int("ra_period"),
1204
            "ipv6_nameservers": config["ipv6"]["nameservers"],
1205
            "dhcpv6_domains": config["ipv6"]["domains"],
1206
        })
1207

    
1208
    # pylint: disable=W0142
1209
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1210

    
1211
    logging.info("Ready to serve requests")
1212

    
1213

    
1214
    def debug_handler(signum, _):
1215
        logging.debug('Received signal %d. Printing proxy state...', signum)
1216
        proxy.print_clients()
1217

    
1218
    # Set the signal handler for debuging clients
1219
    signal.signal(signal.SIGUSR1, debug_handler)
1220
    signal.siginterrupt(signal.SIGUSR1, False)
1221

    
1222
    try:
1223
        proxy.serve()
1224
    except Exception:
1225
        if opts.daemonize:
1226
            exc = "".join(traceback.format_exception(*sys.exc_info()))
1227
            logging.critical(exc)
1228
        raise
1229

    
1230

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