Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 06e6d9bc

History | View | Annotate | Download (40.8 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
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
"""
100

    
101

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

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

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

    
128

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

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

    
144
    return indev_ifindex
145

    
146

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

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

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

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

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

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

    
204

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

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

    
213
        Currently this removes an interface from the watch list
214

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

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

    
221
        Currently this adds an interface to the watch list
222

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

    
226

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

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

    
249

    
250
    def open_socket(self):
251

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

    
261

    
262
    def sendp(self, data):
263

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

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

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

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

    
283

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
353

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

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

    
379
        if ipv6_nameservers is None:
380
            self.ipv6_nameservers = []
381
        else:
382
            self.ipv6_nameservers = ipv6_nameservers
383

    
384
        self.ipv6_enabled = False
385

    
386
        self.clients = {}
387
        #self.subnets = {}
388
        #self.ifaces = {}
389
        #self.v6nets = {}
390
        self.nfq = {}
391

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

    
400
        # NFQUEUE setup
401
        if dhcp_queue_num is not None:
402
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
403

    
404
        if rs_queue_num is not None:
405
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
406
            self.ipv6_enabled = True
407

    
408
        if ns_queue_num is not None:
409
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
410
            self.ipv6_enabled = True
411

    
412
        if dhcpv6_queue_num is not None:
413
            self._setup_nfqueue(dhcpv6_queue_num, AF_INET6, self.dhcpv6_response, 10)
414
            self.ipv6_enabled = True
415

    
416
    def get_binding(self, ifindex, mac):
417
        try:
418
            if self.mac_indexed_clients:
419
                logging.debug(" - Getting binding for mac %s", mac)
420
                b = self.clients[mac]
421
            else:
422
                logging.debug(" - Getting binding for ifindex %s", ifindex)
423
                b = self.clients[ifindex]
424
            return b
425
        except KeyError:
426
            logging.debug(" - No client found for mac / ifindex %s / %s",
427
                          mac, ifindex)
428
            return None
429

    
430
    def _cleanup(self):
431
        """ Free all resources for a graceful exit
432

    
433
        """
434
        logging.info("Cleaning up")
435

    
436
        logging.debug(" - Closing netfilter queues")
437
        for q, _ in self.nfq.values():
438
            q.close()
439

    
440
        logging.debug(" - Stopping inotify watches")
441
        self.notifier.stop()
442

    
443
        logging.info(" - Cleanup finished")
444

    
445
    def _setup_nfqueue(self, queue_num, family, callback, pending):
446
        logging.info("Setting up NFQUEUE for queue %d, AF %s",
447
                      queue_num, family)
448
        q = nfqueue.queue()
449
        q.set_callback(callback)
450
        q.fast_open(queue_num, family)
451
        q.set_queue_maxlen(5000)
452
        # This is mandatory for the queue to operate
453
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
454
        self.nfq[q.get_fd()] = (q, pending)
455
        logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
456

    
457
    def build_config(self):
458
        self.clients.clear()
459

    
460
        for path in glob.glob(os.path.join(self.data_path, "*")):
461
            self.add_tap(path)
462

    
463
        self.print_clients()
464

    
465
    def get_ifindex(self, iface):
466
        """ Get the interface index from sysfs
467

    
468
        """
469
        logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
470

    
471
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
472
        if not path.startswith(SYSFS_NET):
473
            return None
474

    
475
        ifindex = None
476

    
477
        try:
478
            f = open(path, 'r')
479
        except EnvironmentError:
480
            logging.debug(" - %s is probably down, removing", iface)
481
            self.remove_tap(iface)
482

    
483
            return ifindex
484

    
485
        try:
486
            ifindex = f.readline().strip()
487
            try:
488
                ifindex = int(ifindex)
489
            except ValueError, e:
490
                logging.warn(" - Failed to get ifindex for %s, cannot parse"
491
                             " sysfs output '%s'", iface, ifindex)
492
        except EnvironmentError, e:
493
            logging.warn(" - Error reading %s's ifindex from sysfs: %s",
494
                         iface, str(e))
495
            self.remove_tap(iface)
496
        finally:
497
            f.close()
498

    
499
        return ifindex
500

    
501
    def get_iface_hw_addr(self, iface):
502
        """ Get the interface hardware address from sysfs
503

    
504
        """
505
        logging.debug(" - Getting mac for iface %s", iface)
506
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
507
        if not path.startswith(SYSFS_NET):
508
            return None
509

    
510
        addr = None
511
        try:
512
            f = open(path, 'r')
513
        except EnvironmentError:
514
            logging.debug(" - %s is probably down, removing", iface)
515
            self.remove_tap(iface)
516
            return addr
517

    
518
        try:
519
            addr = f.readline().strip()
520
        except EnvironmentError, e:
521
            logging.warn(" - Failed to read hw address for %s from sysfs: %s",
522
                         iface, str(e))
523
        finally:
524
            f.close()
525

    
526
        return addr
527

    
528
    def add_tap(self, path):
529
        """ Add an interface to monitor
530

    
531
        """
532
        tap = os.path.basename(path)
533

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

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

    
552
    def remove_tap(self, tap):
553
        """ Cleanup clients on a removed interface
554

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

    
568

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

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

    
586
        # Get the client MAC address
587
        resp = pkt.getlayer(BOOTP).copy()
588
        hlen = resp.hlen
589
        mac = resp.chaddr[:hlen].encode("hex")
590
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen - 1)
591

    
592
        # Server responses are always BOOTREPLYs
