Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ d2b16e51

History | View | Annotate | Download (29.2 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.pidlockfile
35
import nfqueue
36
import pyinotify
37

    
38
import IPy
39
import socket
40
from select import select
41
from socket import AF_INET, AF_INET6
42

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

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

    
61
LOG_FILENAME = "nfdhcpd.log"
62

    
63
SYSFS_NET = "/sys/class/net"
64

    
65
LOG_FORMAT = "%(asctime)-15s %(levelname)-6s %(message)s"
66

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

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

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

    
91

    
92
DHCPDISCOVER = 1
93
DHCPOFFER = 2
94
DHCPREQUEST = 3
95
DHCPDECLINE = 4
96
DHCPACK = 5
97
DHCPNAK = 6
98
DHCPRELEASE = 7
99
DHCPINFORM = 8
100

    
101
DHCP_TYPES = {
102
    DHCPDISCOVER: "DHCPDISCOVER",
103
    DHCPOFFER: "DHCPOFFER",
104
    DHCPREQUEST: "DHCPREQUEST",
105
    DHCPDECLINE: "DHCPDECLINE",
106
    DHCPACK: "DHCPACK",
107
    DHCPNAK: "DHCPNAK",
108
    DHCPRELEASE: "DHCPRELEASE",
109
    DHCPINFORM: "DHCPINFORM",
110
}
111

    
112
DHCP_REQRESP = {
113
    DHCPDISCOVER: DHCPOFFER,
114
    DHCPREQUEST: DHCPACK,
115
    DHCPINFORM: DHCPACK,
116
    }
117

    
118

    
119
def parse_binding_file(path):
120
    """ Read a client configuration from a tap file
121

    
122
    """
123
    try:
124
        iffile = open(path, 'r')
125
    except EnvironmentError, e:
126
        logging.warn("Unable to open binding file %s: %s", path, str(e))
127
        return None
128

    
129
    ifname = os.path.basename(path)
130
    mac = None
131
    ips = None
132
    link = None
133
    hostname = None
134
    subnet = None
135
    gateway = None
136
    subnet6 = None
137
    gateway6 = None
138

    
139
    for line in iffile:
140
        if line.startswith("IP="):
141
            ip = line.strip().split("=")[1]
142
            ips = ip.split()
143
        elif line.startswith("MAC="):
144
            mac = line.strip().split("=")[1]
145
        elif line.startswith("HOSTNAME="):
146
            hostname = line.strip().split("=")[1]
147
        elif line.startswith("IFACE="):
148
            iface = line.strip().split("=")[1]
149
        elif line.startswith("SUBNET="):
150
            subnet = line.strip().split("=")[1]
151
        elif line.startswith("GATEWAY="):
152
            gateway = line.strip().split("=")[1]
153
        elif line.startswith("SUBNET6="):
154
            subnet6 = line.strip().split("=")[1]
155
        elif line.startswith("GATEWAY6="):
156
            gateway6 = line.strip().split("=")[1]
157

    
158
    return Client(ifname=ifname, mac=mac, ips=ips, link=link,
159
                  hostname=hostname, iface=iface, subnet=subnet,
160
                  gateway=gateway, subnet6=subnet6, gateway6=gateway6 )
161

    
162
class ClientFileHandler(pyinotify.ProcessEvent):
163
    def __init__(self, server):
164
        pyinotify.ProcessEvent.__init__(self)
165
        self.server = server
166

    
167
    def process_IN_DELETE(self, event): # pylint: disable=C0103
168
        """ Delete file handler
169

    
170
        Currently this removes an interface from the watch list
171

    
172
        """
173
        self.server.remove_iface(event.name)
174

    
175
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
176
        """ Add file handler
177

    
178
        Currently this adds an interface to the watch list
179

    
180
        """
181
        self.server.add_iface(os.path.join(event.path, event.name))
182

    
183

    
184
class Client(object):
185
    def __init__(self, ifname=None, mac=None, ips=None, link=None,
186
                 hostname=None, iface=None, subnet=None, gateway=None,
187
                 subnet6=None, gateway6=None ):
188
        self.mac = mac
189
        self.ips = ips
190
        self.hostname = hostname
191
        self.link = link
192
        self.iface = iface
193
        self.ifname = ifname
194
        self.subnet = subnet
195
        self.gateway = gateway
196
        self.net = Subnet(net=subnet, gw=gateway, dev=ifname)
197
        self.subnet6 = subnet6
198
        self.gateway6 = gateway6
199
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=ifname)
200

    
201
    @property
