Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ 2b9e52e1

History | View | Annotate | Download (28.8 kB)

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

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

    
22
import os
23
import 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
    tap = os.path.basename(path)
130
    indev = None
131
    mac = None
132
    ip = None
133
    hostname = None
134
    subnet = None
135
    gateway = None
136
    subnet6 = None
137
    gateway6 = None
138
    eui64 = None
139

    
140
    def get_value(line):
141
        v = line.strip().split('=')[1]
142
        if v == '':
143
          return None
144
        return v
145

    
146
    for line in iffile:
147
        if line.startswith("IP="):
148
            ip = get_value(line)
149
        elif line.startswith("MAC="):
150
            mac = get_value(line)
151
        elif line.startswith("HOSTNAME="):
152
            hostname = get_value(line)
153
        elif line.startswith("INDEV="):
154
            indev = get_value(line)
155
        elif line.startswith("SUBNET="):
156
            subnet = get_value(line)
157
        elif line.startswith("GATEWAY="):
158
            gateway = get_value(line)
159
        elif line.startswith("SUBNET6="):
160
            subnet6 = get_value(line)
161
        elif line.startswith("GATEWAY6="):
162
            gateway6 = get_value(line)
163
        elif line.startswith("EUI64="):
164
            eui64 = get_value(line)
165

    
166
    return Client(tap=tap, mac=mac, ip=ip, 
167
                  hostname=hostname, indev=indev, subnet=subnet,
168
                  gateway=gateway, subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
169

    
170
class ClientFileHandler(pyinotify.ProcessEvent):
171
    def __init__(self, server):
172
        pyinotify.ProcessEvent.__init__(self)
173
        self.server = server
174

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

    
178
        Currently this removes an interface from the watch list
179

    
180
        """
181
        self.server.remove_tap(event.name)
182

    
183
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
184
        """ Add file handler
185

    
186
        Currently this adds an interface to the watch list
187

    
188
        """
189
        self.server.add_tap(os.path.join(event.path, event.name))
190

    
191

    
192
class Client(object):
193
    def __init__(self, tap=None, indev=None, mac=None, ip=None, hostname=None, 
194
                 subnet=None, gateway=None, subnet6=None, gateway6=None, eui64=None ):
195
        self.mac = mac
196
        self.ip = ip
197
        self.hostname = hostname
198
        self.indev = indev
199
        self.tap = tap
200
        self.subnet = subnet
201
        self.gateway = gateway
202
        self.net = Subnet(net=subnet, gw=gateway, dev=tap)
203
        self.subnet6 = subnet6
204
        self.gateway6 = gateway6
205
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
206
        self.eui64 = eui64
207

    
208
    def is_valid(self):
209
        return self.mac is not None and self.ip is not None\
210
               and self.hostname is not None
211

    
212

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

    
222
    @property
223
    def netmask(self):
224
        """ Return the netmask in textual representation
225

    
226
        """
227
        return str(self.net.netmask())
228

    
229
    @property
230
    def broadcast(self):
231
        """ Return the broadcast address in textual representation
232

    
233
        """
234
        return str(self.net.broadcast())
235

    
236
    @property
237
    def prefix(self):
238
        """ Return the network as an IPy.IP
239

    
240
        """
241
        return self.net.net()
242

    
243
    @property
244
    def prefixlen(self):
245
        """ Return the prefix length as an integer
246

    
247
        """
248
        return self.net.prefixlen()
249

    
250
    @staticmethod
251
    def _make_eui64(net, mac):
252
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
253

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

    
263
    def make_eui64(self, mac):
264
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
265
        subnet.
266

    
267
        """
268
        return self._make_eui64(self.net, mac)
269

    
270
    def make_ll64(self, mac):
271
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
272

    
273
        """
274
        return self._make_eui64("fe80::", mac)
275

    
276

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

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

    
295
        if ipv6_nameservers is None:
296
            self.ipv6_nameservers = []
297
        else:
298
            self.ipv6_nameservers = ipv6_nameservers
299

    
300
        self.ipv6_enabled = False
301

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

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

    
319
        # NFQUEUE setup
320
        if dhcp_queue_num is not None:
321
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
322

    
323
        if rs_queue_num is not None:
324
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
325
            self.ipv6_enabled = True
326

    
327
        if ns_queue_num is not None:
328
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
329
            self.ipv6_enabled = True
330

    
331
    def _cleanup(self):
332
        """ Free all resources for a graceful exit
333

    
334
        """
335
        logging.info("Cleaning up")
336

    
337
        logging.debug("Closing netfilter queues")
338
        for q in self.nfq.values():
339
            q.close()
340

    
341
        logging.debug("Closing socket")
342
        self.l2socket.close()
343

    
344
        logging.debug("Stopping inotify watches")
345
        self.notifier.stop()
346

    
347
        logging.info("Cleanup finished")
348

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

    
360
    def sendp(self, data, dev):
361
        """ Send a raw packet using a layer-2 socket
362

    
363
        """
364
        logging.debug("%s", data)
365
        if isinstance(data, BasePacket):
366
            data = str(data)
367

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

    
375
    def build_config(self):
376
        self.clients.clear()
377

    
378
        for path in glob.glob(os.path.join(self.data_path, "*")):
379
            self.add_tap(path)
380

    
381
    def get_ifindex(self, iface):
382
        """ Get the interface index from sysfs
383

    
384
        """
385
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
386
        if not path.startswith(SYSFS_NET):
387
            return None
388

    
389
        ifindex = None
390

    
391
        try:
392
            f = open(path, 'r')
393
        except EnvironmentError:
394
            logging.debug("%s is probably down, removing", iface)
395
            self.remove_tap(iface)
396

    
397
            return ifindex
398

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

    
413
        return ifindex
414

    
415

    
416
    def get_iface_hw_addr(self, iface):
417
        """ Get the interface hardware address from sysfs
418

    
419
        """
420
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
421
        if not path.startswith(SYSFS_NET):
422
            return None
423

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

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

    
440
        return addr
441

    
442
    def add_tap(self, path):
443
        """ Add an interface to monitor
444

    
445
        """
446
        tap = os.path.basename(path)
447

    
448
        logging.debug("Updating configuration for %s", tap)
449
        binding = parse_binding_file(path)
450
        if binding is None:
451
            return
452
        ifindex = self.get_ifindex(binding.indev)
453

    
454
        if ifindex is None:
455
            logging.warn("Stale configuration for %s found", tap)
456
        else:
457
            if binding.is_valid():
458
                self.clients[binding.mac] = binding
459
                logging.debug("Added client %s on %s", binding.hostname, tap)
460
        logging.debug("clients %s", self.clients.keys())
461

    
462
    def remove_tap(self, tap):
463
        """ Cleanup clients on a removed interface
464

    
465
        """
466
        for b in self.clients.values():
467
            if b.tap == tap:
468
                del b
469

    
470
        logging.debug("Removed interface %s", tap)
471

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

    
475
        """
476
        # Decode the response - NFQUEUE relays IP packets
477
        pkt = IP(payload.get_data())
478

    
479
        # Get the client MAC address
480
        resp = pkt.getlayer(BOOTP).copy()
481
        hlen = resp.hlen
482
        mac = resp.chaddr[:hlen].encode("hex")
483
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
484

    
485
        # Server responses are always BOOTREPLYs
486
        resp.op = "BOOTREPLY"
487
        del resp.payload
488

    
489
        try:
490
            binding = self.clients[mac]
491
        except KeyError:
492
            logging.warn("Invalid client for mac %s ", mac)
493
            payload.set_verdict(nfqueue.NF_ACCEPT)
494
            return
495

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

    
499
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(binding.indev))/\
500
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
501
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
502
        subnet = binding.net