593
        resp.op = "BOOTREPLY"
594
        del resp.payload
595

    
596
        indev = get_indev(payload)
597

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

    
608
        # Signal the kernel that it shouldn't further process the packet
609
        payload.set_verdict(nfqueue.NF_DROP)
610

    
611
        if mac != binding.mac:
612
            logging.warn(" - Recieved spoofed DHCP request: mac %s, indev %s",
613
                         mac, indev)
614
            return
615

    
616
        if not binding.ip:
617
            logging.info(" - No IP found in binding file.")
618
            return
619

    
620
        logging.info(" - Generating DHCP response:"
621
                     " host %s, mac %s, tap %s, indev %s",
622
                       binding.hostname, mac, binding.tap, indev)
623

    
624

    
625
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
626
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
627
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
628
        subnet = binding.net
629

    
630
        if not DHCP in pkt:
631
            logging.warn(" - Invalid request from %s on %s, no DHCP"
632
                         " payload found", binding.mac, binding.tap)
633
            return
634

    
635
        dhcp_options = []
636
        requested_addr = binding.ip
637
        for opt in pkt[DHCP].options:
638
            if type(opt) is tuple and opt[0] == "message-type":
639
                req_type = opt[1]
640
            if type(opt) is tuple and opt[0] == "requested_addr":
641
                requested_addr = opt[1]
642

    
643
        logging.info(" - %s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
644
                     binding.mac, binding.tap)
645

    
646
        if self.dhcp_domain:
647
            domainname = self.dhcp_domain
648
        else:
649
            domainname = binding.hostname.split('.', 1)[-1]
650

    
651
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
652
            resp_type = DHCPNAK
653
            logging.info(" - Sending DHCPNAK to %s on %s: requested %s"
654
                         " instead of %s", binding.mac, binding.tap,
655
                         requested_addr, binding.ip)
656

    
657
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
658
            resp_type = DHCP_REQRESP[req_type]
659
            resp.yiaddr = binding.ip
660
            dhcp_options += [
661
                 ("hostname", binding.hostname),
662
                 ("domain", domainname),
663
                 ("broadcast_address", str(subnet.broadcast)),
664
                 ("subnet_mask", str(subnet.netmask)),
665
                 ("renewal_time", self.lease_renewal),
666
                 ("lease_time", self.lease_lifetime),
667
            ]
668
            if subnet.gw:
669
                dhcp_options += [("router", subnet.gw)]
670
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
671

    
672
        elif req_type == DHCPINFORM:
673
            resp_type = DHCP_REQRESP[req_type]
674
            dhcp_options += [
675
                 ("hostname", binding.hostname),
676
                 ("domain", domainname),
677
            ]
678
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
679

    
680
        elif req_type == DHCPRELEASE:
681
            # Log and ignore
682
            logging.info(" - DHCPRELEASE from %s on %s",
683
                         binding.hostname, binding.tap)
684
            return
685

    
686
        # Finally, always add the server identifier and end options
687
        dhcp_options += [
688
            ("message-type", resp_type),
689
            ("server_id", DHCP_DUMMY_SERVER_IP),
690
            "end"
691
        ]
692
        resp /= DHCP(options=dhcp_options)
693

    
694
        logging.info(" - %s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
695
                     binding.ip, binding.tap)
696
        try:
697
            binding.sendp(resp)
698
        except socket.error, e:
