Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ feaba806

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_routing_table(table="main", family=4):
120
    """ Parse the given routing table to get connected route, gateway and
121
    default device.
122

    
123
    """
124
    ipro = subprocess.Popen(["ip", "-%d" % family, "ro", "ls",
125
                             "table", table], stdout=subprocess.PIPE)
126
    routes = ipro.stdout.readlines()
127

    
128
    def_gw = None
129
    def_dev = None
130
    def_net = None
131

    
132
    for route in routes:
133
        match = re.match(r'^default.*via ([^\s]+).*dev ([^\s]+)', route)
134
        if match:
135
            def_gw, def_dev = match.groups()
136
            break
137

    
138
    for route in routes:
139
        # Find the least-specific connected route
140
        m = re.match("^([^\\s]+) dev %s" % def_dev, route)
141
        if not m:
142
            continue
143

    
144
        if family == 6 and m.group(1).startswith("fe80:"):
145
            # Skip link-local declarations in "main" table
146
            continue
147

    
148
        def_net = m.group(1)
149

    
150
        try:
151
            def_net = IPy.IP(def_net)
152
        except ValueError, e:
153
            logging.warn("Unable to parse default route entry %s: %s",
154
                         def_net, str(e))
155

    
156
    return Subnet(net=def_net, gw=def_gw, dev=def_dev)
157

    
158

    
159
def parse_binding_file(path):
160
    """ Read a client configuration from a tap file
161

    
162
    """
163
    try:
164
        iffile = open(path, 'r')
165
    except EnvironmentError, e:
166
        logging.warn("Unable to open binding file %s: %s", path, str(e))
167
        return None
168

    
169
    mac = None
170
    ips = None
171
    link = None
172
    hostname = None
173

    
174
    for line in iffile:
175
        if line.startswith("IP="):
176
            ip = line.strip().split("=")[1]
177
            ips = ip.split()
178
        elif line.startswith("MAC="):
179
            mac = line.strip().split("=")[1]
180
        elif line.startswith("LINK="):
181
            link = line.strip().split("=")[1]
182
        elif line.startswith("HOSTNAME="):
183
            hostname = line.strip().split("=")[1]
184

    
185
    return Client(mac=mac, ips=ips, link=link, hostname=hostname)
186

    
187

    
188
class ClientFileHandler(pyinotify.ProcessEvent):
189
    def __init__(self, server):
190
        pyinotify.ProcessEvent.__init__(self)
191
        self.server = server
192

    
193
    def process_IN_DELETE(self, event): # pylint: disable=C0103
194
        """ Delete file handler
195

    
196
        Currently this removes an interface from the watch list
197

    
198
        """
199
        self.server.remove_iface(event.name)
200

    
201
    def process_IN_CLOSE_WRITE(self, event): # pylint: disable=C0103
202
        """ Add file handler
203

    
204
        Currently this adds an interface to the watch list
205

    
206
        """
207
        self.server.add_iface(os.path.join(event.path, event.name))
208

    
209

    
210
class Client(object):
211
    def __init__(self, mac=None, ips=None, link=None, hostname=None):
212
        self.mac = mac
213
        self.ips = ips
214
        self.hostname = hostname
215
        self.link = link
216
        self.iface = None
217

    
218
    @property
219
    def ip(self):
220
        return self.ips[0]
221

    
222
    def is_valid(self):
223
        return self.mac is not None and self.ips is not None\
224
               and self.hostname is not None
225

    
226

    
227
class Subnet(object):
228
    def __init__(self, net=None, gw=None, dev=None):
229
        if isinstance(net, str):
230
            self.net = IPy.IP(net)
231
        else:
232
            self.net = net
233
        self.gw = gw
234
        self.dev = dev
235

    
236
    @property
237
    def netmask(self):
238
        """ Return the netmask in textual representation
239

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

    
243
    @property
244
    def broadcast(self):
245
        """ Return the broadcast address in textual representation
