Statistics
| Branch: | Tag: | Revision:

root / nfdhcpd @ 99915273

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

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

    
93

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

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

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

    
120

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

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

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

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

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

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

    
150
        def_net = m.group(1)
151

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

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

    
160

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

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

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

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

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

    
189

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

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

    
198
        Currently this removes an interface from the watch list
199

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

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

    
206
        Currently this adds an interface to the watch list
207

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

    
211

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

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

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

    
228

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
292

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

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

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

    
318
        self.ipv6_enabled = False
319

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

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

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

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

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

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

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

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

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

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

    
365
        logging.info("Cleanup finished")
366

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

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

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

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

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

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

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

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

    
407
        ifindex = None
408

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

    
415
            return ifindex
416

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

    
431
        return ifindex
432

    
433

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

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

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

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

    
458
        return addr
459

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
590
        elif req_type == DHCPINFORM:
591
            resp_type = DHCP_REQRESP[req_type]
592
            dhcp_options += [
593
                 ("hostname", binding.hostname),
594
                 ("domain", domainname),
595

    
596
            ]
597
            dhcp_options += [("name_server", x) for x in self.dhcp_nameservers]
598

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

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

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

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

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

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

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

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

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

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

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

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

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

    
671
        ns = IPv6(payload.get_data())
672

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

    
678
        payload.set_verdict(nfqueue.NF_DROP)
679

    
680
        try:
681
            client_lladdr = ns.lladdr
682
        except AttributeError:
683
            return 1
684

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

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

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

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

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

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

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

    
736
        """
737
        try:
738
            self._serve()
739
        finally:
740
            self._cleanup()
741

    
742
    def _serve(self):
743
        """ Loop forever, serving DHCP requests
744

    
745
        """
746
        self.build_config()
747

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

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

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

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

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

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

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

    
791

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

    
799
    import validate
800

    
801
    validator = validate.Validator()
802

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

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

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

    
823
    validator.functions["ip_addr_list"] = is_ip_list
824
    config_spec = StringIO(CONFIG_SPEC)
825

    
826

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

    
837

    
838
    opts, args = parser.parse_args()
839

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

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

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

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

    
872
    handler.setFormatter(logging.Formatter(LOG_FORMAT))
873
    logger.addHandler(handler)
874

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

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

    
886
    logging.info("Starting up")
887

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

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

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

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

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

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

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

    
938

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