699
            logging.warn(" - DHCP response on %s (%s) failed: %s",
700
                         binding.tap, binding.hostname, str(e))
701
        except Exception, e:
702
            logging.warn(" - Unkown error during DHCP response on %s (%s): %s",
703
                         binding.tap, binding.hostname, str(e))
704

    
705
    def dhcpv6_response(self, arg1, arg2=None):  # pylint: disable=W0613
706

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

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

    
731
        # Signal the kernel that it shouldn't further process the packet
732
        payload.set_verdict(nfqueue.NF_DROP)
733

    
734
        subnet = binding.net6
735

    
736
        indevmac = self.get_iface_hw_addr(binding.indev)
737
        ifll = subnet.make_ll64(indevmac)
738
        if ifll is None:
739
            return
740

    
741
        ofll = subnet.make_ll64(binding.mac)
742
        if ofll is None:
743
            return
744

    
745
        logging.info(" - Generating DHCPv6 response for host %s (mac %s) on tap %s",
746
                      binding.hostname, binding.mac, binding.tap)
747

    
748
        resp = Ether(src=indevmac, dst=binding.mac)/\
749
               IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
750
               UDP(sport=pkt.dport, dport=pkt.sport)/\
751
               DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
752
               DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
753
               DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
754
               DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers,
755
                                  optlen=16 * len(self.ipv6_nameservers))
756

    
757
        try:
758
            binding.sendp(resp)
759
        except socket.error, e:
760
            logging.warn(" - DHCPv6 on %s (%s) failed: %s",
761
                         binding.tap, binding.hostname, str(e))
762
        except Exception, e:
763
            logging.warn(" - Unkown error during DHCPv6 on %s (%s): %s",
764
                         binding.tap, binding.hostname, str(e))
765

    
766

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

    
770
        """
771
        logging.info(" * Processing pending RS request")
772
        # Workaround for supporting both squeezy's nfqueue-bindings-python
773
        # and wheezy's python-nfqueue because for some reason the function's
774
        # signature has changed and has broken compatibility
775
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
776
        if arg2:
777
            payload = arg2
778
        else:
779
            payload = arg1
780
        pkt = IPv6(payload.get_data())
781
        #logging.debug(pkt.show())
782
        try:
783
            mac = pkt.lladdr
784
        except:
785
            logging.debug(" - Cannot obtain lladdr in rs")
786
            return
787

    
788
        indev = get_indev(payload)
789

    
790
        binding = self.get_binding(indev, mac)
791
        if binding is None:
792
            # We don't know anything about this interface, so accept the packet
793
            # and return
794
            logging.debug(" - Ignoring router solicitation on for mac %s", mac)
795
            # We don't know what to do with this packet, so let the kernel
796
            # handle it
797
            payload.set_verdict(nfqueue.NF_ACCEPT)
798
            return
799

    
800
        # Signal the kernel that it shouldn't further process the packet
801
        payload.set_verdict(nfqueue.NF_DROP)
802

    
803
        if mac != binding.mac:
804
            logging.warn(" - Received spoofed RS request: mac %s, tap %s",
805
                         mac, binding.tap)
806
            return
807

    
808
        subnet = binding.net6
809

    
810
        if subnet.net is None:
811
            logging.debug(" - No IPv6 network assigned for tap %s", binding.tap)
812
            return
813

    
814
        indevmac = self.get_iface_hw_addr(binding.indev)
815
        ifll = subnet.make_ll64(indevmac)
816
        if ifll is None:
817
            return
818

    
819
        logging.info(" - Generating RA for host %s (mac %s) on tap %s",
820
                      binding.hostname, mac, binding.tap)
821

    
822
        resp = Ether(src=indevmac)/\
823
               IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
824
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
825
                                     prefixlen=subnet.prefixlen)
826

    
827
        if self.ipv6_nameservers:
828
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
829
                                     lifetime=self.ra_period * 3)
830

    
831
        try:
832
            binding.sendp(resp)
833
        except socket.error, e:
834
            logging.warn(" - RA on %s (%s) failed: %s",
835
                         binding.tap, binding.hostname, str(e))
836
        except Exception, e:
837
            logging.warn(" - Unkown error during RA on %s (%s): %s",
838
                         binding.tap, binding.hostname, str(e))
839

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

    
843
        """
844

    
845
        logging.info(" * Processing pending NS request")
846
        # Workaround for supporting both squeezy's nfqueue-bindings-python