246

    
247
        """
248
        return str(self.net.broadcast())
249

    
250
    @property
251
    def prefix(self):
252
        """ Return the network as an IPy.IP
253

    
254
        """
255
        return self.net.net()
256

    
257
    @property
258
    def prefixlen(self):
259
        """ Return the prefix length as an integer
260

    
261
        """
262
        return self.net.prefixlen()
263

    
264
    @staticmethod
265
    def _make_eui64(net, mac):
266
        """ Compute an EUI-64 address from an EUI-48 (MAC) address
267

    
268
        """
269
        comp = mac.split(":")
270
        prefix = IPy.IP(net).net().strFullsize().split(":")[:4]
271
        eui64 = comp[:3] + ["ff", "fe"] + comp[3:]
272
        eui64[0] = "%02x" % (int(eui64[0], 16) ^ 0x02)
273
        for l in range(0, len(eui64), 2):
274
            prefix += ["".join(eui64[l:l+2])]
275
        return IPy.IP(":".join(prefix))
276

    
277
    def make_eui64(self, mac):
278
        """ Compute an EUI-64 address from an EUI-48 (MAC) address in this
279
        subnet.
280

    
281
        """
282
        return self._make_eui64(self.net, mac)
283

    
284
    def make_ll64(self, mac):
285
        """ Compute an IPv6 Link-local address from an EUI-48 (MAC) address
286

    
287
        """
288
        return self._make_eui64("fe80::", mac)
289

    
290

    
291
class VMNetProxy(object): # pylint: disable=R0902
292
    def __init__(self, data_path, dhcp_queue_num=None, # pylint: disable=R0913
293
                 rs_queue_num=None, ns_queue_num=None,
294
                 dhcp_lease_lifetime=DEFAULT_LEASE_LIFETIME,
295
                 dhcp_lease_renewal=DEFAULT_LEASE_RENEWAL,
296
                 dhcp_server_ip=DHCP_DUMMY_SERVER_IP, dhcp_nameservers=None,
297
                 ra_period=DEFAULT_RA_PERIOD, ipv6_nameservers=None):
298

    
299
        self.data_path = data_path
300
        self.lease_lifetime = dhcp_lease_lifetime
301
        self.lease_renewal = dhcp_lease_renewal
302
        self.dhcp_server_ip = dhcp_server_ip
303
        self.ra_period = ra_period
304
        if dhcp_nameservers is None:
305
            self.dhcp_nameserver = []
306
        else:
307
            self.dhcp_nameservers = dhcp_nameservers
308

    
309
        if ipv6_nameservers is None:
310
            self.ipv6_nameservers = []
311
        else:
312
            self.ipv6_nameservers = ipv6_nameservers
313

    
314
        self.ipv6_enabled = False
315

    
316
        self.clients = {}
317
        self.subnets = {}
318
        self.ifaces = {}
319
        self.v6nets = {}
320
        self.nfq = {}
321
        self.l2socket = socket.socket(socket.AF_PACKET,
322
                                      socket.SOCK_RAW, ETH_P_ALL)
323
        self.l2socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 0)
324

    
325
        # Inotify setup
326
        self.wm = pyinotify.WatchManager()
327
        mask = pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"]
328
        mask |= pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"]
329
        inotify_handler = ClientFileHandler(self)
330
        self.notifier = pyinotify.Notifier(self.wm, inotify_handler)
331
        self.wm.add_watch(self.data_path, mask, rec=True)
332

    
333
        # NFQUEUE setup
334
        if dhcp_queue_num is not None:
335
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
336

    
337
        if rs_queue_num is not None:
338
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
339
            self.ipv6_enabled = True
340

    
341
        if ns_queue_num is not None:
342
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
343
            self.ipv6_enabled = True
344

    
345
    def _cleanup(self):
346
        """ Free all resources for a graceful exit