202
    def ip(self):
203
        return self.ips[0]
204

    
205
    def is_valid(self):
206
        return self.mac is not None and self.ips is not None\
207
               and self.hostname is not None
208

    
209

    
210
class Subnet(object):
211
    def __init__(self, net=None, gw=None, dev=None):
212
        if isinstance(net, str):
213
            self.net = IPy.IP(net)
214
        else:
215
            self.net = net
216
        self.gw = gw
217
        self.dev = dev
218

    
219
    @property
220
    def netmask(self):
221
        """ Return the netmask in textual representation
222

    
223
        """
224
        return str(self.net.netmask())
225

    
226
    @property
227
    def broadcast(self):
228
        """ Return the broadcast address in textual representation
229

    
230
        """
231
        return str(self.net.broadcast())
232

    
233
    @property
234
    def prefix(self):
235
        """ Return the network as an IPy.IP
236

    
237
        """
238
        return self.net.net()
239

    
240
    @property
241
    def prefixlen(self):
242
        """ Return the prefix length as an integer
243

    
244
        """
245
        return self.net.prefixlen()
246

    
247
    @staticmethod
248
    def _make_eui64(net, mac):
249
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
250

    
251
        """
252
        comp = mac.split(":")
253
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
254
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
255
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
256
        for l in range(0, len(eui64), 2):
257
            prefix += ["".join(eui64[l:l+2])]
258
        return IPy.IP(":".join(prefix))
259

    
260
    def make_eui64(self, mac):
261
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
262
        subnet.
263

    
264
        """
265
        return self._make_eui64(self.net, mac)
266

    
267
    def make_ll64(self, mac):
268
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
269

    
270
        """
271
        return self._make_eui64("fe80::", mac)
272

    
273

    
274
class VMNetProxy(object): # pylint: disable=R0902
275
    def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
276
                 rs_queue_num=None, ns_queue_num=None,
277
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
278
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
279
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
280
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
281

    
282
        self.data_path = data_path
283
        self.lease_lifetime = dhcp_lease_lifetime
284
        self.lease_renewal = dhcp_lease_renewal
285
        self.dhcp_server_ip = dhcp_server_ip
286
        self.ra_period = ra_period
287
        if dhcp_nameservers is None:
288
            self.dhcp_nameserver = []
289
        else:
290
            self.dhcp_nameservers = dhcp_nameservers
291

    
292
        if ipv6_nameservers is None:
293
            self.ipv6_nameservers = []
294
        else:
295
            self.ipv6_nameservers = ipv6_nameservers
296

    
297
        self.ipv6_enabled = False
298

    
299
        self.clients = {}
300
        #self.subnets = {}
301
        self.ifaces = {}
302
        #self.v6nets = {}
303
        self.nfq = {}
304
        self.l2socket = socket.socket(socket.AF_PACKET,
305
                                      socket.SOCK_RAW, ETH_P_ALL)
306
        self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
307

    
308
        # Inotify setup
309
        self.wm = pyinotify.WatchManager()
310
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
311
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
312
        inotify_handler = ClientFileHandler(self)
313
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
314
        self.wm.add_watch(self.data_path, mask, rec=True)
315

    
316
        # NFQUEUE setup
317
        if dhcp_queue_num is not None:
318
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
319

    
320
        if rs_queue_num is not None:
321
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
322
            self.ipv6_enabled = True
323

    
324
        if ns_queue_num is not None:
325
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
326
            self.ipv6_enabled = True
327

    
328
    def _cleanup(self):
329
        """ Free all resources for a graceful exit
330

    
331
        """
332
        logging.info("Cleaning up")
333

    
334
        logging.debug("Closing netfilter queues")
335
        for q in self.nfq.values():
336
            q.close()
337

    
338
        logging.debug("Closing socket")
339
        self.l2socket.close()
340

    
341
        logging.debug("Stopping inotify watches")
342
        self.notifier.stop()
343

    
344
        logging.info("Cleanup finished")
345

    
346
    def _setup_nfqueue(self, queue_num, family, callback):
347
        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
348
                      queue_num, family)
349
        q = nfqueue.queue()
350
        q.set_callback(callback)
351
        q.fast_open(queue_num, family)
352
        q.set_queue_maxlen(5000)
353
        # This is mandatory for the queue to operate
354
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
355
        self.nfq[q.get_fd()] = q
356

    
357
    def sendp(self, data, iface):
358
        """ Send a raw packet using a layer-2 socket