503

    
504
        if not DHCP in pkt:
505
            logging.warn("Invalid request from %s on %s, no DHCP"
506
                         " payload found", binding.mac, binding.tap)
507
            return
508

    
509
        dhcp_options = []
510
        requested_addr = binding.ip
511
        for opt in pkt[DHCP].options:
512
            if type(opt) is tuple and opt[0] == "message-type":
513
                req_type = opt[1]
514
            if type(opt) is tuple and opt[0] == "requested_addr":
515
                requested_addr = opt[1]
516

    
517
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
518
                     binding.mac, binding.tap)
519

    
520
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
521
            resp_type = DHCPNAK
522
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
523
                         " instead of %s", binding.mac, binding.tap, requested_addr,
524
                         binding.ip)
525

    
526
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
527
            resp_type = DHCP_REQRESP[req_type]
528
            resp.yiaddr = self.clients[mac].ip
529
            dhcp_options += [
530
                 ("hostname", binding.hostname),
531
                 ("domain", binding.hostname.split('.', 1)[-1]),
532
                 ("broadcast_address", str(subnet.broadcast)),
533
                 ("subnet_mask", str(subnet.netmask)),
534
                 ("renewal_time", self.lease_renewal),
535
                 ("lease_time", self.lease_lifetime),
536
            ]
