Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 2048471a

History | View | Annotate | Download (40.1 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
    def __repr__(self):
285
        ret =  "hostname %s, tap %s, mac %s" % \
286
               (self.hostname, self.tap, self.mac)
287
        if self.ip:
288
            ret += ", ip %s" % self.ip
289
        if self.eui64:
290
            ret += ", eui64 %s" % self.eui64
291
        return ret
292

    
293

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

    
307
    @property
308
    def netmask(self):
309
        """ Return the netmask in textual representation
310

    
311
        """
312
        return str(self.net.netmask())
313

    
314
    @property
315
    def broadcast(self):
316
        """ Return the broadcast address in textual representation
317

    
318
        """
319
        return str(self.net.broadcast())
320

    
321
    @property
322
    def prefix(self):
323
        """ Return the network as an IPy.IP
324

    
325
        """
326
        return self.net.net()
327

    
328
    @property
329
    def prefixlen(self):
330
        """ Return the prefix length as an integer
331

    
332
        """
333
        return self.net.prefixlen()
334

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

    
339
        """
340
        if mac is None:
341
            return None
342
        comp = mac.split(":")
343
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
344
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
345
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
346
        for l in range(0, len(eui64), 2):
347
            prefix += ["".join(eui64[l:l+2])]
348
        return IPy.IP(":".join(prefix))
349

    
350
    def make_eui64(self, mac):
351
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
352
        subnet.
353

    
354
        """
355
        return self._make_eui64(self.net, mac)
356

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

    
360
        """
361
        return self._make_eui64("fe80::", mac)
362

    
363

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

    
374
        try:
375
            getattr(nfqueue.payload, 'get_physindev')
376
            self.mac_indexed_clients = False
377
        except AttributeError:
378
            self.mac_indexed_clients = True
379
        self.data_path = data_path
380
        self.lease_lifetime = dhcp_lease_lifetime
381
        self.lease_renewal = dhcp_lease_renewal
382
        self.dhcp_domain = dhcp_domain
383
        self.dhcp_server_ip = dhcp_server_ip
384
        self.ra_period = ra_period
385
        if dhcp_nameservers is None:
386
            self.dhcp_nameserver = []
387
        else:
388
            self.dhcp_nameservers = dhcp_nameservers
389

    
390
        if ipv6_nameservers is None:
391
            self.ipv6_nameservers = []
392
        else:
393
            self.ipv6_nameservers = ipv6_nameservers
394

    
395
        if dhcpv6_domains is None:
396
            self.dhcpv6_domains = []
397
        else:
398
            self.dhcpv6_domains = dhcpv6_domains
399

    
400
        self.ipv6_enabled = False
401

    
402
        self.clients = {}
403
        #self.subnets = {}
404
        #self.ifaces = {}
405
        #self.v6nets = {}
406
        self.nfq = {}
407

    
408
        # Inotify setup
409
        self.wm = pyinotify.WatchManager()
410
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
411
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
412
        inotify_handler = ClientFileHandler(self)
413
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
414
        self.wm.add_watch(self.data_path, mask, rec=True)
415

    
416
        # NFQUEUE setup
417
        if dhcp_queue_num is not None:
418
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response, 0)
419

    
420
        if rs_queue_num is not None:
421
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response, 10)
422
            self.ipv6_enabled = True
423

    
424
        if ns_queue_num is not None:
425
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response, 10)
426
            self.ipv6_enabled = True
427

    
428
        if dhcpv6_queue_num is not None:
429
            self._setup_nfqueue(dhcpv6_queue_num, AF_INET6, self.dhcpv6_response, 10)
430
            self.ipv6_enabled = True
431

    
432
    def get_binding(self, ifindex, mac):
433
        try:
434
            if self.mac_indexed_clients:
435
                logging.debug(" - Getting binding for mac %s", mac)
436
                b = self.clients[mac]
437
            else:
438
                logging.debug(" - Getting binding for ifindex %s", ifindex)
439
                b = self.clients[ifindex]
440
            logging.info(" - Client found. %s", b)
441
            return b
442
        except KeyError:
443
            logging.info(" - No client found. mac: %s, ifindex: %s",
444
                         mac, ifindex)
445
            return None
446

    
447
    def _cleanup(self):
448
        """ Free all resources for a graceful exit
449

    
450
        """
451
        logging.info("Cleaning up")
452

    
453
        logging.debug(" - Closing netfilter queues")
454
        for q, _ in self.nfq.values():
455
            q.close()
456

    
457
        logging.debug(" - Stopping inotify watches")
458
        self.notifier.stop()
459

    
460
        logging.info(" - Cleanup finished")
461

    
462
    def _setup_nfqueue(self, queue_num, family, callback, pending):
463
        logging.info("Setting up NFQUEUE for queue %d, AF %s",
464
                      queue_num, family)
465
        q = nfqueue.queue()
466
        q.set_callback(callback)
467
        q.fast_open(queue_num, family)
468
        q.set_queue_maxlen(5000)
469
        # This is mandatory for the queue to operate
470
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
471
        self.nfq[q.get_fd()] = (q, pending)
472
        logging.debug(" - Successfully set up NFQUEUE %d", queue_num)
473

    
474
    def build_config(self):
475
        self.clients.clear()
476

    
477
        for path in glob.glob(os.path.join(self.data_path, "*")):
478
            self.add_tap(path)
479

    
480
        self.print_clients()
481

    
482
    def get_ifindex(self, iface):
483
        """ Get the interface index from sysfs
484

    
485
        """
486
        logging.debug(" - Getting ifindex for interface %s from sysfs", iface)
487

    
488
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
489
        if not path.startswith(SYSFS_NET):
490
            return None
491

    
492
        ifindex = None
493

    
494
        try:
495
            f = open(path, 'r')
496
        except EnvironmentError:
497
            logging.debug(" - %s is probably down, removing", iface)
498
            self.remove_tap(iface)
499

    
500
            return ifindex
501

    
502
        try:
503
            ifindex = f.readline().strip()
504
            try:
505
                ifindex = int(ifindex)
506
            except ValueError, e:
507
                logging.warn(" - Failed to get ifindex for %s, cannot parse"
508
                             " sysfs output '%s'", iface, ifindex)
509
        except EnvironmentError, e:
510
            logging.warn(" - Error reading %s's ifindex from sysfs: %s",
511
                         iface, str(e))
512
            self.remove_tap(iface)
513
        finally:
514
            f.close()
515

    
516
        return ifindex
517

    
518
    def get_iface_hw_addr(self, iface):
519
        """ Get the interface hardware address from sysfs
520

    
521
        """
522
        logging.debug(" - Getting mac for iface %s", iface)
523
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
524
        if not path.startswith(SYSFS_NET):
525
            return None
526

    
527
        addr = None
528
        try:
529
            f = open(path, 'r')
530
        except EnvironmentError:
531
            logging.debug(" - %s is probably down, removing", iface)
532
            self.remove_tap(iface)
533
            return addr
534

    
535
        try:
536
            addr = f.readline().strip()
537
        except EnvironmentError, e:
538
            logging.warn(" - Failed to read hw address for %s from sysfs: %s",
539
                         iface, str(e))
540
        finally:
541
            f.close()
542

    
543
        return addr
544

    
545
    def add_tap(self, path):
546
        """ Add an interface to monitor
547

    
548
        """
549
        tap = os.path.basename(path)
550

    
551
        logging.info("Updating configuration for %s", tap)
552
        b = parse_binding_file(path)
553
        if b is None:
554
            return
555
        ifindex = self.get_ifindex(b.tap)
556

    
557
        if ifindex is None:
558
            logging.warn(" - Stale configuration for %s found", tap)
559
        else:
560
            if b.is_valid():
561
                if self.mac_indexed_clients:
562
                    self.clients[b.mac] = b
563
                    k = b.mac
564
                else:
565
                    self.clients[ifindex] = b
566
                    k = ifindex
567
                logging.info(" - Added client %s. %s", k, b)
568

    
569
    def remove_tap(self, tap):
570
        """ Cleanup clients on a removed interface
571

    
572
        """
573
        try:
574
            for k, cl in self.clients.items():
575
                if cl.tap == tap:
576
                    cl.socket.close()
577
                    del self.clients[k]
578
                    logging.info("Removed client %s. %s", k, cl)
579
        except:
580
            logging.debug("Client on %s disappeared!!!", tap)
581

    
582

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

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

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

    
606
        # Server responses are always BOOTREPLYs
607
        resp.op = "BOOTREPLY"
608
        del resp.payload
609

    
610
        indev = get_indev(payload)
611

    
612
        binding = self.get_binding(indev, mac)
613
        if binding is None:
614
            # We don't know anything about this interface, so accept the packet
615
            # and return an let the kernel handle it
616
            payload.set_verdict(nfqueue.NF_ACCEPT)
617
            return
618

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

    
622
        if mac != binding.mac:
623
            logging.warn(" - DHCP: Recieved spoofed request from %s (and not %s)",
624
                         mac, binding)
625
            return
626

    
627
        if not binding.ip:
628
            logging.info(" - DHCP: No IP found in binding file %s.", binding)
629
            return
630

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

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

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

    
648
        logging.info(" - DHCP: %s from %s",
649
                     DHCP_TYPES.get(req_type, "UNKNOWN"), binding)
650

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

    
656
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
657
            resp_type = DHCPNAK
658
            logging.info(" - DHCP: Sending DHCPNAK to %s (because requested %s)",
659
                         binding, requested_addr)
660

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

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

    
684
        elif req_type == DHCPRELEASE:
685
            # Log and ignore
686
            logging.info(" - DHCP: DHCPRELEASE from %s", binding)
687
            return
688

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

    
697
        logging.info(" - RESPONSE: %s for %s", DHCP_TYPES[resp_type], binding)
698
        try:
699
            binding.sendp(resp)
700
        except socket.error, e:
701
            logging.warn(" - DHCP: Response on %s failed: %s", binding, str(e))
702
        except Exception, e:
703
            logging.warn(" - DHCP: Unkown error during response on %s: %s",
704
                         binding, str(e))
705

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

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

    
720
        #TODO: figure out how to find the src mac
721
        mac = None
722
        binding = self.get_binding(indev, mac)
723
        if binding is None:
724
            # We don't know anything about this interface, so accept the packet
725
            # and return and let the kernel handle it
726
            payload.set_verdict(nfqueue.NF_ACCEPT)
727
            return
728

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

    
732
        subnet = binding.net6
733

    
734
        if subnet.net is None:
735
            logging.debug(" - DHCPv6: No IPv6 network assigned to %s", binding)
736
            return
737

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

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

    
747
        if self.dhcpv6_domains:
748
            domains = self.dhcpv6_domains
749
        else:
750
            domains = [binding.hostname.split('.', 1)[-1]]
751

    
752
        # We do this in order not to caclulate optlen ourselves
753
        dnsdomains = str(DHCP6OptDNSDomains(dnsdomains=domains))
754
        dnsservers = str(DHCP6OptDNSServers(dnsservers=self.ipv6_nameservers))
755

    
756
        resp = Ether(src=indevmac, dst=binding.mac)/\
757
               IPv6(tc=192, src=str(ifll), dst=str(ofll))/\
758
               UDP(sport=pkt.dport, dport=pkt.sport)/\
759
               DHCP6_Reply(trid=pkt[DHCP6_InfoRequest].trid)/\
760
               DHCP6OptClientId(duid=pkt[DHCP6OptClientId].duid)/\
761
               DHCP6OptServerId(duid=DUID_LLT(lladdr=indevmac, timeval=time.time()))/\
762
               DHCP6OptDNSDomains(dnsdomains)/\
763
               DHCP6OptDNSServers(dnsservers)
764

    
765
        logging.info(" - RESPONSE: DHCPv6 reply for %s", binding)
766

    
767
        try:
768
            binding.sendp(resp)
769
        except socket.error, e:
770
            logging.warn(" - DHCPv6: Response on %s failed: %s",
771
                         binding, str(e))
772
        except Exception, e:
773
            logging.warn(" - DHCPv6: Unkown error during response on %s: %s",
774
                         binding, str(e))
775

    
776

    
777
    def rs_response(self, arg1, arg2=None):  # pylint: disable=W0613
778
        """ Generate a reply to an ICMPv6 router solicitation
779

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

    
798
        indev = get_indev(payload)
799

    
800
        binding = self.get_binding(indev, mac)
801
        if binding is None:
802
            # We don't know anything about this interface, so accept the packet
803
            # and return and let the kernel handle it
804
            payload.set_verdict(nfqueue.NF_ACCEPT)
805
            return
806

    
807
        # Signal the kernel that it shouldn't further process the packet
808
        payload.set_verdict(nfqueue.NF_DROP)
809

    
810
        if mac != binding.mac:
811
            logging.warn(" - RS: Received spoofed request from %s (and not %s)",
812
                         mac, binding)
813
            return
814

    
815
        subnet = binding.net6
816

    
817
        if subnet.net is None:
818
            logging.debug(" - RS: No IPv6 network assigned to %s", binding)
819
            return
820

    
821
        indevmac = self.get_iface_hw_addr(binding.indev)
822
        ifll = subnet.make_ll64(indevmac)
823
        if ifll is None:
824
            return
825

    
826
        resp = Ether(src=indevmac)/\
827
               IPv6(src=str(ifll))/ICMPv6ND_RA(O=1, routerlifetime=14400)/\
828
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
829
                                     prefixlen=subnet.prefixlen)