359

    
360
        """
361
        logging.debug("%s", data)
362
        if isinstance(data, BasePacket):
363
            data = str(data)
364

    
365
        self.l2socket.bind((iface, ETH_P_ALL))
366
        count = self.l2socket.send(data)
367
        ldata = len(data)
368
        if count != ldata:
369
            logging.warn("Truncated send on %s (%d/%d bytes sent)",
370
                         iface, count, ldata)
371

    
372
    def build_config(self):
373
        self.clients.clear()
374

    
375
        for path in glob.glob(os.path.join(self.data_path, "*")):
376
            self.add_iface(path)
377

    
378
    def get_ifindex(self, iface):
379
        """ Get the interface index from sysfs
380

    
381
        """
382
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
383
        if not path.startswith(SYSFS_NET):
384
            return None
385

    
386
        ifindex = None
387

    
388
        try:
389
            f = open(path, 'r')
390
        except EnvironmentError:
391
            logging.debug("%s is probably down, removing", iface)
392
            self.remove_iface(iface)
393

    
394
            return ifindex
395

    
396
        try:
397
            ifindex = f.readline().strip()
398
            try:
399
                ifindex = int(ifindex)
400
            except ValueError, e:
401
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
402
                             " output '%s'", iface, ifindex)
403
        except EnvironmentError, e:
404
            logging.warn("Error reading %s's ifindex from sysfs: %s",
405
                         iface, str(e))
406
            self.remove_iface(iface)
407
        finally:
408
            f.close()
409

    
410
        return ifindex
411

    
412

    
413
    def get_iface_hw_addr(self, iface):
414
        """ Get the interface hardware address from sysfs
415

    
416
        """
417
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
418
        if not path.startswith(SYSFS_NET):
419
            return None
420

    
421
        addr = None
422
        try:
423
            f = open(path, 'r')
424
        except EnvironmentError:
425
            logging.debug("%s is probably down, removing", iface)
426
            self.remove_iface(iface)
427
            return addr
428

    
429
        try:
430
            addr = f.readline().strip()
431
        except EnvironmentError, e:
432
            logging.warn("Failed to read hw address for %s from sysfs: %s",
433
                         iface, str(e))
434
        finally:
435
            f.close()
436

    
437
        return addr
438

    
439
    def add_iface(self, path):
440
        """ Add an interface to monitor
441

    
442
        """
443
        iface = os.path.basename(path)
444

    
445
        logging.debug("Updating configuration for %s", iface)
446
        binding = parse_binding_file(path)
447
        if binding is None:
448
            return
449
        ifindex = self.get_ifindex(binding.iface)
450

    
451
        if ifindex is None:
452
            logging.warn("Stale configuration for %s found", iface)
453
        else:
454
            if binding.is_valid():
455
                self.clients[binding.mac] = binding
456
                logging.debug("Added client %s on %s", binding.hostname, iface)
457
                self.ifaces[ifindex] = binding.iface
458

    
459
    def remove_iface(self, ifname):
460
        """ Cleanup clients on a removed interface
461

    
462
        """
463
        for mac in self.clients.keys():
464
            if self.clients[mac].ifname == ifname:
465
                iface = self.client[mac].iface
466
                del self.clients[mac]
467

    
468
        for ifindex in self.ifaces.keys():
469
            if self.ifaces[ifindex] == ifname == iface:
470
                del self.ifaces[ifindex]
471

    
472
        logging.debug("Removed interface %s", ifname)
473

    
474
    def dhcp_response(self, i, payload): # pylint: disable=W0613,R0914
475
        """ Generate a reply to a BOOTP/DHCP request