347

    
348
        """
349
        logging.info("Cleaning up")
350

    
351
        logging.debug("Closing netfilter queues")
352
        for q in self.nfq.values():
353
            q.close()
354

    
355
        logging.debug("Closing socket")
356
        self.l2socket.close()
357

    
358
        logging.debug("Stopping inotify watches")
359
        self.notifier.stop()
360

    
361
        logging.info("Cleanup finished")
362

    
363
    def _setup_nfqueue(self, queue_num, family, callback):
364
        logging.debug("Setting up NFQUEUE for queue %d, AF %s",
365
                      queue_num, family)
366
        q = nfqueue.queue()
367
        q.set_callback(callback)
368
        q.fast_open(queue_num, family)
369
        q.set_queue_maxlen(5000)
370
        # This is mandatory for the queue to operate
371
        q.set_mode(nfqueue.NFQNL_COPY_PACKET)
372
        self.nfq[q.get_fd()] = q
373

    
374
    def sendp(self, data, iface):
375
        """ Send a raw packet using a layer-2 socket
376

    
377
        """
378
        if isinstance(data, BasePacket):
379
            data = str(data)
380

    
381
        self.l2socket.bind((iface, ETH_P_ALL))
382
        count = self.l2socket.send(data)
383
        ldata = len(data)
384
        if count != ldata:
385
            logging.warn("Truncated send on %s (%d/%d bytes sent)",
386
                         iface, count, ldata)
387

    
388
    def build_config(self):
389
        self.clients.clear()
390
        self.subnets.clear()
391

    
392
        for path in glob.glob(os.path.join(self.data_path, "*")):
393
            self.add_iface(path)
394

    
395
    def get_ifindex(self, iface):
396
        """ Get the interface index from sysfs
397

    
398
        """
399
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
400
        if not path.startswith(SYSFS_NET):
401
            return None
402

    
403
        ifindex = None
404

    
405
        try:
406
            f = open(path, 'r')
407
        except EnvironmentError:
408
            logging.debug("%s is probably down, removing", iface)
409
            self.remove_iface(iface)
410

    
411
            return ifindex
412

    
413
        try:
414
            ifindex = f.readline().strip()
415
            try:
416
                ifindex = int(ifindex)
417
            except ValueError, e:
418
                logging.warn("Failed to get ifindex for %s, cannot parse sysfs"
419
                             " output '%s'", iface, ifindex)
420
        except EnvironmentError, e:
421
            logging.warn("Error reading %s's ifindex from sysfs: %s",
422
                         iface, str(e))
423
            self.remove_iface(iface)
424
        finally:
425
            f.close()
426

    
427
        return ifindex
428

    
429

    
430
    def get_iface_hw_addr(self, iface):
431
        """ Get the interface hardware address from sysfs
432

    
433
        """
434
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
435
        if not path.startswith(SYSFS_NET):
436
            return None
437

    
438
        addr = None
439
        try:
440
            f = open(path, 'r')
441
        except EnvironmentError:
442
            logging.debug("%s is probably down, removing", iface)
443
            self.remove_iface(iface)
444
            return addr
445

    
446
        try:
447
            addr = f.readline().strip()
448
        except EnvironmentError, e:
449
            logging.warn("Failed to read hw address for %s from sysfs: %s",
450
                         iface, str(e))
451
        finally:
452
            f.close()
453

    
454
        return addr
455

    
456
    def add_iface(self, path):
457
        """ Add an interface to monitor
458

    
459
        """
460
        iface = os.path.basename(path)
461

    
462
        logging.debug("Updating configuration for %s", iface)
463
        binding = parse_binding_file(path)
464
        if binding is None:
465
            return
466
        ifindex = self.get_ifindex(iface)
467

    
468
        if ifindex is None:
469
            logging.warn("Stale configuration for %s found", iface)
470
        else:
471
            if binding.is_valid():
472
                binding.iface = iface
473
                self.clients[binding.mac] = binding
474
                self.subnets[binding.link] = parse_routing_table(binding.link)
475
                logging.debug("Added client %s on %s", binding.hostname, iface)
476
                self.ifaces[ifindex] = iface
477
                self.v6nets[iface] = parse_routing_table(binding.link, 6)
478

    
479
    def remove_iface(self, iface):
480
        """ Cleanup clients on a removed interface