537
            if subnet.gw:
538
              dhcp_options += [("router", subnet.gw)]
539
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
540

    
541
        elif req_type == DHCPINFORM:
542
            resp_type = DHCP_REQRESP[req_type]
543
            dhcp_options += [
544
                 ("hostname", binding.hostname),
545
                 ("domain", binding.hostname.split('.', 1)[-1]),
546
            ]
547
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
548

    
549
        elif req_type == DHCPRELEASE:
550
            # Log and ignore
551
            logging.info("DHCPRELEASE from %s on %s", binding.mac, binding.tap )
552
            return
553

    
554
        # Finally, always add the server identifier and end options
555
        dhcp_options += [
556
            ("message-type", resp_type),
557
            ("server_id", DHCP_DUMMY_SERVER_IP),
558
            "end"
559
        ]
560
        resp /= DHCP(options=dhcp_options)
561

    
562
        if payload.get_indev() != self.get_ifindex(binding.indev):
563
            logging.warn("Received spoofed DHCP request for %s from interface"
564
                         " %s instead of %s", mac, payload.get_indev(), binding.indev)
565
            return
566

    
567
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
568
                     binding.ip, binding.tap)
569
        self.sendp(resp, binding.indev)
570

    
571
    def rs_response(self, i, payload): # pylint: disable=W0613
572
        """ Generate a reply to a BOOTP/DHCP request
573

    
574
        """
575
        pkt = IPv6(payload.get_data())
576
        mac = pkt.lladdr
577
        logging.debug("rs for mac %s", mac)
578
        try:
579
          binding = self.clients[mac]
580
        except KeyError:
581
            logging.debug("Ignoring router solicitation on"
582
                          " for mac %s", mac)
583
            # We don't know what to do with this packet, so let the kernel
584
            # handle it
585
            payload.set_verdict(nfqueue.NF_ACCEPT)
586
            return
587
  
588
        # Signal the kernel that it shouldn't further process the packet
589
        payload.set_verdict(nfqueue.NF_DROP)
590

    
591
        subnet = binding.net6
592

    
593
        if subnet.net is None:
594
          logging.debug("No IPv6 network assigned for the interface")
595
          return
596

    
597
        ifmac = self.get_iface_hw_addr(binding.indev)
598
        ifll = subnet.make_ll64(ifmac)
599

    
600

    
601
        resp = Ether(src=ifmac)/\
602
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
603
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
604
                                     prefixlen=subnet.prefixlen)
605

    
606
        if self.ipv6_nameservers:
607
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
608
                                     lifetime=self.ra_period * 3)
609

    
610
        logging.info("RA on %s for %s", binding.indev, subnet.net)
611
        self.sendp(resp, binding.indev)
612

    
613
    def ns_response(self, i, payload): # pylint: disable=W0613
614
        """ Generate a reply to an ICMPv6 neighbor solicitation
615

    
616
        """
617
        ns = IPv6(payload.get_data())
618

    
619
        binding = None
620
        for b in self.clients.values():
621
          if b.eui64 == ns.tgt:
622
            binding = b
623
            break
624

    
625
        if binding is None:
626
            logging.debug("Ignoring neighbour solicitation on"
627
                          " for eui64 %s", ns.tgt)
628
            # We don't know what to do with this packet, so let the kernel
629
            # handle it
630
            payload.set_verdict(nfqueue.NF_ACCEPT)
631
            return
632

    
633
        payload.set_verdict(nfqueue.NF_DROP)
634

    
635
        subnet = binding.net6
636
        if subnet.net is None:
637
          logging.debug("No IPv6 network assigned for the interface")
