Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 07c8990f

History | View | Annotate | Download (41.5 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
        if subnet.net is None:
744
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
745
            return
746

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

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

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

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

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

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

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

    
786

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

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

    
808
        indev = get_indev(payload)
809

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

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

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

    
828
        subnet = binding.net6
829

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

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

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

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

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

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

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

    
863
        """
864

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

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

    
883

    
884
        indev = get_indev(payload)
885

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

    
897
        payload.set_verdict(nfqueue.NF_DROP)
898

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

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

    
909
        indevmac = self.get_iface_hw_addr(binding.indev)
910

    
911
        ifll = subnet.make_ll64(indevmac)
912
        if ifll is None:
913
            return
914

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

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

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

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

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

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

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

    
979
        """
980
        try:
981
            self._serve()
982
        finally:
983
            self._cleanup()
984

    
985
    def _serve(self):
986
        """ Loop forever, serving DHCP requests
987

    
988
        """
989
        self.build_config()
990

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

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

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

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

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

    
1023
                logging.debug("Pending requests on fds %s", rlist)
1024

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

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

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

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

    
1053

    
1054

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

    
1062
    import validate
1063

    
1064
    validator = validate.Validator()
1065

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

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

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

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

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

    
1099
    opts, args = parser.parse_args()
1100

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

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

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

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

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

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

    
1156
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
1157
    logger.addHandler(handler)
1158

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

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

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

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

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

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

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

    
1215
    logging.info("Ready to serve requests")
1216

    
1217

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

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

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

    
1234

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