Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ d7b852fd

History | View | Annotate | Download (33.5 kB)

1
#!/usr/bin/env python
2
#
3

    
4
# nfdcpd: A promiscuous, NFQUEUE-based DHCP server for virtual machine hosting
5
# Copyright (c) 2010 GRNET SA
6
#
7
#    This program is free software; you can redistribute it and/or modify
8
#    it under the terms of the GNU General Public License as published by
9
#    the Free Software Foundation; either version 2 of the License, or
10
#    (at your option) any later version.
11
#
12
#    This program is distributed in the hope that it will be useful,
13
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
#    GNU General Public License for more details.
16
#
17
#    You should have received a copy of the GNU General Public License along
18
#    with this program; if not, write to the Free Software Foundation, Inc.,
19
#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
#
21

    
22
import os
23
import re
24
import sys
25
import glob
26
import time
27
import logging
28
import logging.handlers
29
import threading
30
import traceback
31
import subprocess
32

    
33
import daemon
34
import daemon.runner
35
import daemon.pidlockfile
36
import nfqueue
37
import pyinotify
38
from lockfile import LockTimeout
39

    
40
import IPy
41
import socket
42
from select import select
43
from socket import AF_INET, AF_INET6
44

    
45
from scapy.data import ETH_P_ALL
46
from scapy.packet import BasePacket
47
from scapy.layers.l2 import Ether
48
from scapy.layers.inet import IP, UDP
49
from scapy.layers.inet6 import IPv6, ICMPv6ND_RA, ICMPv6ND_NA, \
50
                               ICMPv6NDOptDstLLAddr, \
51
                               ICMPv6NDOptPrefixInfo, \
52
                               ICMPv6NDOptRDNSS
53
from scapy.layers.dhcp import BOOTP, DHCP
54

    
55
DEFAULT_CONFIG = "/etc/nfdhcpd/nfdhcpd.conf"
56
DEFAULT_PATH = "/var/run/ganeti-dhcpd"
57
DEFAULT_USER = "nobody"
58
DEFAULT_LEASE_LIFETIME = 604800 # 1 week
59
DEFAULT_LEASE_RENEWAL = 600  # 10 min
60
DEFAULT_RA_PERIOD = 300 # seconds
61
DHCP_DUMMY_SERVER_IP = "1.2.3.4"
62

    
63
LOG_FILENAME = "nfdhcpd.log"
64

    
65
SYSFS_NET = "/sys/class/net"
66

    
67
LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
68

    
69
# Configuration file specification (see configobj documentation)
70
CONFIG_SPEC = """
71
[general]
72
pidfile = string()
73
datapath = string()
74
logdir = string()
75
user = string()
76

    
77
[dhcp]
78
enable_dhcp = boolean(default=True)
79
lease_lifetime = integer(min=0, max=4294967295)
80
lease_renewal = integer(min=0, max=4294967295)
81
server_ip = ip_addr()
82
dhcp_queue = integer(min=0, max=65535)
83
nameservers = ip_addr_list(family=4)
84
domain = string(default=None)
85

    
86
[ipv6]
87
enable_ipv6 = boolean(default=True)
88
ra_period = integer(min=1, max=4294967295)
89
rs_queue = integer(min=0, max=65535)
90
ns_queue = integer(min=0, max=65535)
91
nameservers = ip_addr_list(family=6)
92
"""
93

    
94

    
95
DHCPDISCOVER = 1
96
DHCPOFFER = 2
97
DHCPREQUEST = 3
98
DHCPDECLINE = 4
99
DHCPACK = 5
100
DHCPNAK = 6
101
DHCPRELEASE = 7
102
DHCPINFORM = 8
103

    
104
DHCP_TYPES = {
105
    DHCPDISCOVER: "DHCPDISCOVER",
106
    DHCPOFFER: "DHCPOFFER",
107
    DHCPREQUEST: "DHCPREQUEST",
108
    DHCPDECLINE: "DHCPDECLINE",
109
    DHCPACK: "DHCPACK",
110
    DHCPNAK: "DHCPNAK",
111
    DHCPRELEASE: "DHCPRELEASE",
112
    DHCPINFORM: "DHCPINFORM",
113
}
114

    
115
DHCP_REQRESP = {
116
    DHCPDISCOVER: DHCPOFFER,
117
    DHCPREQUEST: DHCPACK,
118
    DHCPINFORM: DHCPACK,
119
    }