481

    
482
        """
483
        if iface in self.v6nets:
484
            del self.v6nets[iface]
485

    
486
        for mac in self.clients.keys():
487
            if self.clients[mac].iface == iface:
488
                del self.clients[mac]
489

    
490
        for ifindex in self.ifaces.keys():
491
            if self.ifaces[ifindex] == iface:
492
                del self.ifaces[ifindex]
493

    
494
        logging.debug("Removed interface %s", iface)
495

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

    
499
        """
500
        indev = payload.get_indev()
501
        try:
502
            # Get the actual interface from the ifindex
503
            iface = self.ifaces[indev]
504
        except KeyError:
505
            # We don't know anything about this interface, so accept the packet
506
            # and return
507
            logging.debug("Ignoring DHCP request on unknown iface %d", indev)
508
            # We don't know what to do with this packet, so let the kernel
509
            # handle it
510
            payload.set_verdict(nfqueue.NF_ACCEPT)
511
            return
512

    
513
        # Decode the response - NFQUEUE relays IP packets
514
        pkt = IP(payload.get_data())
515

    
516
        # Signal the kernel that it shouldn't further process the packet
517
        payload.set_verdict(nfqueue.NF_DROP)
518

    
519
        # Get the client MAC address
520
        resp = pkt.getlayer(BOOTP).copy()
521
        hlen = resp.hlen
522
        mac = resp.chaddr[:hlen].encode("hex")
523
        mac, _ = re.subn(r'([0-9a-fA-F]{2})', r'\1:', mac, hlen-1)
524

    
525
        # Server responses are always BOOTREPLYs
526
        resp.op = "BOOTREPLY"
527
        del resp.payload
528

    
529
        try:
530
            binding = self.clients[mac]
531
        except KeyError:
532
            logging.warn("Invalid client %s on %s", mac, iface)
533
            return
534

    
535
        if iface != binding.iface:
536
            logging.warn("Received spoofed DHCP request for %s from interface"
537
                         " %s instead of %s", mac, iface, binding.iface)
538
            return
539

    
540
        resp = Ether(dst=mac, src=self.get_iface_hw_addr(iface))/\
541
               IP(src=DHCP_DUMMY_SERVER_IP, dst=binding.ip)/\
542
               UDP(sport=pkt.dport, dport=pkt.sport)/resp
543
        subnet = self.subnets[binding.link]
544

    
545
        if not DHCP in pkt:
546
            logging.warn("Invalid request from %s on %s, no DHCP"
547
                         " payload found", binding.mac, iface)
548
            return
549

    
550
        dhcp_options = []
551
        requested_addr = binding.ip
552
        for opt in pkt[DHCP].options:
553
            if type(opt) is tuple and opt[0] == "message-type":
554
                req_type = opt[1]
555
            if type(opt) is tuple and opt[0] == "requested_addr":
556
                requested_addr = opt[1]
557

    
558
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
559
                     binding.mac, iface)
560

    
561
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
562
            resp_type = DHCPNAK
563
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
564
                         " instead of %s", binding.mac, iface, requested_addr,
565
                         binding.ip)
566

    
567
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
568
            resp_type = DHCP_REQRESP[req_type]
569
            resp.yiaddr = self.clients[mac].ip
570
            dhcp_options += [
571
                 ("hostname", binding.hostname),
572
                 ("domain", binding.hostname.split('.', 1)[-1]),
573
                 ("router", subnet.gw),
574
                 ("broadcast_address", str(subnet.broadcast)),
575
                 ("subnet_mask", str(subnet.netmask)),
576
                 ("renewal_time", self.lease_renewal),
577
                 ("lease_time", self.lease_lifetime),
578
            ]