638
          return
639

    
640
        indevmac = self.get_iface_hw_addr(binding.indev)
641

    
642
        ifll = subnet.make_ll64(indevmac)
643

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

    
649
        logging.debug("na ether %s %s", binding.mac, ns.src)
650
        resp = Ether(src=indevmac, dst=binding.mac)/\
651
               IPv6(src=str(ifll), dst=ns.src)/\
652
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
653
               ICMPv6NDOptDstLLAddr(lladdr=indevmac)
654

    
655
        logging.info("NA on %s for %s", binding.indev, ns.tgt)
656
        self.sendp(resp, binding.indev)
657

    
658
    def send_periodic_ra(self):
659
        # Use a separate thread as this may take a _long_ time with
660
        # many interfaces and we want to be responsive in the mean time
661
        threading.Thread(target=self._send_periodic_ra).start()
662

    
663
    def _send_periodic_ra(self):
664
        logging.debug("Sending out periodic RAs")
665
        start = time.time()
666
        i = 0
667
        for binding in self.clients.values():
668
            tap = binding.tap
669
            indev = binding.indev
670
            mac = binding.mac
671
            subnet = binding.net6
672
            if subnet.net is None:
673
                logging.debug("Skipping periodic RA on interface %s,"
674
                              " as it is not IPv6-connected", tap)
675
                continue
676
            indevmac = self.get_iface_hw_addr(indev)
677
            ifll = subnet.make_ll64(indevmac)
678
            resp = Ether(src=indevmac)/\
679
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
680
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
681
                                         prefixlen=subnet.prefixlen)
682
            if self.ipv6_nameservers:
683
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
684
                                         lifetime=self.ra_period * 3)
685
            try:
686
                self.sendp(resp, indev)
687
            except socket.error, e:
688
                logging.warn("Periodic RA on %s failed: %s", tap, str(e))
689
            except Exception, e:
690
                logging.warn("Unkown error during periodic RA on %s: %s",
691
                             tap, str(e))
692
            i += 1
693
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
694

    
695
    def serve(self):
696
        """ Safely perform the main loop, freeing all resources upon exit
697

    
698
        """
699
        try:
700
            self._serve()
701
        finally:
702
            self._cleanup()
703

    
704
    def _serve(self):
705
        """ Loop forever, serving DHCP requests
706

    
707
        """
708
        self.build_config()
709

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

    
714
        start = time.time()
715
        if self.ipv6_enabled:
716
            timeout = self.ra_period
717
            self.send_periodic_ra()
718
        else:
719
            timeout = None
720

    
721
        while True:
722
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
723
            if xlist:
724
                logging.warn("Warning: Exception on %s",
725
                             ", ".join([ str(fd) for fd in xlist]))
726

    
727
            if rlist:
728
                if iwfd in rlist:
729
                # First check if there are any inotify (= configuration change)
730
                # events
731
                    self.notifier.read_events()
732
                    self.notifier.process_events()
733
                    rlist.remove(iwfd)
734

    
735
                for fd in rlist:
736
                    try:
737
                        self.nfq[fd].process_pending()
738
                    except RuntimeError, e:
739
                        logging.warn("Error processing fd %d: %s", fd, str(e))
740
                    except Exception, e:
741
                        logging.warn("Unknown error processing fd %d: %s",
742
                                     fd, str(e))
743

    
744
            if self.ipv6_enabled:
745
                # Calculate the new timeout
746
                timeout = self.ra_period - (time.time() - start)
747

    
748
                if timeout <= 0:
749
                    start = time.time()
750
                    self.send_periodic_ra()
751
                    timeout = self.ra_period - (time.time() - start)
752

    
753

    
754
if __name__ == "__main__":
755
    import capng
756
    import optparse
757
    from cStringIO import StringIO
758
    from pwd import getpwnam, getpwuid
759
    from configobj import ConfigObj, ConfigObjError, flatten_errors
760

    
761
    import validate
762

    
763
    validator = validate.Validator()
764

    
765
    def is_ip_list(value, family=4):
766
        try:
767
            family = int(family)
768
        except ValueError:
769
            raise validate.VdtParamError(family)
770
        if isinstance(value, (str, unicode)):
771
            value = [value]