847
        # and wheezy's python-nfqueue because for some reason the function's
848
        # signature has changed and has broken compatibility
849
        # See bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=718894
850
        if arg2:
851
            payload = arg2
852
        else:
853
            payload = arg1
854

    
855
        ns = IPv6(payload.get_data())
856
        #logging.debug(ns.show())
857
        try:
858
            mac = ns.lladdr
859
        except:
860
            logging.debug(" - Cannot obtain lladdr from ns")
861
            return
862

    
863

    
864
        indev = get_indev(payload)
865

    
866
        binding = self.get_binding(indev, mac)
867
        if binding is None:
868
            # We don't know anything about this interface, so accept the packet
869
            # and return
870
            logging.debug(" - Ignoring neighbour solicitation for eui64 %s",
871
                          ns.tgt)
872
            # We don't know what to do with this packet, so let the kernel
873
            # handle it
874
            payload.set_verdict(nfqueue.NF_ACCEPT)
875
            return
876

    
877
        payload.set_verdict(nfqueue.NF_DROP)
878

    
879
        if mac != binding.mac:
880
            logging.warn(" - Received spoofed NS request"
881
                         " for mac %s from tap %s", mac, binding.tap)
882
            return
883

    
884
        subnet = binding.net6
885
        if subnet.net is None:
886
            logging.debug(" - No IPv6 network assigned for the interface")
887
            return
888

    
889
        indevmac = self.get_iface_hw_addr(binding.indev)
890

    
891
        ifll = subnet.make_ll64(indevmac)
892
        if ifll is None:
893
            return
894

    
895
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
896
            logging.debug(" - Received NS for a non-routable IP (%s)", ns.tgt)
897
            return 1
898

    
899
        logging.info(" - Generating NA for host %s (mac %s) on tap %s",
900
                     binding.hostname, mac, binding.tap)
901

    
902
        resp = Ether(src=indevmac, dst=binding.mac)/\
903
               IPv6(src=str(ifll), dst=ns.src)/\
904
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
905
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
906

    
907
        try:
908
            binding.sendp(resp)
909
        except socket.error, e:
910
            logging.warn(" - NA on %s (%s) failed: %s",
911
                         binding.tap, binding.hostname, str(e))
912
        except Exception, e:
913
            logging.warn(" - Unkown error during periodic NA to %s (%s): %s",
914
                         binding.tap, binding.hostname, str(e))
915

    
916
    def send_periodic_ra(self):
917
        # Use a separate thread as this may take a _long_ time with
918
        # many interfaces and we want to be responsive in the mean time
919
        threading.Thread(target=self._send_periodic_ra).start()
920

    
921
    def _send_periodic_ra(self):
922
        logging.info("Sending out periodic RAs")
923
        start = time.time()
924
        i = 0
925
        for binding in self.clients.values():
926
            tap = binding.tap
927
            indev = binding.indev
928
            # mac = binding.mac
929
            subnet = binding.net6
930
            if subnet.net is None:
931
                logging.debug(" - Skipping periodic RA on interface %s,"
932
                              " as it is not IPv6-connected", tap)
933
                continue
934
            indevmac = self.get_iface_hw_addr(indev)
935
            ifll = subnet.make_ll64(indevmac)
936
            if ifll is None:
937
                continue
938
            resp = Ether(src=indevmac)/\
939
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
940
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
941
                                         prefixlen=subnet.prefixlen)
942
            if self.ipv6_nameservers:
943
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
944
                                         lifetime=self.ra_period * 3)
945
            try:
946
                binding.sendp(resp)
947
            except socket.error, e:
948
                logging.warn(" - Periodic RA on %s (%s) failed: %s",
949
                             tap, binding.hostname, str(e))
950
            except Exception, e:
951
                logging.warn(" - Unkown error during periodic RA on %s (%s):"
952
                             " %s", tap, binding.hostname, str(e))
953
            i += 1
954
        logging.info(" - Sent %d RAs in %.2f seconds", i, time.time() - start)
955

    
956
    def serve(self):
957
        """ Safely perform the main loop, freeing all resources upon exit
958

    
959
        """
960
        try:
961
            self._serve()
962
        finally:
963
            self._cleanup()
964

    
965
    def _serve(self):
966
        """ Loop forever, serving DHCP requests
967

    
968
        """
969
        self.build_config()
970

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

    
975
        start = time.time()
976
        if self.ipv6_enabled:
977
            timeout = self.ra_period