579
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
580

    
581
        elif req_type == DHCPINFORM:
582
            resp_type = DHCP_REQRESP[req_type]
583
            dhcp_options += [
584
                 ("hostname", binding.hostname),
585
                 ("domain", binding.hostname.split('.', 1)[-1]),
586
            ]
587
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
588

    
589
        elif req_type == DHCPRELEASE:
590
            # Log and ignore
591
            logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
592
            return
593

    
594
        # Finally, always add the server identifier and end options
595
        dhcp_options += [
596
            ("message-type", resp_type),
597
            ("server_id", DHCP_DUMMY_SERVER_IP),
598
            "end"
599
        ]
600
        resp /= DHCP(options=dhcp_options)
601

    
602
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
603
                     binding.ip, iface)
604
        self.sendp(resp, iface)
605

    
606
    def rs_response(self, i, payload): # pylint: disable=W0613
607
        """ Generate a reply to a BOOTP/DHCP request
608

    
609
        """
610
        indev = payload.get_indev()
611
        try:
612
            # Get the actual interface from the ifindex
613
            iface = self.ifaces[indev]
614
        except KeyError:
615
            logging.debug("Ignoring router solicitation on"
616
                          " unknown interface %d", indev)
617
            # We don't know what to do with this packet, so let the kernel
618
            # handle it
619
            payload.set_verdict(nfqueue.NF_ACCEPT)
620
            return
621

    
622
        ifmac = self.get_iface_hw_addr(iface)
623
        subnet = self.v6nets[iface]
624
        ifll = subnet.make_ll64(ifmac)
625

    
626
        # Signal the kernel that it shouldn't further process the packet
627
        payload.set_verdict(nfqueue.NF_DROP)
628

    
629
        resp = Ether(src=self.get_iface_hw_addr(iface))/\
630
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
631
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
632
                                     prefixlen=subnet.prefixlen)
633

    
634
        if self.ipv6_nameservers:
635
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
636
                                     lifetime=self.ra_period * 3)
637

    
638
        logging.info("RA on %s for %s", iface, subnet.net)
639
        self.sendp(resp, iface)
640

    
641
    def ns_response(self, i, payload): # pylint: disable=W0613
642
        """ Generate a reply to an ICMPv6 neighbor solicitation
643

    
644
        """
645
        indev = payload.get_indev()
646
        try:
647
            # Get the actual interface from the ifindex
648
            iface = self.ifaces[indev]
649
        except KeyError:
650
            logging.debug("Ignoring neighbour solicitation on"
651
                          " unknown interface %d", indev)
652
            # We don't know what to do with this packet, so let the kernel
653
            # handle it
654
            payload.set_verdict(nfqueue.NF_ACCEPT)
655
            return
656

    
657
        ifmac = self.get_iface_hw_addr(iface)
658
        subnet = self.v6nets[iface]
659
        ifll = subnet.make_ll64(ifmac)
660

    
661
        ns = IPv6(payload.get_data())
662

    
663
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
664
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
665
            payload.set_verdict(nfqueue.NF_ACCEPT)
666
            return 1
667

    
668
        payload.set_verdict(nfqueue.NF_DROP)
669

    
670
        try:
671
            client_lladdr = ns.lladdr
672
        except AttributeError:
673
            return 1
674

    
675
        resp = Ether(src=ifmac, dst=client_lladdr)/\
676
               IPv6(src=str(ifll), dst=ns.src)/\
677
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
678
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
679

    
680
        logging.info("NA on %s for %s", iface, ns.tgt)
681
        self.sendp(resp, iface)
682
        return 1
683

    
684
    def send_periodic_ra(self):
685
        # Use a separate thread as this may take a _long_ time with
686
        # many interfaces and we want to be responsive in the mean time