120

    
121

    
122
def get_indev(payload):
123
    try:
124
        indev_ifindex = payload.get_physindev()
125
        logging.debug("get_physindev %s", indev_ifindex)
126
        if indev_ifindex:
127
            logging.debug("Incomming packet from bridge %s", indev_ifindex)
128
            return indev_ifindex
129
    except AttributeError:
130
        #TODO: return error value
131
        logging.debug("No get_physindev supported")
132
        return 0
133

    
134
    indev_ifindex = payload.get_indev()
135
    logging.debug("Incomming packet from tap %s", indev_ifindex)
136

    
137
    return indev_ifindex
138

    
139
def get_binding(proxy, ifindex, mac):
140
    try:
141
        if proxy.mac_indexed_clients:
142
            logging.debug("get_binding for mac %s", mac)
143
            b = proxy.clients[mac]
144
        else:
145
            logging.debug("get_binding for ifindex %s", ifindex)
146
            b = proxy.clients[ifindex]
147
        return b
148
    except KeyError:
149
        logging.debug("No client found for mac/ifindex %s/%s", mac, ifindex)
150
        return None
151

    
152
def parse_binding_file(path):
153
    """ Read a client configuration from a tap file
154

    
155
    """
156
    try:
157
        iffile = open(path, 'r')
158
    except EnvironmentError, e:
159
        logging.warn("Unable to open binding file %s: %s", path, str(e))
160
        return None
161

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

    
173
    def get_value(line):
174
        v = line.strip().split('=')[1]
175
        if v == '':
176
          return None
177
        return v
178

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

    
199
    try:
