Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd / nfdhcpd @ f04c0767

History | View | Annotate | Download (29.5 kB)

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

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

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

    
33
import daemon
34
import daemon.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.clients[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 = [ b for b in self.clients.values() if b.ifname == iface ]
604
        subnet = binding[0].net6
605
        if subnet.net is None:
606
          logging.debug("No IPv6 network assigned for the interface")
607
          return
608
        ifll = subnet.make_ll64(ifmac)
609

    
610
        # Signal the kernel that it shouldn't further process the packet
611
        payload.set_verdict(nfqueue.NF_DROP)
612

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

    
618
        if self.ipv6_nameservers:
619
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
620
                                     lifetime=self.ra_period * 3)
621

    
622
        logging.info("RA on %s for %s", iface, subnet.net)
623
        self.sendp(resp, iface)
624

    
625
    def ns_response(self, i, payload): # pylint: disable=W0613
626
        """ Generate a reply to an ICMPv6 neighbor solicitation
627

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

    
641
        ifmac = self.get_iface_hw_addr(iface)
642
        binding = [ b for b in self.clients.values() if b.ifname == iface ]
643
        subnet = binding[0].net6
644
        if subnet.net is None:
645
          logging.debug("No IPv6 network assigned for the interface")
646
          return
647

    
648
        ifll = subnet.make_ll64(ifmac)
649

    
650
        ns = IPv6(payload.get_data())
651

    
652
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
653
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
654
            payload.set_verdict(nfqueue.NF_ACCEPT)
655
            return 1
656

    
657
        payload.set_verdict(nfqueue.NF_DROP)
658

    
659
        try:
660
            client_lladdr = ns.lladdr
661
        except AttributeError:
662
            return 1
663

    
664
        resp = Ether(src=ifmac, dst=client_lladdr)/\
665
               IPv6(src=str(ifll), dst=ns.src)/\
666
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
667
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
668

    
669
        logging.info("NA on %s for %s", iface, ns.tgt)
670
        self.sendp(resp, iface)
671
        return 1
672

    
673
    def send_periodic_ra(self):
674
        # Use a separate thread as this may take a _long_ time with
675
        # many interfaces and we want to be responsive in the mean time
676
        threading.Thread(target=self._send_periodic_ra).start()
677

    
678
    def _send_periodic_ra(self):
679
        logging.debug("Sending out periodic RAs")
680
        start = time.time()
681
        i = 0
682
        for binding in self.clients.values():
683
            iface = binding.ifname
684
            ifmac = binding.mac
685
            subnet = binding.net6
686
            if subnet.net is None:
687
                logging.debug("Skipping periodic RA on interface %s,"
688
                              " as it is not IPv6-connected", iface)
689
                continue
690

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

    
709
    def serve(self):
710
        """ Safely perform the main loop, freeing all resources upon exit
711

    
712
        """
713
        try:
714
            self._serve()
715
        finally:
716
            self._cleanup()
717

    
718
    def _serve(self):
719
        """ Loop forever, serving DHCP requests