978
            self.send_periodic_ra()
979
        else:
980
            timeout = None
981

    
982
        while True:
983
            try:
984
                rlist, _, xlist = select.select(self.nfq.keys() + [iwfd],
985
                                                [], [], timeout)
986
            except select.error, e:
987
                if e[0] == errno.EINTR:
988
                    logging.debug("select() got interrupted")
989
                    continue
990

    
991
            if xlist:
992
                logging.warn("Warning: Exception on %s",
993
                             ", ".join([str(fd) for fd in xlist]))
994

    
995
            if rlist:
996
                if iwfd in rlist:
997
                # First check if there are any inotify (= configuration change)
998
                # events
999
                    self.notifier.read_events()
1000
                    self.notifier.process_events()
1001
                    rlist.remove(iwfd)
1002

    
1003
                logging.debug("Pending requests on fds %s", rlist)
1004

    
1005
                for fd in rlist:
1006
                    try:
1007
                        q, num = self.nfq[fd]
1008
                        cnt = q.process_pending(num)
1009
                        logging.debug(" * Processed %d requests on NFQUEUE"
1010
                                      " with fd %d", cnt, fd)
1011
                    except RuntimeError, e:
1012
                        logging.warn("Error processing fd %d: %s", fd, str(e))
1013
                    except Exception, e:
1014
                        logging.warn("Unknown error processing fd %d: %s",
1015
                                     fd, str(e))
1016

    
1017
            if self.ipv6_enabled:
1018
                # Calculate the new timeout
1019
                timeout = self.ra_period - (time.time() - start)
1020

    
1021
                if timeout <= 0:
1022
                    start = time.time()
1023
                    self.send_periodic_ra()
1024
                    timeout = self.ra_period - (time.time() - start)
1025

    
1026
    def print_clients(self):
1027
        logging.info("%10s   %20s %20s %10s %20s",
1028
                     'Key', 'Client', 'MAC', 'TAP', 'IP')
1029
        for k, cl in self.clients.items():
1030
            logging.info("%10s | %20s %20s %10s %20s",
1031
                         k, cl.hostname, cl.mac, cl.tap, cl.ip)
1032

    
1033

    
1034

    
1035
if __name__ == "__main__":
1036
    import capng
1037
    import optparse
1038
    from cStringIO import StringIO
1039
    from pwd import getpwnam, getpwuid
1040
    from configobj import ConfigObj, ConfigObjError, flatten_errors
1041

    
1042
    import validate
1043

    
1044
    validator = validate.Validator()
1045

    
1046
    def is_ip_list(value, family=4):
1047
        try:
1048
            family = int(family)
1049
        except ValueError:
1050
            raise validate.VdtParamError(family)
1051
        if isinstance(value, (str, unicode)):
1052
            value = [value]
1053
        if not isinstance(value, list):
1054
            raise validate.VdtTypeError(value)
1055

    
1056
        for entry in value:
1057
            try:
1058
                ip = IPy.IP(entry)
1059
            except ValueError:
1060
                raise validate.VdtValueError(entry)
1061

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

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

    
1069
    parser = optparse.OptionParser()
1070
    parser.add_option("-c", "--config", dest="config_file",
1071
                      help="The location of the data files", metavar="FILE",
1072
                      default=DEFAULT_CONFIG)
1073
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
1074
                      help="Turn on debugging messages")
1075
    parser.add_option("-f", "--foreground", action="store_false",
1076
                      dest="daemonize", default=True,
1077
                      help="Do not daemonize, stay in the foreground")
1078

    
1079
    opts, args = parser.parse_args()
1080

    
1081
    try:
1082
        config = ConfigObj(opts.config_file, configspec=config_spec)
1083
    except ConfigObjError, err:
1084
        sys.stderr.write("Failed to parse config file %s: %s" %
1085
                         (opts.config_file, str(err)))
1086
        sys.exit(1)
1087

    
1088
    results = config.validate(validator)
1089
    if results != True:
1090
        logging.fatal("Configuration file validation failed! See errors below:")
1091
        for (section_list, key, unused) in flatten_errors(config, results):
1092
            if key is not None:
1093
                logging.fatal(" '%s' in section '%s' failed validation",
1094
                              key, ", ".join(section_list))
1095
            else:
1096
                logging.fatal(" Section '%s' is missing",
1097
                              ", ".join(section_list))
1098
        sys.exit(1)
1099

    
1100
    try:
1101
        uid = getpwuid(config["general"].as_int("user"))