476

    
477
        """
478
        logging.info("%s",payload)
479
        indev = payload.get_indev()
480
        try:
481
            # Get the actual interface from the ifindex
482
            iface = self.ifaces[indev]
483
        except KeyError:
484
            # We don't know anything about this interface, so accept the packet
485
            # and return
486
            logging.debug("Ignoring DHCP request on unknown iface %d", indev)
487
            # We don't know what to do with this packet, so let the kernel
488
            # handle it
489
            payload.set_verdict(nfqueue.NF_ACCEPT)
490
            return
491

    
492
        # Decode the response - NFQUEUE relays IP packets
493
        pkt = IP(payload.get_data())
494

    
495
        # Signal the kernel that it shouldn't further process the packet
496
        payload.set_verdict(nfqueue.NF_DROP)
497

    
498
        # Get the client MAC address
499
        resp = pkt.getlayer(BOOTP).copy()
500
        hlen = resp.hlen
501
        mac = resp.chaddr[:hlen].encode("hex")
502
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
503

    
504
        # Server responses are always BOOTREPLYs
505
        resp.op = "BOOTREPLY"
506
        del resp.payload
507

    
508
        try:
509
            binding = self.clients[mac]
510
        except KeyError:
511
            logging.warn("Invalid client %s on %s", mac, iface)
512
            return
513

    
514
        if iface != binding.iface:
515
            logging.warn("Received spoofed DHCP request for %s from interface"
516
                         " %s instead of %s", mac, iface, binding.iface)
517
            return
518

    
519
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
520
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
521
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
522
        subnet = binding.net
523

    
524
        if not DHCP in pkt:
525
            logging.warn("Invalid request from %s on %s, no DHCP"
526
                         " payload found", binding.mac, iface)
527
            return
528

    
529
        dhcp_options = []
530
        requested_addr = binding.ip
531
        for opt in pkt[DHCP].options:
532
            if type(opt) is tuple and opt[0] == "message-type":
533
                req_type = opt[1]
534
            if type(opt) is tuple and opt[0] == "requested_addr":
535
                requested_addr = opt[1]
536

    
537
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
538
                     binding.mac, iface)
539

    
540
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
541
            resp_type = DHCPNAK
542
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
543
                         " instead of %s", binding.mac, iface, requested_addr,
544
                         binding.ip)
545

    
546
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
547
            resp_type = DHCP_REQRESP[req_type]
548
            resp.yiaddr = self.clients[mac].ip
549
            dhcp_options += [
550
                 ("hostname", binding.hostname),
551
                 ("domain", binding.hostname.split('.', 1)[-1]),
552
                 ("broadcast_address", str(subnet.broadcast)),
553
                 ("subnet_mask", str(subnet.netmask)),
554
                 ("renewal_time", self.lease_renewal),
555
                 ("lease_time", self.lease_lifetime),
556
            ]
557
            if subnet.gw:
558
              dhcp_options += [("router", subnet.gw)]
559
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
560

    
561
        elif req_type == DHCPINFORM:
562
            resp_type = DHCP_REQRESP[req_type]
563
            dhcp_options += [
564
                 ("hostname", binding.hostname),
565
                 ("domain", binding.hostname.split('.', 1)[-1]),
566
            ]
567
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
568

    
569
        elif req_type == DHCPRELEASE:
570
            # Log and ignore
571
            logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
572
            return
573

    
574
        # Finally, always add the server identifier and end options
575
        dhcp_options += [
576
            ("message-type", resp_type),
577
            ("server_id", DHCP_DUMMY_SERVER_IP),
578
            "end"
579
        ]
580
        resp /= DHCP(options=dhcp_options)
581

    
582
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
583
                     binding.ip, iface)
584
        self.sendp(resp, iface)
585

    
586
    def rs_response(self, i, payload): # pylint: disable=W0613
587
        """ Generate a reply to a BOOTP/DHCP request
588

    
589
        """
590
        indev = payload.get_indev()
591
        try:
592
            # Get the actual interface from the ifindex
593
            iface = self.ifaces[indev]
594
        except KeyError:
595
            logging.debug("Ignoring router solicitation on"
596
                          " unknown interface %d", indev)
597
            # We don't know what to do with this packet, so let the kernel
598
            # handle it
599
            payload.set_verdict(nfqueue.NF_ACCEPT)
600
            return
601

    
602
        ifmac = self.get_iface_hw_addr(iface)
603
        binding = self.clients[ifmac]
604
        subnet = binding.net6
605
        ifll = subnet.make_ll64(ifmac)
606

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

    
610
        resp = Ether(src=self.get_iface_hw_addr(iface))/\
611
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
612
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
613
                                     prefixlen=subnet.prefixlen)
614

    
615
        if self.ipv6_nameservers:
616
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
617
                                     lifetime=self.ra_period * 3)
618

    
619
        logging.info("RA on %s for %s", iface, subnet.net)
620
        self.sendp(resp, iface)
621

    
622
    def ns_response(self, i, payload): # pylint: disable=W0613
623
        """ Generate a reply to an ICMPv6 neighbor solicitation