720

    
721
        """
722
        self.build_config()
723

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

    
728
        start = time.time()
729
        if self.ipv6_enabled:
730
            timeout = self.ra_period
731
            self.send_periodic_ra()
732
        else:
733
            timeout = None
734

    
735
        while True:
736
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
737
            if xlist:
738
                logging.warn("Warning: Exception on %s",
739
                             ", ".join([ str(fd) for fd in xlist]))
740

    
741
            if rlist:
742
                if iwfd in rlist:
743
                # First check if there are any inotify (= configuration change)
744
                # events
745
                    self.notifier.read_events()
746
                    self.notifier.process_events()
747
                    rlist.remove(iwfd)
748

    
749
                for fd in rlist:
750
                    try:
751
                        self.nfq[fd].process_pending()
752
                    except RuntimeError, e:
753
                        logging.warn("Error processing fd %d: %s", fd, str(e))
754
                    except Exception, e:
755
                        logging.warn("Unknown error processing fd %d: %s",
756
                                     fd, str(e))
757

    
758
            if self.ipv6_enabled:
759
                # Calculate the new timeout
760
                timeout = self.ra_period - (time.time() - start)
761

    
762
                if timeout <= 0:
763
                    start = time.time()
764
                    self.send_periodic_ra()
765
                    timeout = self.ra_period - (time.time() - start)
766

    
767

    
768
if __name__ == "__main__":
769
    import capng
770
    import optparse
771
    from cStringIO import StringIO
772
    from pwd import getpwnam, getpwuid
773
    from configobj import ConfigObj, ConfigObjError, flatten_errors
774

    
775
    import validate
776

    
777
    validator = validate.Validator()
778

    
779
    def is_ip_list(value, family=4):
780
        try:
781
            family = int(family)
782
        except ValueError:
783
            raise validate.VdtParamError(family)
784
        if isinstance(value, (str, unicode)):
785
            value = [value]
786
        if not isinstance(value, list):
787
            raise validate.VdtTypeError(value)
788

    
789
        for entry in value:
790
            try:
791
                ip = IPy.IP(entry)
792
            except ValueError:
793
                raise validate.VdtValueError(entry)
794

    
795
            if ip.version() != family:
796
                raise validate.VdtValueError(entry)
797
        return value
798

    
799
    validator.functions["ip_addr_list"] = is_ip_list
800
    config_spec = StringIO(CONFIG_SPEC)
801

    
802

    
803
    parser = optparse.OptionParser()
804
    parser.add_option("-c", "--config", dest="config_file",
805
                      help="The location of the data files", metavar="FILE",
806
                      default=DEFAULT_CONFIG)
807
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
808
                      help="Turn on debugging messages")
809
    parser.add_option("-f", "--foreground", action="store_false",
810
                      dest="daemonize", default=True,
811
                      help="Do not daemonize, stay in the foreground")
812

    
813

    
814
    opts, args = parser.parse_args()
815

    
816
    try:
817
        config = ConfigObj(opts.config_file, configspec=config_spec)
818
    except ConfigObjError, err:
819
        sys.stderr.write("Failed to parse config file %s: %s" %
820
                         (opts.config_file, str(err)))
821
        sys.exit(1)
822

    
823
    results = config.validate(validator)
824
    if results != True:
825
        logging.fatal("Configuration file validation failed! See errors below:")
826
        for (section_list, key, unused) in flatten_errors(config, results):
827
            if key is not None:
828
                logging.fatal(" '%s' in section '%s' failed validation",
829
                              key, ", ".join(section_list))
830
            else:
831
                logging.fatal(" Section '%s' is missing",
832
                              ", ".join(section_list))
833
        sys.exit(1)
834

    
835
    logger = logging.getLogger()
836
    if opts.debug:
837
        logger.setLevel(logging.DEBUG)
838
    else:
839
        logger.setLevel(logging.INFO)
840

    
841
    if opts.daemonize:
842
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
843
        handler = logging.handlers.RotatingFileHandler(logfile,
844
                                                       maxBytes=2097152)
845
    else:
846
        handler = logging.StreamHandler()
847

    
848
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
849
    logger.addHandler(handler)
850

    
851
    if opts.daemonize:
852
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile(
853
            config["general"]["pidfile"], 10)
854

    
855
        d = daemon.DaemonContext(pidfile=pidfile,
856
                                 stdout=handler.stream,
857
                                 stderr=handler.stream,
858
                                 files_preserve=[handler.stream])
859
        d.umask = 0022
860
        d.open()
861

    
862
    logging.info("Starting up")
863

    
864
    proxy_opts = {}
865
    if config["dhcp"].as_bool("enable_dhcp"):
866
        proxy_opts.update({
867
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
868
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
869
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
870
            "dhcp_server_ip": config["dhcp"]["server_ip"],
871
            "dhcp_nameservers": config["dhcp"]["nameservers"],
872
        })
873

    
874
    if config["ipv6"].as_bool("enable_ipv6"):
875
        proxy_opts.update({
876
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
877
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
878
            "ra_period": config["ipv6"].as_int("ra_period"),
879
            "ipv6_nameservers": config["ipv6"]["nameservers"],
880
        })
881

    
882
    # pylint: disable=W0142
883
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
884

    
885
    # Drop all capabilities except CAP_NET_RAW and change uid
886
    try:
887
        uid = getpwuid(config["general"].as_int("user"))
888
    except ValueError:
889
        uid = getpwnam(config["general"]["user"])
890

    
891
    logging.debug("Setting capabilities and changing uid")
892
    logging.debug("User: %s, uid: %d, gid: %d",
893
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
894

    
895
    # Keep only the capabilities we need
896
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
897
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
898
    capng.capng_update(capng.CAPNG_ADD,
899
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
900
                       capng.CAP_NET_ADMIN)
901
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
902
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
903

    
904
    logging.info("Ready to serve requests")
905
    try:
906
        proxy.serve()
907
    except Exception:
908
        if opts.daemonize:
909
            exc = "".join(traceback.format_exception(*sys.exc_info()))
910
            logging.critical(exc)
911
        raise
912

    
913

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