1102
    except ValueError:
1103
        uid = getpwnam(config["general"]["user"])
1104

    
1105
    # Keep only the capabilities we need
1106
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1107
    # CAP_NET_RAW: we need to reopen socket in case the buffer gets full
1108
    # CAP_SETPCAP: needed by capng_change_id()
1109
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1110
    capng.capng_update(capng.CAPNG_ADD,
1111
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1112
                       capng.CAP_NET_ADMIN)
1113
    capng.capng_update(capng.CAPNG_ADD,
1114
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1115
                       capng.CAP_NET_RAW)
1116
    capng.capng_update(capng.CAPNG_ADD,
1117
                       capng.CAPNG_EFFECTIVE | capng.CAPNG_PERMITTED,
1118
                       capng.CAP_SETPCAP)
1119
    # change uid
1120
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1121
                          capng.CAPNG_DROP_SUPP_GRP | \
1122
                          capng.CAPNG_CLEAR_BOUNDING)
1123

    
1124
    logger = logging.getLogger()
1125
    if opts.debug:
1126
        logger.setLevel(logging.DEBUG)
1127
    else:
1128
        logger.setLevel(logging.INFO)
1129

    
1130
    if opts.daemonize:
1131
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
1132
        handler = logging.handlers.WatchedFileHandler(logfile)
1133
    else:
1134
        handler = logging.StreamHandler()
1135

    
1136
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
1137
    logger.addHandler(handler)
1138

    
1139
    # Rename this process so 'ps' output looks like
1140
    # this is a native executable.
1141
    # NOTE: due to a bug in python-setproctitle, one cannot yet
1142
    # set individual values for command-line arguments, so only show
1143
    # the name of the executable instead.
1144
    # setproctitle.setproctitle("\x00".join(sys.argv))
1145
    setproctitle.setproctitle(sys.argv[0])
1146

    
1147
    if opts.daemonize:
1148
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
1149
            config["general"]["pidfile"], 10)
1150
        # Remove any stale PID files, left behind by previous invocations
1151
        if daemon.runner.is_pidfile_stale(pidfile):
1152
            logger.warning("Removing stale PID lock file %s", pidfile.path)
1153
            pidfile.break_lock()
1154

    
1155
        d = daemon.DaemonContext(pidfile=pidfile,
1156
                                 umask=0022,
1157
                                 stdout=handler.stream,
1158
                                 stderr=handler.stream,
1159
                                 files_preserve=[handler.stream])
1160
        try:
1161
            d.open()
1162
        except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
1163
            logger.critical("Failed to lock pidfile %s,"
1164
                            " another instance running?", pidfile.path)
1165
            sys.exit(1)
1166

    
1167
    logging.info("Starting up")
1168
    logging.info("Running as %s (uid:%d, gid: %d)",
1169
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
1170

    
1171
    proxy_opts = {}
1172
    if config["dhcp"].as_bool("enable_dhcp"):
1173
        proxy_opts.update({
1174
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
1175
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
1176
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
1177
            "dhcp_server_ip": config["dhcp"]["server_ip"],
1178
            "dhcp_nameservers": config["dhcp"]["nameservers"],
1179
            "dhcp_domain": config["dhcp"]["domain"],
1180
        })
1181

    
1182
    if config["ipv6"].as_bool("enable_ipv6"):
1183
        proxy_opts.update({
1184
            "dhcpv6_queue_num": config["ipv6"].as_int("dhcp_queue"),
1185
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
1186
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
1187
            "ra_period": config["ipv6"].as_int("ra_period"),
1188
            "ipv6_nameservers": config["ipv6"]["nameservers"],
1189
        })
1190

    
1191
    # pylint: disable=W0142
1192
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1193

    
1194
    logging.info("Ready to serve requests")
1195

    
1196

    
1197
    def debug_handler(signum, _):
1198
        logging.debug('Received signal %d. Printing proxy state...', signum)
1199
        proxy.print_clients()
1200

    
1201
    # Set the signal handler for debuging clients
1202
    signal.signal(signal.SIGUSR1, debug_handler)
1203
    signal.siginterrupt(signal.SIGUSR1, False)
1204

    
1205
    try:
1206
        proxy.serve()
1207
    except Exception:
1208
        if opts.daemonize:
1209
            exc = "".join(traceback.format_exception(*sys.exc_info()))
1210
            logging.critical(exc)
1211
        raise
1212

    
1213

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