624

    
625
        """
626
        indev = payload.get_indev()
627
        try:
628
            # Get the actual interface from the ifindex
629
            iface = self.ifaces[indev]
630
        except KeyError:
631
            logging.debug("Ignoring neighbour solicitation on"
632
                          " unknown interface %d", indev)
633
            # We don't know what to do with this packet, so let the kernel
634
            # handle it
635
            payload.set_verdict(nfqueue.NF_ACCEPT)
636
            return
637

    
638
        ifmac = self.get_iface_hw_addr(iface)
639
        binding = self.clients[ifmac]
640
        subnet = binding.net6
641
        ifll = subnet.make_ll64(ifmac)
642

    
643
        ns = IPv6(payload.get_data())
644

    
645
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
646
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
647
            payload.set_verdict(nfqueue.NF_ACCEPT)
648
            return 1
649

    
650
        payload.set_verdict(nfqueue.NF_DROP)
651

    
652
        try:
653
            client_lladdr = ns.lladdr
654
        except AttributeError:
655
            return 1
656

    
657
        resp = Ether(src=ifmac, dst=client_lladdr)/\
658
               IPv6(src=str(ifll), dst=ns.src)/\
659
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
660
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
661

    
662
        logging.info("NA on %s for %s", iface, ns.tgt)
663
        self.sendp(resp, iface)
664
        return 1
665

    
666
    def send_periodic_ra(self):
667
        # Use a separate thread as this may take a _long_ time with
668
        # many interfaces and we want to be responsive in the mean time
669
        threading.Thread(target=self._send_periodic_ra).start()
670

    
671
    def _send_periodic_ra(self):
672
        logging.debug("Sending out periodic RAs")
673
        start = time.time()
674
        i = 0
675
        for binding in self.clients.values():
676
            iface = binding.ifname
677
            ifmac = binding.mac
678
            subnet = binding.net6
679
            if subnet.net is None:
680
                logging.debug("Skipping periodic RA on interface %s,"
681
                              " as it is not IPv6-connected", iface)
682
                continue
683

    
684
            ifll = subnet.make_ll64(ifmac)
685
            resp = Ether(src=ifmac)/\
686
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
687
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
688
                                         prefixlen=subnet.prefixlen)
689
            if self.ipv6_nameservers:
690
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
691
                                         lifetime=self.ra_period * 3)
692
            try:
693
                self.sendp(resp, iface)
694
            except socket.error, e:
695
                logging.warn("Periodic RA on %s failed: %s", iface, str(e))
696
            except Exception, e:
697
                logging.warn("Unkown error during periodic RA on %s: %s",
698
                             iface, str(e))
699
            i += 1
700
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
701

    
702
    def serve(self):
703
        """ Safely perform the main loop, freeing all resources upon exit
704

    
705
        """
706
        try:
707
            self._serve()
708
        finally:
709
            self._cleanup()
710

    
711
    def _serve(self):
712
        """ Loop forever, serving DHCP requests