830

    
831
        if self.ipv6_nameservers:
832
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
833
                                     lifetime=self.ra_period * 3)
834

    
835
        logging.info(" - RESPONSE: RA for %s", binding)
836

    
837
        try:
838
            binding.sendp(resp)
839
        except socket.error, e:
840
            logging.warn(" - RS: RA failed on %s: %s",
841
                         binding, str(e))
842
        except Exception, e:
843
            logging.warn(" - RS: Unkown error during RA on %s: %s",
844
                         binding, str(e))
845

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

    
849
        """
850

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

    
861
        ns = IPv6(payload.get_data())
862
        #logging.debug(ns.show())
863
        try:
864
            mac = ns.lladdr
865
        except:
866
            logging.debug(" - NS: Cannot obtain lladdr")
867
            return
868

    
869

    
870
        indev = get_indev(payload)
871

    
872
        binding = self.get_binding(indev, mac)
873
        if binding is None:
874
            # We don't know anything about this interface, so accept the packet
875
            # and return and let the kernel handle it
876
            payload.set_verdict(nfqueue.NF_ACCEPT)
877
            return
878

    
879
        payload.set_verdict(nfqueue.NF_DROP)
880

    
881
        if mac != binding.mac:
882
            logging.warn(" - NS: Received spoofed request from %s (and not %s)",
883
                         mac, binding)
884
            return
885

    
886
        subnet = binding.net6
887
        if subnet.net is None:
888
            logging.debug(" - NS: No IPv6 network assigned to %s", binding)
889
            return
890

    
891
        indevmac = self.get_iface_hw_addr(binding.indev)
892

    
893
        ifll = subnet.make_ll64(indevmac)
894
        if ifll is None:
895
            return
896

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

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

    
906
        logging.info(" - RESPONSE: NA for %s ", binding)
907

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

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

    
922
    def _send_periodic_ra(self):
923
        logging.info(" * Periodic RA: Starting...")
924
        start = time.time()
925
        i = 0
926
        for binding in self.clients.values():
927
            tap = binding.tap
928
            indev = binding.indev
929
            # mac = binding.mac
930
            subnet = binding.net6
931
            if subnet.net is None:
932
                logging.debug(" - Periodic RA: Skipping %s", binding)
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(O=1, 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
            logging.info(" - RESPONSE: NA for %s ", binding)
946
            try:
947
                binding.sendp(resp)
948
            except socket.error, e:
949
                logging.warn(" - Periodic RA: Failed on %s: %s",
950
                             binding, str(e))
951
            except Exception, e:
952
                logging.warn(" - Periodic RA: Unkown error on %s: %s",
953
                             binding, str(e))
954
            i += 1
955
        logging.info(" - Periodic RA: Sent %d RAs in %.2f seconds", i, time.time() - start)
956

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

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

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

    
969
        """
970
        self.build_config()
971

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

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

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

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

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

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

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

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

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

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

    
1034

    
1035

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

    
1043
    import validate
1044

    
1045
    validator = validate.Validator()
1046

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

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

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

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

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

    
1080
    opts, args = parser.parse_args()
1081

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
1196
    logging.info("Ready to serve requests")
1197

    
1198

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

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

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

    
1215

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