687
        threading.Thread(target=self._send_periodic_ra).start()
688

    
689
    def _send_periodic_ra(self):
690
        logging.debug("Sending out periodic RAs")
691
        start = time.time()
692
        i = 0
693
        for client in self.clients.values():
694
            iface = client.iface
695
            ifmac = self.get_iface_hw_addr(iface)
696
            if not ifmac:
697
                continue
698

    
699
            subnet = self.v6nets[iface]
700
            if subnet.net is None:
701
                logging.debug("Skipping periodic RA on interface %s,"
702
                              " as it is not IPv6-connected", iface)
703
                continue
704

    
705
            ifll = subnet.make_ll64(ifmac)
706
            resp = Ether(src=ifmac)/\
707
                   IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
708
                   ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
709
                                         prefixlen=subnet.prefixlen)
710
            if self.ipv6_nameservers:
711
                resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
712
                                         lifetime=self.ra_period * 3)
713
            try:
714
                self.sendp(resp, iface)
715
            except socket.error, e:
716
                logging.warn("Periodic RA on %s failed: %s", iface, str(e))
717
            except Exception, e:
718
                logging.warn("Unkown error during periodic RA on %s: %s",
719
                             iface, str(e))
720
            i += 1
721
        logging.debug("Sent %d RAs in %.2f seconds", i, time.time() - start)
722

    
723
    def serve(self):
724
        """ Safely perform the main loop, freeing all resources upon exit
725

    
726
        """
727
        try:
728
            self._serve()
729
        finally:
730
            self._cleanup()
731

    
732
    def _serve(self):
733
        """ Loop forever, serving DHCP requests
734

    
735
        """
736
        self.build_config()
737

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

    
742
        start = time.time()
743
        if self.ipv6_enabled:
744
            timeout = self.ra_period
745
            self.send_periodic_ra()
746
        else:
747
            timeout = None
748

    
749
        while True:
750
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
751
            if xlist:
752
                logging.warn("Warning: Exception on %s",
753
                             ", ".join([ str(fd) for fd in xlist]))
754

    
755
            if rlist:
756
                if iwfd in rlist:
757
                # First check if there are any inotify (= configuration change)
758
                # events
759
                    self.notifier.read_events()
760
                    self.notifier.process_events()
761
                    rlist.remove(iwfd)
762

    
763
                for fd in rlist:
764
                    try:
765
                        self.nfq[fd].process_pending()
766
                    except RuntimeError, e:
767
                        logging.warn("Error processing fd %d: %s", fd, str(e))
768
                    except Exception, e:
769
                        logging.warn("Unknown error processing fd %d: %s",
770
                                     fd, str(e))
771

    
772
            if self.ipv6_enabled:
773
                # Calculate the new timeout
774
                timeout = self.ra_period - (time.time() - start)
775

    
776
                if timeout <= 0:
777
                    start = time.time()
778
                    self.send_periodic_ra()
779
                    timeout = self.ra_period - (time.time() - start)
780

    
781

    
782
if __name__ == "__main__":
783
    import capng
784
    import optparse
785
    from cStringIO import StringIO
786
    from pwd import getpwnam, getpwuid
787
    from configobj import ConfigObj, ConfigObjError, flatten_errors
788

    
789
    import validate
790

    
791
    validator = validate.Validator()
792

    
793
    def is_ip_list(value, family=4):
794
        try:
795
            family = int(family)
796
        except ValueError:
797
            raise validate.VdtParamError(family)
798
        if isinstance(value, (str, unicode)):
799
            value = [value]
800
        if not isinstance(value, list):
801
            raise validate.VdtTypeError(value)
802

    
803
        for entry in value:
804
            try:
805
                ip = IPy.IP(entry)
806
            except ValueError:
807
                raise validate.VdtValueError(entry)
808

    
809
            if ip.version() != family:
810
                raise validate.VdtValueError(entry)
