Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 26ba9dba

History | View | Annotate | Download (29.7 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
domain = string(default=None)
83

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

    
92

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

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

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

    
119

    
120
def parse_routing_table(table="main", family=4):
121
    """ Parse the given routing table to get connected route, gateway and
122
    default device.
123

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

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

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

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

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

    
149
        def_net = m.group(1)
150

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

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

    
159

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

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

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

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

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

    
188

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

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

    
197
        Currently this removes an interface from the watch list
198

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

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

    
205
        Currently this adds an interface to the watch list
206

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

    
210

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

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

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

    
227

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
291

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

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

    
312
        if ipv6_nameservers is None:
313
            self.ipv6_nameservers = []
314
        else:
315
            self.ipv6_nameservers = ipv6_nameservers
316

    
317
        self.ipv6_enabled = False
318

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

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

    
336
        # NFQUEUE setup
337
        if dhcp_queue_num is not None:
338
            self._setup_nfqueue(dhcp_queue_num, AF_INET, self.dhcp_response)
339

    
340
        if rs_queue_num is not None:
341
            self._setup_nfqueue(rs_queue_num, AF_INET6, self.rs_response)
342
            self.ipv6_enabled = True
343

    
344
        if ns_queue_num is not None:
345
            self._setup_nfqueue(ns_queue_num, AF_INET6, self.ns_response)
346
            self.ipv6_enabled = True
347

    
348
    def _cleanup(self):
349
        """ Free all resources for a graceful exit
350

    
351
        """
352
        logging.info("Cleaning up")
353

    
354
        logging.debug("Closing netfilter queues")
355
        for q in self.nfq.values():
356
            q.close()
357

    
358
        logging.debug("Closing socket")
359
        self.l2socket.close()
360

    
361
        logging.debug("Stopping inotify watches")
362
        self.notifier.stop()
363

    
364
        logging.info("Cleanup finished")
365

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

    
377
    def sendp(self, data, iface):
378
        """ Send a raw packet using a layer-2 socket
379

    
380
        """
381
        if isinstance(data, BasePacket):
382
            data = str(data)
383

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

    
391
    def build_config(self):
392
        self.clients.clear()
393
        self.subnets.clear()
394

    
395
        for path in glob.glob(os.path.join(self.data_path, "*")):
396
            self.add_iface(path)
397

    
398
    def get_ifindex(self, iface):
399
        """ Get the interface index from sysfs
400

    
401
        """
402
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "ifindex"))
403
        if not path.startswith(SYSFS_NET):
404
            return None
405

    
406
        ifindex = None
407

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

    
414
            return ifindex
415

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

    
430
        return ifindex
431

    
432

    
433
    def get_iface_hw_addr(self, iface):
434
        """ Get the interface hardware address from sysfs
435

    
436
        """
437
        path = os.path.abspath(os.path.join(SYSFS_NET, iface, "address"))
438
        if not path.startswith(SYSFS_NET):
439
            return None
440

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

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

    
457
        return addr
458

    
459
    def add_iface(self, path):
460
        """ Add an interface to monitor
461

    
462
        """
463
        iface = os.path.basename(path)
464

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

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

    
482
    def remove_iface(self, iface):
483
        """ Cleanup clients on a removed interface
484

    
485
        """
486
        if iface in self.v6nets:
487
            del self.v6nets[iface]
488

    
489
        for mac in self.clients.keys():
490
            if self.clients[mac].iface == iface:
491
                del self.clients[mac]
492

    
493
        for ifindex in self.ifaces.keys():
494
            if self.ifaces[ifindex] == iface:
495
                del self.ifaces[ifindex]
496

    
497
        logging.debug("Removed interface %s", iface)
498

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

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

    
516
        # Decode the response - NFQUEUE relays IP packets
517
        pkt = IP(payload.get_data())
518

    
519
        # Signal the kernel that it shouldn't further process the packet
520
        payload.set_verdict(nfqueue.NF_DROP)
521

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

    
528
        # Server responses are always BOOTREPLYs
529
        resp.op = "BOOTREPLY"
530
        del resp.payload
531

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

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

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

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

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

    
561
        logging.info("%s from %s on %s", DHCP_TYPES.get(req_type, "UNKNOWN"),
562
                     binding.mac, iface)
563

    
564
        if self.dhcp_domain:
565
            domainname = self.dhcp_domain
566
        else:
567
            domainname = binding.hostname.split('.', 1)[-1]
568

    
569
        if req_type == DHCPREQUEST and requested_addr != binding.ip:
570
            resp_type = DHCPNAK
571
            logging.info("Sending DHCPNAK to %s on %s: requested %s"
572
                         " instead of %s", binding.mac, iface, requested_addr,
573
                         binding.ip)
574

    
575
        elif req_type in (DHCPDISCOVER, DHCPREQUEST):
576
            resp_type = DHCP_REQRESP[req_type]
577
            resp.yiaddr = self.clients[mac].ip
578
            dhcp_options += [
579
                 ("hostname", binding.hostname),
580
                 ("domain", domainname),
581
                 ("router", subnet.gw),
582
                 ("broadcast_address", str(subnet.broadcast)),
583
                 ("subnet_mask", str(subnet.netmask)),
584
                 ("renewal_time", self.lease_renewal),
585
                 ("lease_time", self.lease_lifetime),
586
            ]
587
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
588

    
589
        elif req_type == DHCPINFORM:
590
            resp_type = DHCP_REQRESP[req_type]
591
            dhcp_options += [
592
                 ("hostname", binding.hostname),
593
                 ("domain", domainname),
594
            ]
595
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
596

    
597
        elif req_type == DHCPRELEASE:
598
            # Log and ignore
599
            logging.info("DHCPRELEASE from %s on %s", binding.mac, iface)
600
            return
601

    
602
        # Finally, always add the server identifier and end options
603
        dhcp_options += [
604
            ("message-type", resp_type),
605
            ("server_id", DHCP_DUMMY_SERVER_IP),
606
            "end"
607
        ]
608
        resp /= DHCP(options=dhcp_options)
609

    
610
        logging.info("%s to %s (%s) on %s", DHCP_TYPES[resp_type], mac,
611
                     binding.ip, iface)
612
        self.sendp(resp, iface)
613

    
614
    def rs_response(self, i, payload): # pylint: disable=W0613
615
        """ Generate a reply to a BOOTP/DHCP request
616

    
617
        """
618
        indev = payload.get_indev()
619
        try:
620
            # Get the actual interface from the ifindex
621
            iface = self.ifaces[indev]
622
        except KeyError:
623
            logging.debug("Ignoring router solicitation on"
624
                          " unknown interface %d", indev)
625
            # We don't know what to do with this packet, so let the kernel
626
            # handle it
627
            payload.set_verdict(nfqueue.NF_ACCEPT)
628
            return
629

    
630
        ifmac = self.get_iface_hw_addr(iface)
631
        subnet = self.v6nets[iface]
632
        ifll = subnet.make_ll64(ifmac)
633

    
634
        # Signal the kernel that it shouldn't further process the packet
635
        payload.set_verdict(nfqueue.NF_DROP)
636

    
637
        resp = Ether(src=self.get_iface_hw_addr(iface))/\
638
               IPv6(src=str(ifll))/ICMPv6ND_RA(routerlifetime=14400)/\
639
               ICMPv6NDOptPrefixInfo(prefix=str(subnet.prefix),
640
                                     prefixlen=subnet.prefixlen)
641

    
642
        if self.ipv6_nameservers:
643
            resp /= ICMPv6NDOptRDNSS(dns=self.ipv6_nameservers,
644
                                     lifetime=self.ra_period * 3)
645

    
646
        logging.info("RA on %s for %s", iface, subnet.net)
647
        self.sendp(resp, iface)
648

    
649
    def ns_response(self, i, payload): # pylint: disable=W0613
650
        """ Generate a reply to an ICMPv6 neighbor solicitation
651

    
652
        """
653
        indev = payload.get_indev()
654
        try:
655
            # Get the actual interface from the ifindex
656
            iface = self.ifaces[indev]
657
        except KeyError:
658
            logging.debug("Ignoring neighbour solicitation on"
659
                          " unknown interface %d", indev)
660
            # We don't know what to do with this packet, so let the kernel
661
            # handle it
662
            payload.set_verdict(nfqueue.NF_ACCEPT)
663
            return
664

    
665
        ifmac = self.get_iface_hw_addr(iface)
666
        subnet = self.v6nets[iface]
667
        ifll = subnet.make_ll64(ifmac)
668

    
669
        ns = IPv6(payload.get_data())
670

    
671
        if not (subnet.net.overlaps(ns.tgt) or str(ns.tgt) == str(ifll)):
672
            logging.debug("Received NS for a non-routable IP (%s)", ns.tgt)
673
            payload.set_verdict(nfqueue.NF_ACCEPT)
674
            return 1
675

    
676
        payload.set_verdict(nfqueue.NF_DROP)
677

    
678
        try:
679
            client_lladdr = ns.lladdr
680
        except AttributeError:
681
            return 1
682

    
683
        resp = Ether(src=ifmac, dst=client_lladdr)/\
684
               IPv6(src=str(ifll), dst=ns.src)/\
685
               ICMPv6ND_NA(R=1, O=0, S=1, tgt=ns.tgt)/\
686
               ICMPv6NDOptDstLLAddr(lladdr=ifmac)
687

    
688
        logging.info("NA on %s for %s", iface, ns.tgt)
689
        self.sendp(resp, iface)
690
        return 1
691

    
692
    def send_periodic_ra(self):
693
        # Use a separate thread as this may take a _long_ time with
694
        # many interfaces and we want to be responsive in the mean time
695
        threading.Thread(target=self._send_periodic_ra).start()
696

    
697
    def _send_periodic_ra(self):
698
        logging.debug("Sending out periodic RAs")
699
        start = time.time()
700
        i = 0
701
        for client in self.clients.values():
702
            iface = client.iface
703
            ifmac = self.get_iface_hw_addr(iface)
704
            if not ifmac:
705
                continue
706

    
707
            subnet = self.v6nets[iface]
708
            if subnet.net is None:
709
                logging.debug("Skipping periodic RA on interface %s,"
710
                              " as it is not IPv6-connected", iface)
711
                continue
712

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

    
731
    def serve(self):
732
        """ Safely perform the main loop, freeing all resources upon exit
733

    
734
        """
735
        try:
736
            self._serve()
737
        finally:
738
            self._cleanup()
739

    
740
    def _serve(self):
741
        """ Loop forever, serving DHCP requests
742

    
743
        """
744
        self.build_config()
745

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

    
750
        start = time.time()
751
        if self.ipv6_enabled:
752
            timeout = self.ra_period
753
            self.send_periodic_ra()
754
        else:
755
            timeout = None
756

    
757
        while True:
758
            rlist, _, xlist = select(self.nfq.keys() + [iwfd], [], [], timeout)
759
            if xlist:
760
                logging.warn("Warning: Exception on %s",
761
                             ", ".join([ str(fd) for fd in xlist]))
762

    
763
            if rlist:
764
                if iwfd in rlist:
765
                # First check if there are any inotify (= configuration change)
766
                # events
767
                    self.notifier.read_events()
768
                    self.notifier.process_events()
769
                    rlist.remove(iwfd)
770

    
771
                for fd in rlist:
772
                    try:
773
                        self.nfq[fd].process_pending()
774
                    except RuntimeError, e:
775
                        logging.warn("Error processing fd %d: %s", fd, str(e))
776
                    except Exception, e:
777
                        logging.warn("Unknown error processing fd %d: %s",
778
                                     fd, str(e))
779

    
780
            if self.ipv6_enabled:
781
                # Calculate the new timeout
782
                timeout = self.ra_period - (time.time() - start)
783

    
784
                if timeout <= 0:
785
                    start = time.time()
786
                    self.send_periodic_ra()
787
                    timeout = self.ra_period - (time.time() - start)
788

    
789

    
790
if __name__ == "__main__":
791
    import capng
792
    import optparse
793
    from cStringIO import StringIO
794
    from pwd import getpwnam, getpwuid
795
    from configobj import ConfigObj, ConfigObjError, flatten_errors
796

    
797
    import validate
798

    
799
    validator = validate.Validator()
800

    
801
    def is_ip_list(value, family=4):
802
        try:
803
            family = int(family)
804
        except ValueError:
805
            raise validate.VdtParamError(family)
806
        if isinstance(value, (str, unicode)):
807
            value = [value]
808
        if not isinstance(value, list):
809
            raise validate.VdtTypeError(value)
810

    
811
        for entry in value:
812
            try:
813
                ip = IPy.IP(entry)
814
            except ValueError:
815
                raise validate.VdtValueError(entry)
816

    
817
            if ip.version() != family:
818
                raise validate.VdtValueError(entry)
819
        return value
820

    
821
    validator.functions["ip_addr_list"] = is_ip_list
822
    config_spec = StringIO(CONFIG_SPEC)
823

    
824

    
825
    parser = optparse.OptionParser()
826
    parser.add_option("-c", "--config", dest="config_file",
827
                      help="The location of the data files", metavar="FILE",
828
                      default=DEFAULT_CONFIG)
829
    parser.add_option("-d", "--debug", action="store_true", dest="debug",
830
                      help="Turn on debugging messages")
831
    parser.add_option("-f", "--foreground", action="store_false",
832
                      dest="daemonize", default=True,
833
                      help="Do not daemonize, stay in the foreground")
834

    
835

    
836
    opts, args = parser.parse_args()
837

    
838
    try:
839
        config = ConfigObj(opts.config_file, configspec=config_spec)
840
    except ConfigObjError, err:
841
        sys.stderr.write("Failed to parse config file %s: %s" %
842
                         (opts.config_file, str(err)))
843
        sys.exit(1)
844

    
845
    results = config.validate(validator)
846
    if results != True:
847
        logging.fatal("Configuration file validation failed! See errors below:")
848
        for (section_list, key, unused) in flatten_errors(config, results):
849
            if key is not None:
850
                logging.fatal(" '%s' in section '%s' failed validation",
851
                              key, ", ".join(section_list))
852
            else:
853
                logging.fatal(" Section '%s' is missing",
854
                              ", ".join(section_list))
855
        sys.exit(1)
856

    
857
    logger = logging.getLogger()
858
    if opts.debug:
859
        logger.setLevel(logging.DEBUG)
860
    else:
861
        logger.setLevel(logging.INFO)
862

    
863
    if opts.daemonize:
864
        logfile = os.path.join(config["general"]["logdir"], LOG_FILENAME)
865
        handler = logging.handlers.RotatingFileHandler(logfile,
866
                                                       maxBytes=2097152)
867
    else:
868
        handler = logging.StreamHandler()
869

    
870
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
871
    logger.addHandler(handler)
872

    
873
    if opts.daemonize:
874
        pidfile = daemon.pidlockfile.TimeoutPIDLockFile
875
            config["general"]["pidfile"], 10)
876

    
877
        d = daemon.DaemonContext(pidfile=pidfile,
878
                                 stdout=handler.stream,
879
                                 stderr=handler.stream,
880
                                 files_preserve=[handler.stream])
881
        d.umask = 0022
882
        d.open()
883

    
884
    logging.info("Starting up")
885

    
886
    proxy_opts = {}
887
    if config["dhcp"].as_bool("enable_dhcp"):
888
        proxy_opts.update({
889
            "dhcp_queue_num": config["dhcp"].as_int("dhcp_queue"),
890
            "dhcp_lease_lifetime": config["dhcp"].as_int("lease_lifetime"),
891
            "dhcp_lease_renewal": config["dhcp"].as_int("lease_renewal"),
892
            "dhcp_server_ip": config["dhcp"]["server_ip"],
893
            "dhcp_nameservers": config["dhcp"]["nameservers"],
894
            "dhcp_domain": config["dhcp"]["domain"],
895
        })
896

    
897
    if config["ipv6"].as_bool("enable_ipv6"):
898
        proxy_opts.update({
899
            "rs_queue_num": config["ipv6"].as_int("rs_queue"),
900
            "ns_queue_num": config["ipv6"].as_int("ns_queue"),
901
            "ra_period": config["ipv6"].as_int("ra_period"),
902
            "ipv6_nameservers": config["ipv6"]["nameservers"],
903
        })
904

    
905
    # pylint: disable=W0142
906
    proxy = VMNetProxy(data_path=config["general"]["datapath"], **proxy_opts)
907

    
908
    # Drop all capabilities except CAP_NET_RAW and change uid
909
    try:
910
        uid = getpwuid(config["general"].as_int("user"))
911
    except ValueError:
912
        uid = getpwnam(config["general"]["user"])
913

    
914
    logging.debug("Setting capabilities and changing uid")
915
    logging.debug("User: %s, uid: %d, gid: %d",
916
                  config["general"]["user"], uid.pw_uid, uid.pw_gid)
917

    
918
    # Keep only the capabilities we need
919
    # CAP_NET_ADMIN: we need to send nfqueue packet verdicts to a netlinkgroup
920
    capng.capng_clear(capng.CAPNG_SELECT_BOTH)
921
    capng.capng_update(capng.CAPNG_ADD,
922
                       capng.CAPNG_EFFECTIVE|capng.CAPNG_PERMITTED,
923
                       capng.CAP_NET_ADMIN)
924
    capng.capng_change_id(uid.pw_uid, uid.pw_gid,
925
                          capng.CAPNG_DROP_SUPP_GRP|capng.CAPNG_CLEAR_BOUNDING)
926

    
927
    logging.info("Ready to serve requests")
928
    try:
929
        proxy.serve()
930
    except Exception:
931
        if opts.daemonize:
932
            exc = "".join(traceback.format_exception(*sys.exc_info()))
933
            logging.critical(exc)
934
        raise
935

    
936

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