713

    
714
        """
715
        self.build_config()
716

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

    
721
        start = time.time()
722
        if self.ipv6_enabled:
723
            timeout = self.ra_period
724
            self.send_periodic_ra()
725
        else:
726
            timeout = None
727

    
728
        while True:
729
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
730
            if xlist:
731
                logging.warn("Warning: Exception on %s",
732
                             ", ".join([ str(fd) for fd in xlist]))
733

    
734
            if rlist:
735
                if iwfd in rlist:
736
                # First check if there are any inotify (= configuration change)
737
                # events
738
                    self.notifier.read_events()
739
                    self.notifier.process_events()
740
                    rlist.remove(iwfd)
741

    
742
                for fd in rlist:
743
                    try:
744
                        self.nfq[fd].process_pending()
745
                    except RuntimeError, e:
746
                        logging.warn("Error processing fd %d: %s", fd, str(e))
747
                    except Exception, e:
748
                        logging.warn("Unknown error processing fd %d: %s",
749
                                     fd, str(e))
750

    
751
            if self.ipv6_enabled:
752
                # Calculate the new timeout
753
                timeout = self.ra_period - (time.time() - start)
754

    
755
                if timeout <= 0:
756
                    start = time.time()
757
                    self.send_periodic_ra()
758
                    timeout = self.ra_period - (time.time() - start)
759

    
760

    
761
if __name__ == "__main__":
762
    import capng
763
    import optparse
764
    from cStringIO import StringIO
765
    from pwd import getpwnam, getpwuid
766
    from configobj import ConfigObj, ConfigObjError, flatten_errors
767

    
768
    import validate
769

    
770
    validator = validate.Validator()
771

    
772
    def is_ip_list(value, family=4):
773
        try:
774
            family = int(family)
775
        except ValueError:
776
            raise validate.VdtParamError(family)
777
        if isinstance(value, (str, unicode)):
778
            value = [value]
779
        if not isinstance(value, list):
780
            raise validate.VdtTypeError(value)
781

    
782
        for entry in value:
783
            try:
784
                ip = IPy.IP(entry)
785
            except ValueError:
786
                raise validate.VdtValueError(entry)
787

    
788
            if ip.version() != family:
789
                raise validate.VdtValueError(entry)
790
        return value
791

    
792
    validator.functions["ip_addr_list"] = is_ip_list
793
    config_spec = StringIO(CONFIG_SPEC)
794

    
795

    
796
    parser = optparse.OptionParser()
797
    parser.add_option("-c", "--config", dest="config_file",
798
                      help="The location of the data files", metavar="FILE",
799
                      default=DEFAULT_CONFIG)
800
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
801
                      help="Turn on debugging messages")
802
    parser.add_option("-f", "--foreground", action="store_false",
803
                      dest="daemonize", default=True,
804
                      help="Do not daemonize, stay in the foreground")
805

    
806

    
807
    opts, args = parser.parse_args()
808

    
809
    try:
810
        config = ConfigObj(opts.config_file, configspec=config_spec)
811
    except ConfigObjError, err:
812
        sys.stderr.write("Failed to parse config file %s: %s" %
813
                         (opts.config_file, str(err)))
814
        sys.exit(1)
815

    
816
    results = config.validate(validator)
817
    if results != True:
818
        logging.fatal("Configuration file validation failed! See errors below:")
819
        for (section_list, key, unused) in flatten_errors(config, results):
820
            if key is not None:
821
                logging.fatal(" '%s' in section '%s' failed validation",
822
                              key, ", ".join(section_list))
823
            else:
824
                logging.fatal(" Section '%s' is missing",
825
                              ", ".join(section_list))
826
        sys.exit(1)
827

    
828
    logger = logging.getLogger()
829
    if opts.debug:
830
        logger.setLevel(logging.DEBUG)
831
    else:
832
        logger.setLevel(logging.INFO)
833

    
834
    if opts.daemonize:
835
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
836
        handler = logging.handlers.RotatingFileHandler(logfile,
837
                                                       maxBytes=2097152)
838
    else:
839
        handler = logging.StreamHandler()
840

    
841
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
842
    logger.addHandler(handler)
843

    
844
    if opts.daemonize:
845
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
846
            config["general"]["pidfile"], 10)
847

    
848
        d = daemon.DaemonContext(pidfile=pidfile,
849
                                 stdout=handler.stream,
850
                                 stderr=handler.stream,
851
                                 files_preserve=[handler.stream])
852
        d.umask = 0022
853
        d.open()
854

    
855
    logging.info("Starting up")
856

    
857
    proxy_opts = {}
858
    if config["dhcp"].as_bool("enable_dhcp"):
859
        proxy_opts.update({
860
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
861
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
862
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
863
            "dhcp_server_ip": config["dhcp"]["server_ip"],
864
            "dhcp_nameservers": config["dhcp"]["nameservers"],
865
        })
866

    
867
    if config["ipv6"].as_bool("enable_ipv6"):
868
        proxy_opts.update({
869
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
870
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
871
            "ra_period": config["ipv6"].as_int("ra_period"),
872
            "ipv6_nameservers": config["ipv6"]["nameservers"],
873
        })
874

    
875
    # pylint: disable=W0142
876
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
877

    
878
    # Drop all capabilities except CAP_NET_RAW and change uid
879
    try:
880
        uid = getpwuid(config["general"].as_int("user"))
881
    except ValueError:
882
        uid = getpwnam(config["general"]["user"])
883

    
884
    logging.debug("Setting capabilities and changing uid")
885
    logging.debug("User: %s, uid: %d, gid: %d",
886
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
887

    
888
    # Keep only the capabilities we need
889
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
890
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
891
    capng.capng_update(capng.CAPNG_ADD,
892
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
893
                       capng.CAP_NET_ADMIN)
894
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
895
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
896

    
897
    logging.info("Ready to serve requests")
898
    try:
899
        proxy.serve()
900
    except Exception:
901
        if opts.daemonize:
902
            exc = "".join(traceback.format_exception(*sys.exc_info()))
903
            logging.critical(exc)
904
        raise
905

    
906

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