Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ f8e790c4

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
    ips = None
133
    hostname = None
134
    subnet = None
135
    gateway = None
136
    subnet6 = None
137
    gateway6 = None
138
    eui64 = None
139

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

    
161
    return Client(tap=tap, mac=mac, ips=ips, 
162
                  hostname=hostname, indev=indev, subnet=subnet,
163
                  gateway=gateway, subnet6=subnet6, gateway6=gateway6, eui64=eui64 )
164

    
165
class ClientFileHandler(pyinotify.ProcessEvent):
166
    def __init__(self, server):
167
        pyinotify.ProcessEvent.__init__(self)
168
        self.server = server
169

    
170
    def process_IN_DELETE(self, event): # pylint: disable=C0103
171
        """ Delete file handler
172

    
173
        Currently this removes an interface from the watch list
174

    
175
        """
176
        self.server.remove_tap(event.name)
177

    
178
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
179
        """ Add file handler
180

    
181
        Currently this adds an interface to the watch list
182

    
183
        """
184
        self.server.add_tap(os.path.join(event.path, event.name))
185

    
186

    
187
class Client(object):
188
    def __init__(self, tap=None, indev=None, mac=None, ips=None, hostname=None, 
189
                 subnet=None, gateway=None, subnet6=None, gateway6=None, eui64=None ):
190
        self.mac = mac
191
        self.ips = ips
192
        self.hostname = hostname
193
        self.indev = indev
194
        self.tap = tap
195
        self.subnet = subnet
196
        self.gateway = gateway
197
        self.net = Subnet(net=subnet, gw=gateway, dev=tap)
198
        self.subnet6 = subnet6
199
        self.gateway6 = gateway6
200
        self.net6 = Subnet(net=subnet6, gw=gateway6, dev=tap)
201
        self.eui64 = eui64
202

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

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

    
211

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
275

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

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

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

    
299
        self.ipv6_enabled = False
300

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

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

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

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

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

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

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

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

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

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

    
346
        logging.info("Cleanup finished")
347

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

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

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

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

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

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

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

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

    
388
        ifindex = None
389

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

    
396
            return ifindex
397

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

    
412
        return ifindex
413

    
414

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

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

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

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

    
439
        return addr
440

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
590
        subnet = binding.net6
591

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

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

    
599

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

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

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

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

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

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

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

    
632
        payload.set_verdict(nfqueue.NF_DROP)
633

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

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

    
641
        ifll = subnet.make_ll64(indevmac)
642

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

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

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

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

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

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

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

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

    
706
        """
707
        self.build_config()
708

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

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

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

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

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

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

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

    
752

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

    
760
    import validate
761

    
762
    validator = validate.Validator()
763

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

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

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

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

    
787

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

    
798

    
799
    opts, args = parser.parse_args()
800

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

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

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

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

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

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

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

    
847
    logging.info("Starting up")
848

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

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

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

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

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

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

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

    
898

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