811
        return value
812

    
813
    validator.functions["ip_addr_list"] = is_ip_list
814
    config_spec = StringIO(CONFIG_SPEC)
815

    
816

    
817
    parser = optparse.OptionParser()
818
    parser.add_option("-c", "--config", dest="config_file",
819
                      help="The location of the data files", metavar="FILE",
820
                      default=DEFAULT_CONFIG)
821
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
822
                      help="Turn on debugging messages")
823
    parser.add_option("-f", "--foreground", action="store_false",
824
                      dest="daemonize", default=True,
825
                      help="Do not daemonize, stay in the foreground")
826

    
827

    
828
    opts, args = parser.parse_args()
829

    
830
    try:
831
        config = ConfigObj(opts.config_file, configspec=config_spec)
832
    except ConfigObjError, err:
833
        sys.stderr.write("Failed to parse config file %s: %s" %
834
                         (opts.config_file, str(err)))
835
        sys.exit(1)
836

    
837
    results = config.validate(validator)
838
    if results != True:
839
        logging.fatal("Configuration file validation failed! See errors below:")
840
        for (section_list, key, unused) in flatten_errors(config, results):
841
            if key is not None:
842
                logging.fatal(" '%s' in section '%s' failed validation",
843
                              key, ", ".join(section_list))
844
            else:
845
                logging.fatal(" Section '%s' is missing",
846
                              ", ".join(section_list))
847
        sys.exit(1)
848

    
849
    logger = logging.getLogger()
850
    if opts.debug:
851
        logger.setLevel(logging.DEBUG)
852
    else:
853
        logger.setLevel(logging.INFO)
854

    
855
    if opts.daemonize:
856
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
857
        handler = logging.handlers.RotatingFileHandler(logfile,
858
                                                       maxBytes=2097152)
859
    else:
860
        handler = logging.StreamHandler()
861

    
862
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
863
    logger.addHandler(handler)
864

    
865
    if opts.daemonize:
866
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile
867
            config["general"]["pidfile"], 10)
868

    
869
        d = daemon.DaemonContext(pidfile=pidfile,
870
                                 stdout=handler.stream,
871
                                 stderr=handler.stream,
872
                                 files_preserve=[handler.stream])
873
        d.umask = 0022
874
        d.open()
875

    
876
    logging.info("Starting up")
877

    
878
    proxy_opts = {}
879
    if config["dhcp"].as_bool("enable_dhcp"):
880
        proxy_opts.update({
881
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
882
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
883
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
884
            "dhcp_server_ip": config["dhcp"]["server_ip"],
885
            "dhcp_nameservers": config["dhcp"]["nameservers"],
886
        })
887

    
888
    if config["ipv6"].as_bool("enable_ipv6"):
889
        proxy_opts.update({
890
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
891
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
892
            "ra_period": config["ipv6"].as_int("ra_period"),
893
            "ipv6_nameservers": config["ipv6"]["nameservers"],
894
        })
895

    
896
    # pylint: disable=W0142
897
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
898

    
899
    # Drop all capabilities except CAP_NET_RAW and change uid
900
    try:
901
        uid = getpwuid(config["general"].as_int("user"))
902
    except ValueError:
903
        uid = getpwnam(config["general"]["user"])
904

    
905
    logging.debug("Setting capabilities and changing uid")
906
    logging.debug("User: %s, uid: %d, gid: %d",
907
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
908

    
909
    # Keep only the capabilities we need
910
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
911
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
912
    capng.capng_update(capng.CAPNG_ADD,
913
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
914
                       capng.CAP_NET_ADMIN)
915
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
916
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
917

    
918
    logging.info("Ready to serve requests")
919
    try:
920
        proxy.serve()
921
    except Exception:
922
        if opts.daemonize:
923
            exc = "".join(traceback.format_exception(*sys.exc_info()))
924
            logging.critical(exc)
925
        raise
926

    
927

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