772
        if not isinstance(value, list):
773
            raise validate.VdtTypeError(value)
774

    
775
        for entry in value:
776
            try:
777
                ip = IPy.IP(entry)
778
            except ValueError:
779
                raise validate.VdtValueError(entry)
780

    
781
            if ip.version() != family:
782
                raise validate.VdtValueError(entry)
783
        return value
784

    
785
    validator.functions["ip_addr_list"] = is_ip_list
786
    config_spec = StringIO(CONFIG_SPEC)
787

    
788

    
789
    parser = optparse.OptionParser()
790
    parser.add_option("-c", "--config", dest="config_file",
791
                      help="The location of the data files", metavar="FILE",
792
                      default=DEFAULT_CONFIG)
793
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
794
                      help="Turn on debugging messages")
795
    parser.add_option("-f", "--foreground", action="store_false",
796
                      dest="daemonize", default=True,
797
                      help="Do not daemonize, stay in the foreground")
798

    
799

    
800
    opts, args = parser.parse_args()
801

    
802
    try:
803
        config = ConfigObj(opts.config_file, configspec=config_spec)
804
    except ConfigObjError, err:
805
        sys.stderr.write("Failed to parse config file %s: %s" %
806
                         (opts.config_file, str(err)))
807
        sys.exit(1)
808

    
809
    results = config.validate(validator)
810
    if results != True:
811
        logging.fatal("Configuration file validation failed! See errors below:")
812
        for (section_list, key, unused) in flatten_errors(config, results):
813
            if key is not None:
814
                logging.fatal(" '%s' in section '%s' failed validation",
815
                              key, ", ".join(section_list))
816
            else:
817
                logging.fatal(" Section '%s' is missing",
818
                              ", ".join(section_list))
819
        sys.exit(1)
820

    
821
    logger = logging.getLogger()
822
    if opts.debug:
823
        logger.setLevel(logging.DEBUG)
824
    else:
825
        logger.setLevel(logging.INFO)
826

    
827
    if opts.daemonize:
828
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
829
        handler = logging.handlers.RotatingFileHandler(logfile,
830
                                                       maxBytes=2097152)
831
    else:
832
        handler = logging.StreamHandler()
833

    
834
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
835
    logger.addHandler(handler)
836

    
837
    if opts.daemonize:
838
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
839
            config["general"]["pidfile"], 10)
840

    
841
        d = daemon.DaemonContext(pidfile=pidfile,
842
                                 stdout=handler.stream,
843
                                 stderr=handler.stream,
844
                                 files_preserve=[handler.stream])
845
        d.umask = 0022
846
        d.open()
847

    
848
    logging.info("Starting up")
849

    
850
    proxy_opts = {}
851
    if config["dhcp"].as_bool("enable_dhcp"):
852
        proxy_opts.update({
853
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
854
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
855
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
856
            "dhcp_server_ip": config["dhcp"]["server_ip"],
857
            "dhcp_nameservers": config["dhcp"]["nameservers"],
858
        })
859

    
860
    if config["ipv6"].as_bool("enable_ipv6"):
861
        proxy_opts.update({
862
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
863
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
864
            "ra_period": config["ipv6"].as_int("ra_period"),
865
            "ipv6_nameservers": config["ipv6"]["nameservers"],
866
        })
867

    
868
    # pylint: disable=W0142
869
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
870

    
871
    # Drop all capabilities except CAP_NET_RAW and change uid
872
    try:
873
        uid = getpwuid(config["general"].as_int("user"))
874
    except ValueError:
875
        uid = getpwnam(config["general"]["user"])
876

    
877
    logging.debug("Setting capabilities and changing uid")
878
    logging.debug("User: %s, uid: %d, gid: %d",
879
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
880

    
881
    # Keep only the capabilities we need
882
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
883
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
884
    capng.capng_update(capng.CAPNG_ADD,
885
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
886
                       capng.CAP_NET_ADMIN)
887
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
888
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
889

    
890
    logging.info("Ready to serve requests")
891
    try:
892
        proxy.serve()
893
    except Exception:
894
        if opts.daemonize:
895
            exc = "".join(traceback.format_exception(*sys.exc_info()))
896
            logging.critical(exc)
897
        raise
898

    
899

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