200
        client = Client(tap=tap, mac=mac, ip=ip,
201
                  hostname=hostname, indev=indev, subnet=subnet,
202
                  gateway=gateway, subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
203
        return client
204
    except:
205
        return None
206

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

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

    
215
        Currently this removes an interface from the watch list
216

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

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

    
223
        Currently this adds an interface to the watch list
224

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

    
228

    
229
class Client(object):
230
    def __init__(self, tap=None, indev=None, mac=None, ip=None, hostname=None,
231
                 subnet=None, gateway=None, 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

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

    
249

    
250
class Subnet(object):
251
    def __init__(self, net=None, gw=None, dev=None):
252
        if isinstance(net, str):
253
            try:
254
                self.net = IPy.IP(net)
255
            except:
256
                raise Exception
257
        else:
258
            self.net = net
259
        self.gw = gw
260
        self.dev = dev
261

    
262
    @property
263
    def netmask(self):
264
        """ Return the netmask in textual representation
265

    
266
        """
267
        return str(self.net.netmask())
268

    
269
    @property
270
    def broadcast(self):
271
        """ Return the broadcast address in textual representation
272

    
273
        """
274
        return str(self.net.broadcast())
275

    
276
    @property
277
    def prefix(self):
278
        """ Return the network as an IPy.IP
279

    
280
        """
281
        return self.net.net()
282

    
283
    @property
284
    def prefixlen(self):
285
        """ Return the prefix length as an integer
286

    
287
        """
288
        return self.net.prefixlen()
289

    
290
    @staticmethod
291
    def _make_eui64(net, mac):
292
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
293

    
294
        """
295
        if mac is None:
296
            return None
297
        comp = mac.split(":")
298
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
299
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
300
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
301
        for l in range(0, len(eui64), 2):
302
            prefix += ["".join(eui64[l:l+2])]
303
        return IPy.IP(":".join(prefix))
304

    
305
    def make_eui64(self, mac):
306
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
307
        subnet.
308

    
309
        """
310
        return self._make_eui64(self.net, mac)
311

    
312
    def make_ll64(self, mac):
313
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
314

    
315
        """
316
        return self._make_eui64("fe80::", mac)
317

    
318

    
319
class VMNetProxy(object): # pylint: disable=R0902
320
    def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
321
                 rs_queue_num=None, ns_queue_num=None,
322
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
323
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
324
                 dhcp_domain='',
325
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
326
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
327

    
328
        try:
329
            getattr(nfqueue.payload, 'get_physindev')
330
            self.mac_indexed_clients = False
331
        except AttributeError:
332
            self.mac_indexed_clients = True
333
        self.data_path = data_path
334
        self.lease_lifetime = dhcp_lease_lifetime
335
        self.lease_renewal = dhcp_lease_renewal
336
        self.dhcp_domain = dhcp_domain
337
        self.dhcp_server_ip = dhcp_server_ip
338
        self.ra_period = ra_period
339
        if dhcp_nameservers is None:
340
            self.dhcp_nameserver = []
341
        else:
342
            self.dhcp_nameservers = dhcp_nameservers
343

    
344
        if ipv6_nameservers is None:
345
            self.ipv6_nameservers = []
346
        else:
347
            self.ipv6_nameservers = ipv6_nameservers
348

    
349
        self.ipv6_enabled = False
350

    
351
        self.clients = {}
352
        #self.subnets = {}
353
        #self.ifaces = {}
354
        #self.v6nets = {}
355
        self.nfq = {}
356
        self.l2socket = socket.socket(socket.AF_PACKET,
357
                                      socket.SOCK_RAW, ETH_P_ALL)
358
        self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
359

    
360
        # Inotify setup
361
        self.wm = pyinotify.WatchManager()
362
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
363
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
364
        inotify_handler = ClientFileHandler(self)
365
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
366
        self.wm.add_watch(self.data_path, mask, rec=True)
367

    
368
        # NFQUEUE setup
369
        if dhcp_queue_num is not None:
370
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
371

    
372
        if rs_queue_num is not None:
373
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
374
            self.ipv6_enabled = True
375

    
376
        if ns_queue_num is not None:
377
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
378
            self.ipv6_enabled = True
379

    
380
    def _cleanup(self):
381
        """ Free all resources for a graceful exit
382

    
383
        """
384
        logging.info("Cleaning up")
385

    
386
        logging.debug("Closing netfilter queues")
387
        for q in self.nfq.values():
388
            q.close()
389

    
390
        logging.debug("Closing socket")
391
        self.l2socket.close()
392

    
393
        logging.debug("Stopping inotify watches")
394
        self.notifier.stop()
395

    
396
        logging.info("Cleanup finished")
397

    
398
    def _setup_nfqueue(self, queue_num, family, callback):
399
        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
400
                      queue_num, family)
401
        q = nfqueue.queue()
402
        q.set_callback(callback)
403
        q.fast_open(queue_num, family)
404
        q.set_queue_maxlen(5000)
405
        # This is mandatory for the queue to operate
406
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
407
        self.nfq[q.get_fd()] = q
408

    
409
    def sendp(self, data, dev):
410
        """ Send a raw packet using a layer-2 socket
411

    
412
        """
413
        logging.debug("%s", data)
414
        if isinstance(data, BasePacket):
415
            data = str(data)
416

    
417
        self.l2socket.bind((dev, ETH_P_ALL))
418
        count = self.l2socket.send(data)
419
        ldata = len(data)
420
        if count != ldata:
421
            logging.warn("Truncated send on %s (%d/%d bytes sent)",
422
                         dev, count, ldata)
423

    
424
    def build_config(self):
425
        self.clients.clear()
426

    
427
        for path in glob.glob(os.path.join(self.data_path, "*")):
428
            self.add_tap(path)
429

    
430
        logging.debug("\n\n\n\n\n")
431
        logging.debug("%10s %20s %7s %15s", 'Client', 'MAC', 'TAP', 'IP')
432
        for b in self.clients.values():
433
            logging.debug("%10s %20s %7s %15s", b.hostname, b.mac, b.tap, b.ip)
434

    
435
    def get_ifindex(self, iface):
436
        """ Get the interface index from sysfs
437

    
438
        """
439
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
440
        if not path.startswith(SYSFS_NET):
441
            return None
442

    
443
        ifindex = None
444

    
445
        try:
446
            f = open(path, 'r')
447
        except EnvironmentError:
448
            logging.debug("%s is probably down, removing", iface)
449
            self.remove_tap(iface)
450

    
451
            return ifindex
452

    
453
        try:
454
            ifindex = f.readline().strip()
455
            try:
456
                ifindex = int(ifindex)
457
            except ValueError, e:
458
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
459
                             " output '%s'", iface, ifindex)
460
        except EnvironmentError, e:
461
            logging.warn("Error reading %s's ifindex from sysfs: %s",
462
                         iface, str(e))
463
            self.remove_tap(iface)
464
        finally:
465
            f.close()
466

    
467
        return ifindex
468

    
469

    
470
    def get_iface_hw_addr(self, iface):
471
        """ Get the interface hardware address from sysfs
472

    
473
        """
474
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
475
        if not path.startswith(SYSFS_NET):
476
            return None
477

    
478
        addr = None
479
        try:
480
            f = open(path, 'r')
481
        except EnvironmentError:
482
            logging.debug("%s is probably down, removing", iface)
483
            self.remove_tap(iface)
484
            return addr
485

    
486
        try:
487
            addr = f.readline().strip()
488
        except EnvironmentError, e:
489
            logging.warn("Failed to read hw address for %s from sysfs: %s",
490
                         iface, str(e))
491
        finally:
492
            f.close()
493

    
494
        return addr
495

    
496
    def add_tap(self, path):
497
        """ Add an interface to monitor
498

    
499
        """
500
        tap = os.path.basename(path)
501

    
502
        logging.debug("Updating configuration for %s", tap)
503
        b = parse_binding_file(path)
504
        if b is None:
505
            return
506
        ifindex = self.get_ifindex(b.tap)
507

    
508
        if ifindex is None:
509
            logging.warn("Stale configuration for %s found", tap)
510
        else:
511
            if b.is_valid():
512
                if self.mac_indexed_clients:
513
                    self.clients[b.mac] = b
514
                else:
515
                    self.clients[ifindex] = b
516
                logging.debug("Added client:")
517
                logging.debug("%5s: %10s %20s %7s %15s",
518
                               ifindex, b.hostname, b.mac, b.tap, b.ip)
519

    
520
    def remove_tap(self, tap):
521
        """ Cleanup clients on a removed interface
522

    
523
        """
524
        try:
525
            for k in self.clients.keys():
526
                b = self.clients[k]
527
                if self.clients[k].tap == tap:
528
                    logging.debug("Removing client on interface %s", tap)
529
                    logging.debug("%10s %20s %7s %15s",
530
                                  b.hostname, b.mac, b.tap, b.ip)
531
                    del self.clients[k]
532
        except:
533
            logging.debug("Client on %s disappeared!!!", tap)
534

    
535

    
536
    def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
537
        """ Generate a reply to bnetfilter-queue-deva BOOTP/DHCP request
538

    
539
        """
540
        # Decode the response - NFQUEUE relays IP packets
541
        pkt = IP(payload.get_data())
542
        logging.debug("IN DHCP RESPONCE")
543
        #logging.debug(pkt.show())
544

    
545
        # Get the client MAC address
546
        resp = pkt.getlayer(BOOTP).copy()
547
        hlen = resp.hlen
548
        mac = resp.chaddr[:hlen].encode("hex")
549
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
550

    
551
        # Server responses are always BOOTREPLYs
552
        resp.op = "BOOTREPLY"
553
        del resp.payload
554

    
555
        indev = get_indev(payload)
556

    
557
        binding = get_binding(self, indev, mac)
558
        if binding is None:
559
            # We don't know anything about this interface, so accept the packet
560
            # and return
561
            logging.debug("Ignoring DHCP request on unknown iface %d", indev)
562
            # We don't know what to do with this packet, so let the kernel
563
            # handle it
564
            payload.set_verdict(nfqueue.NF_ACCEPT)
565
            return
566

    
567

    
568
        # Signal the kernel that it shouldn't further process the packet
569
        payload.set_verdict(nfqueue.NF_DROP)
570

    
571
        if mac != binding.mac:
572
            logging.warn("Recieved spoofed DHCP request for mac %s from tap %s", mac, indev)
573
            return
574

    
575
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
576
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
577
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
578
        subnet = binding.net
579

    
580
        if not DHCP in pkt:
581
            logging.warn("Invalid request from %s on %s, no DHCP"
582
                         " payload found", binding.mac, binding.tap)
583
            return
584

    
585
        dhcp_options = []
586
        requested_addr = binding.ip
587
        for opt in pkt[DHCP].options:
588
            if type(opt) is tuple and opt[0] == "message-type":
589
                req_type = opt[1]
590
            if type(opt) is tuple and opt[0] == "requested_addr":
591
                requested_addr = opt[1]
592

    
593
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
594
                     binding.mac, binding.tap)
595

    
596
        if self.dhcp_domain:
597
            domainname = self.dhcp_domain
598
        else:
599
            domainname = binding.hostname.split('.', 1)[-1]
600

    
601
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
602
            resp_type = DHCPNAK
603
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
604
                         " instead of %s", binding.mac, binding.tap,
605
                         requested_addr, binding.ip)
606

    
607
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
608
            resp_type = DHCP_REQRESP[req_type]
609
            resp.yiaddr = binding.ip
610
            dhcp_options += [
611
                 ("hostname", binding.hostname),
612
                 ("domain", domainname),
613
                 ("broadcast_address", str(subnet.broadcast)),
614
                 ("subnet_mask", str(subnet.netmask)),
615
                 ("renewal_time", self.lease_renewal),
616
                 ("lease_time", self.lease_lifetime),
617
            ]
618
            if subnet.gw:
619
              dhcp_options += [("router", subnet.gw)]
620
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
621

    
622
        elif req_type == DHCPINFORM:
623
            resp_type = DHCP_REQRESP[req_type]
624
            dhcp_options += [
625
                 ("hostname", binding.hostname),
626
                 ("domain", domainname),
627
            ]
628
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
629

    
630
        elif req_type == DHCPRELEASE:
631
            # Log and ignore
632
            logging.info("DHCPRELEASE from %s on %s", binding.mac, binding.tap )
633
            return
634

    
635
        # Finally, always add the server identifier and end options
636
        dhcp_options += [
637
            ("message-type", resp_type),
638
            ("server_id", DHCP_DUMMY_SERVER_IP),
639
            "end"
640
        ]
641
        resp /= DHCP(options=dhcp_options)
642

    
643
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
644
                     binding.ip, binding.tap)
645
        try:
646
            self.sendp(resp, binding.indev)
647
        except socket.error, e:
648
            logging.warn("DHCP response on %s failed: %s", binding.indev, str(e))
649
        except Exception, e:
650
            logging.warn("Unkown error during DHCP response on %s: %s",
651
                         binding.indev, str(e))
652

    
653
    def rs_response(self, i, payload): # pylint: disable=W0613
654
        """ Generate a reply to a BOOTP/DHCP request
655

    
656
        """
657
        pkt = IPv6(payload.get_data())
658
        logging.debug("IN RS RESPONCE")
659
        #logging.debug(pkt.show())
660
        try:
661
            mac = pkt.lladdr
662
        except:
663
            logging.debug("Cannot obtain lladdr in rs")
664
            return
665

    
666
        logging.debug("rs for mac %s", mac)
667

    
668
        indev = get_indev(payload)
669

    
670
        binding = get_binding(self, indev, mac)
671
        if binding is None:
672
            # We don't know anything about this interface, so accept the packet
673
            # and return
674
            logging.debug("Ignoring router solicitation on for mac %s", mac)
675
            # We don't know what to do with this packet, so let the kernel
676
            # handle it
677
            payload.set_verdict(nfqueue.NF_ACCEPT)
678
            return
679

    
680
        # Signal the kernel that it shouldn't further process the packet
681
        payload.set_verdict(nfqueue.NF_DROP)
682

    
683
        if mac != binding.mac:
684
            logging.warn("Recieved spoofed RS request for mac %s from tap %s", mac, tap)
685
            return
686

    
687
        subnet = binding.net6
688

    
689
        if subnet.net is None:
690
          logging.debug("No IPv6 network assigned for the interface")
691
          return
692

    
693
        indevmac = self.get_iface_hw_addr(binding.indev)
694
        ifll = subnet.make_ll64(indevmac)
695
        if ifll is None:
696
            return
697

    
698

    
699
        resp = Ether(src=indevmac)/\
700
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
701
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
702
                                     prefixlen=subnet.prefixlen)
703

    
704
        if self.ipv6_nameservers:
705
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
706
                                     lifetime=self.ra_period * 3)
707

    
708
        logging.info("RA on %s for %s", binding.indev, subnet.net)
709
        try:
710
            self.sendp(resp, binding.indev)
711
        except socket.error, e:
712
            logging.warn("RA on %s failed: %s", binding.indev, str(e))
713
        except Exception, e:
714
            logging.warn("Unkown error during RA on %s: %s",
715
                         binding.indev, str(e))
716

    
717
    def ns_response(self, i, payload): # pylint: disable=W0613
718
        """ Generate a reply to an ICMPv6 neighbor solicitation
719

    
720
        """
721
        ns = IPv6(payload.get_data())
722
        logging.debug("IN NS RESPONCE")
723
        #logging.debug(ns.show())
724
        try:
725
            mac = ns.lladdr
726
        except:
727
            logging.debug("Cannot obtain lladdr from ns")
728
            return
729

    
730
        logging.debug("dst %s  tgt %s" , ns.dst, ns.tgt)
731

    
732
        indev = get_indev(payload)
733

    
734
        binding = get_binding(self, indev, mac)
735
        if binding is None:
736
            # We don't know anything about this interface, so accept the packet
737
            # and return
738
            logging.debug("Ignoring neighbour solicitation for eui64 %s", ns.tgt)
739
            # We don't know what to do with this packet, so let the kernel
740
            # handle it
741
            payload.set_verdict(nfqueue.NF_ACCEPT)
742
            return
743

    
744
        payload.set_verdict(nfqueue.NF_DROP)
745

    
746
        if mac != binding.mac:
747
            logging.warn("Recieved spoofed NS request for mac %s from tap %s", mac, tap)
748
            return
749

    
750
        subnet = binding.net6
751
        if subnet.net is None:
752
          logging.debug("No IPv6 network assigned for the interface")
753
          return
754

    
755
        indevmac = self.get_iface_hw_addr(binding.indev)
756

    
757
        ifll = subnet.make_ll64(indevmac)
758
        if ifll is None:
759
            return
760

    
761
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
762
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
763
            payload.set_verdict(nfqueue.NF_ACCEPT)
764
            return 1
765

    
766
        logging.debug("na ether %s %s", binding.mac, ns.src)
767
        resp = Ether(src=indevmac, dst=binding.mac)/\
768
               IPv6(src=str(ifll), dst=ns.src)/\
769
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
770
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
771

    
772
        logging.info("NA on %s for %s", binding.indev, ns.tgt)
773
        try:
774
            self.sendp(resp, binding.indev)
775
        except socket.error, e:
776
            logging.warn("NA on %s failed: %s", binding.indev, str(e))
777
        except Exception, e:
778
            logging.warn("Unkown error during periodic NA on %s: %s",
779
                         binding.indev, str(e))
780

    
781
    def send_periodic_ra(self):
782
        # Use a separate thread as this may take a _long_ time with
783
        # many interfaces and we want to be responsive in the mean time
784
        threading.Thread(target=self._send_periodic_ra).start()
785

    
786
    def _send_periodic_ra(self):
787
        logging.debug("Sending out periodic RAs")
788
        start = time.time()
789
        i = 0
790
        for binding in self.clients.values():
791
            tap = binding.tap
792
            indev = binding.indev
793
            mac = binding.mac
794
            subnet = binding.net6
795
            if subnet.net is None:
796
                logging.debug("Skipping periodic RA on interface %s,"
797
                              " as it is not IPv6-connected", tap)
798
                continue
799
            indevmac = self.get_iface_hw_addr(indev)
800
            ifll = subnet.make_ll64(indevmac)
801
            if ifll is None:
802
                continue
803
            resp = Ether(src=indevmac)/\
804
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
805
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
806
                                         prefixlen=subnet.prefixlen)
807
            if self.ipv6_nameservers:
808
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
809
                                         lifetime=self.ra_period * 3)
810
            try:
811
                self.sendp(resp, indev)
812
            except socket.error, e:
813
                logging.warn("Periodic RA on %s failed: %s", tap, str(e))
814
            except Exception, e:
815
                logging.warn("Unkown error during periodic RA on %s: %s",
816
                             tap, str(e))
817
            i += 1
818
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
819

    
820
    def serve(self):
821
        """ Safely perform the main loop, freeing all resources upon exit
822

    
823
        """
824
        try:
825
            self._serve()
826
        finally:
827
            self._cleanup()
828

    
829
    def _serve(self):
830
        """ Loop forever, serving DHCP requests
831

    
832
        """
833
        self.build_config()
834

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

    
839
        start = time.time()
840
        if self.ipv6_enabled:
841
            timeout = self.ra_period
842
            self.send_periodic_ra()
843
        else:
844
            timeout = None
845

    
846
        while True:
847
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
848
            if xlist:
849
                logging.warn("Warning: Exception on %s",
850
                             ", ".join([ str(fd) for fd in xlist]))
851

    
852
            if rlist:
853
                if iwfd in rlist:
854
                # First check if there are any inotify (= configuration change)
855
                # events
856
                    self.notifier.read_events()
857
                    self.notifier.process_events()
858
                    rlist.remove(iwfd)
859

    
860
                for fd in rlist:
861
                    try:
862
                        self.nfq[fd].process_pending()
863
                    except RuntimeError, e:
864
                        logging.warn("Error processing fd %d: %s", fd, str(e))
865
                    except Exception, e:
866
                        logging.warn("Unknown error processing fd %d: %s",
867
                                     fd, str(e))
868

    
869
            if self.ipv6_enabled:
870
                # Calculate the new timeout
871
                timeout = self.ra_period - (time.time() - start)
872

    
873
                if timeout <= 0:
874
                    start = time.time()
875
                    self.send_periodic_ra()
876
                    timeout = self.ra_period - (time.time() - start)
877

    
878

    
879
if __name__ == "__main__":
880
    import capng
881
    import optparse
882
    from cStringIO import StringIO
883
    from pwd import getpwnam, getpwuid
884
    from configobj import ConfigObj, ConfigObjError, flatten_errors
885

    
886
    import validate
887

    
888
    validator = validate.Validator()
889

    
890
    def is_ip_list(value, family=4):
891
        try:
892
            family = int(family)
893
        except ValueError:
894
            raise validate.VdtParamError(family)
895
        if isinstance(value, (str, unicode)):
896
            value = [value]
897
        if not isinstance(value, list):
898
            raise validate.VdtTypeError(value)
899

    
900
        for entry in value:
901
            try:
902
                ip = IPy.IP(entry)
903
            except ValueError:
904
                raise validate.VdtValueError(entry)
905

    
906
            if ip.version() != family:
907
                raise validate.VdtValueError(entry)
908
        return value
909

    
910
    validator.functions["ip_addr_list"] = is_ip_list
911
    config_spec = StringIO(CONFIG_SPEC)
912

    
913

    
914
    parser = optparse.OptionParser()
915
    parser.add_option("-c", "--config", dest="config_file",
916
                      help="The location of the data files", metavar="FILE",
917
                      default=DEFAULT_CONFIG)
918
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
919
                      help="Turn on debugging messages")
920
    parser.add_option("-f", "--foreground", action="store_false",
921
                      dest="daemonize", default=True,
922
                      help="Do not daemonize, stay in the foreground")
923

    
924

    
925
    opts, args = parser.parse_args()
926

    
927
    try:
928
        config = ConfigObj(opts.config_file, configspec=config_spec)
929
    except ConfigObjError, err:
930
        sys.stderr.write("Failed to parse config file %s: %s" %
931
                         (opts.config_file, str(err)))
932
        sys.exit(1)
933

    
934
    results = config.validate(validator)
935
    if results != True:
936
        logging.fatal("Configuration file validation failed! See errors below:")
937
        for (section_list, key, unused) in flatten_errors(config, results):
938
            if key is not None:
939
                logging.fatal(" '%s' in section '%s' failed validation",
940
                              key, ", ".join(section_list))
941
            else:
942
                logging.fatal(" Section '%s' is missing",
943
                              ", ".join(section_list))
944
        sys.exit(1)
945

    
946
    logger = logging.getLogger()
947
    if opts.debug:
948
        logger.setLevel(logging.DEBUG)
949
    else:
950
        logger.setLevel(logging.INFO)
951

    
952
    if opts.daemonize:
953
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
954
        handler = logging.handlers.RotatingFileHandler(logfile,
955
                                                       maxBytes=2097152)
956
    else:
957
        handler = logging.StreamHandler()
958

    
959
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
960
    logger.addHandler(handler)
961

    
962
    if opts.daemonize:
963
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
964
            config["general"]["pidfile"], 10)
965
        # Remove any stale PID files, left behind by previous invocations
966
        if daemon.runner.is_pidfile_stale(pidfile):
967
            logger.warning("Removing stale PID lock file %s", pidfile.path)
968
            pidfile.break_lock()
969

    
970
        d = daemon.DaemonContext(pidfile=pidfile,
971
                                 umask=0022,
972
                                 stdout=handler.stream,
973
                                 stderr=handler.stream,
974
                                 files_preserve=[handler.stream])
975
        try:
976
            d.open()
977
        except (daemon.pidlockfile.AlreadyLocked, LockTimeout):
978
            logger.critical("Failed to lock pidfile %s,"
979
                            " another instance running?", pidfile.path)
980
            sys.exit(1)
981

    
982
    logging.info("Starting up")
983

    
984
    proxy_opts = {}
985
    if config["dhcp"].as_bool("enable_dhcp"):
986
        proxy_opts.update({
987
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
988
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
989
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
990
            "dhcp_server_ip": config["dhcp"]["server_ip"],
991
            "dhcp_nameservers": config["dhcp"]["nameservers"],
992
            "dhcp_domain": config["dhcp"]["domain"],
993
        })
994

    
995
    if config["ipv6"].as_bool("enable_ipv6"):
996
        proxy_opts.update({
997
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
998
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
999
            "ra_period": config["ipv6"].as_int("ra_period"),
1000
            "ipv6_nameservers": config["ipv6"]["nameservers"],
1001
        })
1002

    
1003
    # pylint: disable=W0142
1004
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
1005

    
1006
    # Drop all capabilities except CAP_NET_RAW and change uid
1007
    try:
1008
        uid = getpwuid(config["general"].as_int("user"))
1009
    except ValueError:
1010
        uid = getpwnam(config["general"]["user"])
1011

    
1012
    logging.debug("Setting capabilities and changing uid")
1013
    logging.debug("User: %s, uid: %d, gid: %d",
1014
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
1015

    
1016
    # Keep only the capabilities we need
1017
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
1018
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
1019
    capng.capng_update(capng.CAPNG_ADD,
1020
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
1021
                       capng.CAP_NET_ADMIN)
1022
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
1023
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
1024

    
1025
    logging.info("Ready to serve requests")
1026
    try:
1027
        proxy.serve()
1028
    except Exception:
1029
        if opts.daemonize:
1030
            exc = "".join(traceback.format_exception(*sys.exc_info()))
1031
            logging.critical(exc)
1032
        raise
1033

    
1034

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